diff --git a/lib/assets/images/categories/business.png b/lib/assets/images/categories/business.png new file mode 100644 index 0000000..0470a94 Binary files /dev/null and b/lib/assets/images/categories/business.png differ diff --git a/lib/assets/images/categories/economic.png b/lib/assets/images/categories/economic.png new file mode 100644 index 0000000..44f67fb Binary files /dev/null and b/lib/assets/images/categories/economic.png differ diff --git a/lib/assets/images/categories/enviromental.png b/lib/assets/images/categories/enviromental.png new file mode 100644 index 0000000..f56f648 Binary files /dev/null and b/lib/assets/images/categories/enviromental.png differ diff --git a/lib/assets/images/categories/political.png b/lib/assets/images/categories/political.png new file mode 100644 index 0000000..06ea42a Binary files /dev/null and b/lib/assets/images/categories/political.png differ diff --git a/lib/assets/images/categories/social.png b/lib/assets/images/categories/social.png new file mode 100644 index 0000000..2b4e4ea Binary files /dev/null and b/lib/assets/images/categories/social.png differ diff --git a/lib/assets/images/categories/tech.png b/lib/assets/images/categories/tech.png new file mode 100644 index 0000000..76bdc49 Binary files /dev/null and b/lib/assets/images/categories/tech.png differ diff --git a/lib/assets/images/logo/logo-h-t.png b/lib/assets/images/logos/logo-h-t.png similarity index 100% rename from lib/assets/images/logo/logo-h-t.png rename to lib/assets/images/logos/logo-h-t.png diff --git a/lib/assets/images/logo/logo-v-t.png b/lib/assets/images/logos/logo-v-t.png similarity index 100% rename from lib/assets/images/logo/logo-v-t.png rename to lib/assets/images/logos/logo-v-t.png diff --git a/lib/config/design_config.dart b/lib/config/design_config.dart index 57e99fe..493aba0 100644 --- a/lib/config/design_config.dart +++ b/lib/config/design_config.dart @@ -60,9 +60,28 @@ class DesignConfig { static const BorderRadius lowBorderRadius = BorderRadius.all( Radius.circular(8), ); + static const BorderRadius mediumBorderRadius = BorderRadius.all( + Radius.circular(10), + ); + static const BorderRadius highBorderRadius = BorderRadius.all( + Radius.circular(16), + ); static final Border lightBorder = Border.all(color: DesignConfig.greyColor4); + static final BoxDecoration actionCardDecoration = BoxDecoration( + color: lightPrimaryColor3, + boxShadow: defaultShadow, + borderRadius: mediumBorderRadius, + ); + static final List defaultShadow = [ + BoxShadow( + color: const Color(0XFF4D4D4D).withOpacity(0.25), + offset: const Offset(0, 4), + blurRadius: 8, + spreadRadius: 0, + ) + ]; - static const Duration defaultAppDuration = Duration(milliseconds: 400); + static const Duration defaultAnimationDuration = Duration(milliseconds: 600); static final SystemUiOverlayStyle systemUIOverlayStyle = SystemUiOverlayStyle( statusBarBrightness: Brightness.dark, diff --git a/lib/constants/assets.dart b/lib/constants/assets.dart index 029a236..59af3fe 100644 --- a/lib/constants/assets.dart +++ b/lib/constants/assets.dart @@ -4,10 +4,21 @@ class Assets { static const String _baseAnimationsPath = _basePath + '/animations'; static const String verticalLogoWithText = - _baseImagesPath + '/logo/logo-v-t.png'; + _baseImagesPath + '/logos/logo-v-t.png'; static const String horizontalLogoWithText = - _baseImagesPath + '/logo/logo-h-t.png'; + _baseImagesPath + '/logos/logo-h-t.png'; static const String logoLoadingAnimation = _baseAnimationsPath + '/indicator.riv'; + + static const businessCategoryIcon = + _baseImagesPath + '/categories/business.png'; + static const economicCategoryIcon = + _baseImagesPath + '/categories/economic.png'; + static const enviromentalCategoryIcon = + _baseImagesPath + '/categories/enviromental.png'; + static const politicalCategoryIcon = + _baseImagesPath + '/categories/political.png'; + static const socialCategoryIcon = _baseImagesPath + '/categories/social.png'; + static const techCategoryIcon = _baseImagesPath + '/categories/tech.png'; } diff --git a/lib/models/radar_category.dart b/lib/models/radar_category.dart new file mode 100644 index 0000000..4312d46 --- /dev/null +++ b/lib/models/radar_category.dart @@ -0,0 +1,7 @@ +class RadarCategory { + final int id; + final String title; + final String asset; + + RadarCategory({required this.id, required this.title, required this.asset}); +} diff --git a/lib/pages/authentication/authentication.dart b/lib/pages/authentication/authentication.dart index f4a94e2..ade5c92 100644 --- a/lib/pages/authentication/authentication.dart +++ b/lib/pages/authentication/authentication.dart @@ -27,7 +27,7 @@ class _AuthenticationState extends State { return Scaffold( body: Consumer( builder: (context, state, child) => AnimatedSwitcher( - duration: DesignConfig.defaultAppDuration, + duration: DesignConfig.defaultAnimationDuration, child: _pages[state.currentPageIndex], ), ), diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart new file mode 100644 index 0000000..1baa948 --- /dev/null +++ b/lib/pages/home/home.dart @@ -0,0 +1,99 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/pages/home/widgets/categories_gird.dart'; +import 'package:didvan/pages/home/widgets/categories_list.dart'; +import 'package:didvan/pages/home/widgets/search_field.dart'; +import 'package:didvan/widgets/didvan/text.dart'; +import 'package:didvan/widgets/logos/didvan_vertical_logo.dart'; +import 'package:flutter/material.dart'; + +class Home extends StatefulWidget { + const Home({Key? key}) : super(key: key); + + @override + State createState() => _HomeState(); +} + +class _HomeState extends State { + final ScrollController _scrollController = ScrollController(); + + bool _isColapsed = false; + bool _isAnimating = false; + + @override + void initState() { + _scrollController.addListener(() async { + if (_isAnimating) return; + final double position = _scrollController.position.pixels; + if (position > 5 && !_isColapsed) { + _isColapsed = true; + setState(() {}); + _isAnimating = true; + await _scrollController.animateTo( + 380, + duration: DesignConfig.defaultAnimationDuration, + curve: Curves.ease, + ); + _isAnimating = false; + } else if (position < 380 && _isColapsed) { + _isColapsed = false; + setState(() {}); + _isAnimating = true; + await _scrollController.animateTo( + 0, + duration: DesignConfig.defaultAnimationDuration, + curve: Curves.ease, + ); + _isAnimating = false; + } + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final d = MediaQuery.of(context); + return Scaffold( + body: Stack( + children: [ + CustomScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics(), + slivers: [ + SliverPadding( + padding: const EdgeInsets.all( + 20, + ).copyWith(top: d.padding.top + 20), + sliver: SliverToBoxAdapter( + child: Container( + alignment: Alignment.centerRight, + height: 76, + child: const DidvanHorizontalLogo(), + ), + ), + ), + const SliverPadding( + padding: EdgeInsets.symmetric(horizontal: 16), + sliver: SliverToBoxAdapter( + child: SearchField(), + ), + ), + SliverPadding( + padding: const EdgeInsets.only(top: 300, right: 16, bottom: 20), + sliver: SliverToBoxAdapter( + child: DidvanText( + 'آخرین رصد', + style: Theme.of(context).textTheme.subtitle1, + color: DesignConfig.darkPrimaryColor2, + ), + ), + ), + ], + ), + CategoriesRow1(isColapsed: _isColapsed), + CategoriesRow2(isColapsed: _isColapsed), + CategoriesList(isColapsed: _isColapsed), + ], + ), + ); + } +} diff --git a/lib/pages/home/home_state.dart b/lib/pages/home/home_state.dart new file mode 100644 index 0000000..95c5840 --- /dev/null +++ b/lib/pages/home/home_state.dart @@ -0,0 +1,38 @@ +import 'package:didvan/constants/assets.dart'; +import 'package:didvan/models/radar_category.dart'; +import 'package:didvan/providers/core_provider.dart'; + +class HomeState extends CoreProvier { + final List categories = [ + RadarCategory( + id: 1, + title: 'افتصادی', + asset: Assets.economicCategoryIcon, + ), + RadarCategory( + id: 2, + title: 'سیاسی', + asset: Assets.politicalCategoryIcon, + ), + RadarCategory( + id: 3, + title: 'فناوری', + asset: Assets.techCategoryIcon, + ), + RadarCategory( + id: 4, + title: 'کسب و کار', + asset: Assets.businessCategoryIcon, + ), + RadarCategory( + id: 5, + title: 'زیست محیطی', + asset: Assets.enviromentalCategoryIcon, + ), + RadarCategory( + id: 6, + title: 'اجتماعی', + asset: Assets.socialCategoryIcon, + ), + ]; +} diff --git a/lib/pages/home/widgets/categories_gird.dart b/lib/pages/home/widgets/categories_gird.dart new file mode 100644 index 0000000..fe7ae1b --- /dev/null +++ b/lib/pages/home/widgets/categories_gird.dart @@ -0,0 +1,76 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/pages/home/home_state.dart'; +import 'package:didvan/pages/home/widgets/category_item.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class CategoriesRow1 extends StatelessWidget { + final bool isColapsed; + const CategoriesRow1({Key? key, required this.isColapsed}) : super(key: key); + + @override + Widget build(BuildContext context) { + final MediaQueryData d = MediaQuery.of(context); + return AnimatedPositioned( + duration: DesignConfig.defaultAnimationDuration, + top: isColapsed ? 12 : 176 + d.padding.top, + left: isColapsed ? -d.size.width : 0, + right: isColapsed ? d.size.width : 0, + child: Row( + children: context + .read() + .categories + .sublist(0, 3) + .map( + (category) => Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: CategoryItem( + title: category.title, + asset: category.asset, + isColapsed: isColapsed, + ), + ), + ), + ) + .toList(), + ), + ); + } +} + +class CategoriesRow2 extends StatelessWidget { + const CategoriesRow2({ + Key? key, + required this.isColapsed, + }) : super(key: key); + + final bool isColapsed; + + @override + Widget build(BuildContext context) { + final MediaQueryData d = MediaQuery.of(context); + return AnimatedPositioned( + duration: DesignConfig.defaultAnimationDuration, + top: isColapsed ? -60 : 300 + d.padding.top, + left: isColapsed ? -80 : 0, + right: isColapsed ? 124 : 0, + child: Row( + children: context + .read() + .categories + .sublist(3, 6) + .map( + (category) => Expanded( + child: CategoryItem( + title: category.title, + asset: category.asset, + isColapsed: isColapsed, + ), + ), + ) + .toList(), + ), + ); + } +} diff --git a/lib/pages/home/widgets/categories_list.dart b/lib/pages/home/widgets/categories_list.dart new file mode 100644 index 0000000..b57ddc3 --- /dev/null +++ b/lib/pages/home/widgets/categories_list.dart @@ -0,0 +1,76 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/models/radar_category.dart'; +import 'package:didvan/pages/home/home_state.dart'; +import 'package:didvan/widgets/animated_visibility.dart'; +import 'package:didvan/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class CategoriesList extends StatelessWidget { + final bool isColapsed; + const CategoriesList({Key? key, required this.isColapsed}) : super(key: key); + + @override + Widget build(BuildContext context) { + final MediaQueryData d = MediaQuery.of(context); + final HomeState state = context.read(); + final List categories = []; + categories.addAll(state.categories.sublist(3, 6)); + categories.addAll(state.categories.sublist(0, 3)); + return Positioned( + top: 0, + left: 0, + right: 0, + child: AnimatedCrossFade( + crossFadeState: + isColapsed ? CrossFadeState.showSecond : CrossFadeState.showFirst, + duration: DesignConfig.defaultAnimationDuration, + firstChild: const SizedBox(), + secondChild: Container( + height: 60 + d.padding.top, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: DesignConfig.defaultShadow, + ), + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + padding: EdgeInsets.only( + top: d.padding.top + 12, + bottom: 12, + right: 12, + ), + child: Row( + children: [ + AnimatedVisibility( + isVisible: isColapsed, + duration: DesignConfig.defaultAnimationDuration, + child: _itemBuilder( + RadarCategory(title: 'همه', asset: '', id: 0), + ), + ), + for (var category in categories) _itemBuilder(category), + ], + ), + ), + ), + ), + ); + } + + Widget _itemBuilder(RadarCategory category) { + return Container( + margin: const EdgeInsets.only(left: 12), + width: 100, + padding: const EdgeInsets.all(4), + alignment: Alignment.center, + child: DidvanText(category.title), + decoration: BoxDecoration( + border: Border.all( + color: DesignConfig.darkPrimaryColor2, + ), + borderRadius: DesignConfig.lowBorderRadius, + ), + ); + } +} diff --git a/lib/pages/home/widgets/category_item.dart b/lib/pages/home/widgets/category_item.dart new file mode 100644 index 0000000..48560ae --- /dev/null +++ b/lib/pages/home/widgets/category_item.dart @@ -0,0 +1,61 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/widgets/animated_visibility.dart'; +import 'package:didvan/widgets/didvan/text.dart'; +import 'package:flutter/material.dart'; + +class CategoryItem extends StatelessWidget { + final String title; + final String asset; + final bool isColapsed; + + const CategoryItem({ + Key? key, + required this.title, + required this.asset, + required this.isColapsed, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final Size ds = MediaQuery.of(context).size; + return Center( + child: AnimatedContainer( + duration: DesignConfig.defaultAnimationDuration, + padding: isColapsed ? const EdgeInsets.all(4) : EdgeInsets.zero, + width: isColapsed ? 100 : ds.width / 3, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: DesignConfig.lowBorderRadius, + border: isColapsed + ? Border.all(color: DesignConfig.darkPrimaryColor2) + : null, + ), + child: Column( + children: [ + AnimatedVisibility( + duration: DesignConfig.defaultAnimationDuration, + isVisible: !isColapsed, + child: Container( + width: ds.width / 5, + height: ds.width / 5, + decoration: DesignConfig.actionCardDecoration, + padding: const EdgeInsets.all(8), + child: Image.asset( + asset, + ), + ), + ), + const SizedBox( + height: 8, + ), + DidvanText( + title, + style: Theme.of(context).textTheme.subtitle2, + color: DesignConfig.darkPrimaryColor2, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/home/widgets/search_field.dart b/lib/pages/home/widgets/search_field.dart new file mode 100644 index 0000000..f68d931 --- /dev/null +++ b/lib/pages/home/widgets/search_field.dart @@ -0,0 +1,47 @@ +import 'package:didvan/config/design_config.dart'; +import 'package:didvan/constants/app_icons.dart'; +import 'package:flutter/material.dart'; + +class SearchField extends StatelessWidget { + const SearchField({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: 40, + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + ), + child: Row( + children: [ + const Icon( + DidvanIcons.search_regular, + ), + Expanded( + child: TextField( + style: Theme.of(context).textTheme.bodyText1, + textAlignVertical: TextAlignVertical.top, + onChanged: (value) {}, + keyboardType: TextInputType.text, + textInputAction: TextInputAction.search, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + left: 12, + right: 12, + bottom: 8, + ), + border: InputBorder.none, + hintText: 'جستجو مطلب در رادار', + hintStyle: Theme.of(context) + .textTheme + .subtitle2! + .copyWith(color: DesignConfig.greyColor5), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/splash/splash.dart b/lib/pages/splash/splash.dart index da8d1aa..74195cc 100644 --- a/lib/pages/splash/splash.dart +++ b/lib/pages/splash/splash.dart @@ -16,7 +16,7 @@ class _SplashState extends State { Future.delayed( const Duration(seconds: 2), () => Navigator.of(context).pushReplacementNamed( - Routes.authenticaion, + Routes.home, ), ); super.initState(); diff --git a/lib/routes/route_generator.dart b/lib/routes/route_generator.dart index f55e042..2b5479f 100644 --- a/lib/routes/route_generator.dart +++ b/lib/routes/route_generator.dart @@ -1,5 +1,7 @@ import 'package:didvan/pages/authentication/authentication.dart'; import 'package:didvan/pages/authentication/authentication_state.dart'; +import 'package:didvan/pages/home/home.dart'; +import 'package:didvan/pages/home/home_state.dart'; import 'package:didvan/pages/splash/splash.dart'; import 'package:didvan/pages/splash/splash_state.dart'; import 'package:didvan/routes/routes.dart'; @@ -11,18 +13,25 @@ class RouteGenerator { switch (settings.name) { case Routes.splash: return _materialPageRouteGenerator( - ChangeNotifierProvider( + ChangeNotifierProvider( create: (context) => SplashState(), child: const Splash(), ), ); case Routes.authenticaion: return _materialPageRouteGenerator( - ChangeNotifierProvider( + ChangeNotifierProvider( create: (context) => AuthenticationState(), child: const Authentication(), ), ); + case Routes.home: + return _materialPageRouteGenerator( + ChangeNotifierProvider( + create: (context) => HomeState(), + child: const Home(), + ), + ); default: return _errorRoute(); } diff --git a/lib/widgets/animated_visibility.dart b/lib/widgets/animated_visibility.dart new file mode 100644 index 0000000..a6f3c12 --- /dev/null +++ b/lib/widgets/animated_visibility.dart @@ -0,0 +1,90 @@ +library animated_visibility; + +import 'package:flutter/material.dart'; + +enum FadeMode { + vertical, + horizontal, + both, +} + +class AnimatedVisibility extends StatefulWidget { + final bool isVisible; + final Duration duration; + final Widget child; + final Curve curve; + final FadeMode fadeMode; + + const AnimatedVisibility({ + Key? key, + required this.isVisible, + required this.duration, + required this.child, + this.fadeMode = FadeMode.both, + this.curve = Curves.easeIn, + }) : super(key: key); + + @override + _AnimatedVisibilityState createState() => _AnimatedVisibilityState(); +} + +class _AnimatedVisibilityState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _sizeController; + late final Animation _animation; + + @override + void initState() { + super.initState(); + _setupAnimation(); + _runSizeCheck(); + } + + void _setupAnimation() { + _sizeController = + AnimationController(vsync: this, duration: widget.duration); + _animation = CurvedAnimation( + parent: _sizeController, + curve: widget.curve, + ); + } + + @override + void didUpdateWidget(covariant AnimatedVisibility oldWidget) { + _runSizeCheck(); + super.didUpdateWidget(oldWidget); + } + + void _runSizeCheck() { + if (widget.isVisible) { + _sizeController.forward(); + } else { + _sizeController.reverse(); + } + } + + @override + void dispose() { + _sizeController.dispose(); + super.dispose(); + } + + bool get _isVertical => + widget.fadeMode == FadeMode.vertical || widget.fadeMode == FadeMode.both; + bool get _isHorizontal => + widget.fadeMode == FadeMode.horizontal || + widget.fadeMode == FadeMode.both; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + child: widget.child, + builder: (context, child) => Align( + heightFactor: _isVertical ? _animation.value : null, + widthFactor: _isHorizontal ? _animation.value : null, + child: Opacity(opacity: _animation.value, child: child), + ), + ); + } +} diff --git a/lib/widgets/didvan/text.dart b/lib/widgets/didvan/text.dart index 0b05d6e..2192f4d 100644 --- a/lib/widgets/didvan/text.dart +++ b/lib/widgets/didvan/text.dart @@ -7,6 +7,7 @@ class DidvanText extends StatelessWidget { final Color? color; final FontWeight? fontWeight; final double? fontSize; + final TextAlign textAlign; const DidvanText( this.text, { @@ -15,6 +16,7 @@ class DidvanText extends StatelessWidget { this.color, this.fontSize, this.fontWeight, + this.textAlign = TextAlign.right, }) : super(key: key); @override @@ -26,6 +28,7 @@ class DidvanText extends StatelessWidget { fontWeight: fontWeight, fontSize: fontSize, ), + textAlign: textAlign, ); } } diff --git a/lib/widgets/didvan/text_field.dart b/lib/widgets/didvan/text_field.dart index 02e141e..9580aac 100644 --- a/lib/widgets/didvan/text_field.dart +++ b/lib/widgets/didvan/text_field.dart @@ -133,5 +133,7 @@ class _DidvanTextFieldState extends State { } } - String? _validator(String? value) {} + String? _validator(String? value) { + _hasError = false; + } } diff --git a/lib/widgets/ink_wrapper.dart b/lib/widgets/ink_wrapper.dart new file mode 100644 index 0000000..547709d --- /dev/null +++ b/lib/widgets/ink_wrapper.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class InkWrapper extends StatelessWidget { + final Color? splashColor; + final Color? highlightColor; + final Widget child; + final VoidCallback? onPressed; + final BorderRadius? borderRadius; + + const InkWrapper({ + Key? key, + this.splashColor, + this.highlightColor, + required this.child, + this.onPressed, + this.borderRadius, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + child, + Positioned.fill( + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: borderRadius, + splashColor: splashColor, + highlightColor: highlightColor, + onTap: onPressed, + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/logos/didvan_vertical_logo.dart b/lib/widgets/logos/didvan_vertical_logo.dart index e69de29..19e640e 100644 --- a/lib/widgets/logos/didvan_vertical_logo.dart +++ b/lib/widgets/logos/didvan_vertical_logo.dart @@ -0,0 +1,11 @@ +import 'package:didvan/constants/assets.dart'; +import 'package:flutter/material.dart'; + +class DidvanHorizontalLogo extends StatelessWidget { + const DidvanHorizontalLogo({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Image.asset(Assets.horizontalLogoWithText); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 9e56d39..596b680 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,8 +64,15 @@ flutter: # To add assets to your application, add an assets section, like this: assets: - - lib/assets/images/logo/logo-v-t.png - - lib/assets/images/logo/logo-h-t.png + - lib/assets/images/logos/logo-v-t.png + - lib/assets/images/logos/logo-h-t.png + - lib/assets/images/categories/business.png + - lib/assets/images/categories/economic.png + - lib/assets/images/categories/enviromental.png + - lib/assets/images/categories/political.png + - lib/assets/images/categories/social.png + - lib/assets/images/categories/tech.png + - lib/assets/animations/indicator.riv - lib/assets/animations/indicator.riv