├── dart_test.yaml ├── example ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ ├── ephemeral │ │ │ ├── flutter_lldbinit │ │ │ └── flutter_lldb_helper.py │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── Podfile.lock │ ├── .gitignore │ └── Podfile ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ └── Icon-512.png │ ├── manifest.json │ └── index.html ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── .metadata ├── README.md ├── .gitignore ├── test │ └── widget_test.dart ├── pubspec.yaml └── lib │ └── main.dart ├── demo ├── scale.gif ├── slide.gif ├── swap.gif ├── worm.gif ├── pub_thumb.png ├── worm-thin.gif ├── jumping-dot.gif ├── custimizable-1.gif ├── customizable-2.gif ├── customizable-3.gif ├── customizable-4.gif ├── expanding-dot.gif ├── swap-yrotation.gif ├── vertical_demo.gif ├── color-transition.gif ├── four_squares_demo.gif ├── scrolling-dots-2.gif ├── slide_under_demo.gif ├── worm_underground_demo.gif ├── thin_worm_underground_demo.gif ├── smooth_page_indicator_demo_1.gif ├── smooth_page_indicator_demo_2.gif ├── smooth_page_indicator_demo_3.gif ├── smooth_page_indicator_demo_4.gif ├── jumping-dot-effect-with-voffset.gif └── smooth_page_indicator_demo_loop.gif ├── analysis_options.yaml ├── test └── src │ ├── painters │ ├── goldens │ │ ├── ci │ │ │ ├── slide_types.png │ │ │ ├── swap_types.png │ │ │ ├── worm_colors.png │ │ │ ├── worm_types.png │ │ │ ├── scale_offsets.png │ │ │ ├── scale_values.png │ │ │ ├── slide_colors.png │ │ │ ├── slide_offsets.png │ │ │ ├── swap_offsets.png │ │ │ ├── worm_offsets.png │ │ │ ├── jumping_dot_scales.png │ │ │ ├── scale_paint_styles.png │ │ │ ├── swap_types_offsets.png │ │ │ ├── customizable_offsets.png │ │ │ ├── customizable_rotation.png │ │ │ ├── expanding_dots_colors.png │ │ │ ├── jumping_dot_offsets.png │ │ │ ├── customizable_dot_sizes.png │ │ │ ├── expanding_dots_offsets.png │ │ │ ├── worm_types_high_offset.png │ │ │ ├── color_transition_offsets.png │ │ │ ├── customizable_border_radius.png │ │ │ ├── color_transition_dimensions.png │ │ │ ├── color_transition_dot_counts.png │ │ │ ├── customizable_vertical_offset.png │ │ │ ├── jumping_dot_vertical_offsets.png │ │ │ ├── color_transition_custom_colors.png │ │ │ ├── color_transition_stroke_style.png │ │ │ ├── expanding_dots_expansion_factors.png │ │ │ ├── scrolling_dots_fixed_center_offsets.png │ │ │ ├── scrolling_dots_fixed_center_scales.png │ │ │ └── scrolling_dots_fixed_center_max_visible.png │ │ └── macos │ │ │ ├── swap_types.png │ │ │ ├── worm_types.png │ │ │ ├── scale_offsets.png │ │ │ ├── scale_values.png │ │ │ ├── slide_colors.png │ │ │ ├── slide_offsets.png │ │ │ ├── slide_types.png │ │ │ ├── swap_offsets.png │ │ │ ├── worm_colors.png │ │ │ ├── worm_offsets.png │ │ │ ├── jumping_dot_scales.png │ │ │ ├── scale_paint_styles.png │ │ │ ├── swap_types_offsets.png │ │ │ ├── customizable_offsets.png │ │ │ ├── jumping_dot_offsets.png │ │ │ ├── customizable_dot_sizes.png │ │ │ ├── customizable_rotation.png │ │ │ ├── expanding_dots_colors.png │ │ │ ├── expanding_dots_offsets.png │ │ │ ├── worm_types_high_offset.png │ │ │ ├── color_transition_offsets.png │ │ │ ├── color_transition_dimensions.png │ │ │ ├── color_transition_dot_counts.png │ │ │ ├── customizable_border_radius.png │ │ │ ├── customizable_vertical_offset.png │ │ │ ├── jumping_dot_vertical_offsets.png │ │ │ ├── color_transition_custom_colors.png │ │ │ ├── color_transition_stroke_style.png │ │ │ ├── expanding_dots_expansion_factors.png │ │ │ ├── scrolling_dots_fixed_center_offsets.png │ │ │ ├── scrolling_dots_fixed_center_scales.png │ │ │ └── scrolling_dots_fixed_center_max_visible.png │ └── indicator_painter_test.dart │ └── effects │ ├── indicator_effect_test.dart │ ├── color_transition_effect_test.dart │ ├── jumping_dot_effect_test.dart │ ├── expanding_dots_effect_test.dart │ ├── slide_effect_test.dart │ ├── scale_effect_test.dart │ ├── swap_effect_test.dart │ ├── scrolling_dots_effect_test.dart │ └── worm_effect_test.dart ├── .metadata ├── .pubignore ├── lib ├── smooth_page_indicator.dart └── src │ ├── effects │ ├── color_transition_effect.dart │ ├── slide_effect.dart │ ├── worm_effect.dart │ ├── jumping_dot_effect.dart │ ├── swap_effect.dart │ ├── expanding_dots_effect.dart │ ├── scale_effect.dart │ ├── indicator_effect.dart │ ├── scrolling_dots_effect.dart │ └── customizable_effect.dart │ ├── painters │ ├── slide_painter.dart │ ├── color_transition_painter.dart │ ├── expanding_dots_painter.dart │ ├── scale_painter.dart │ ├── jumping_dot_painter.dart │ ├── worm_painter.dart │ ├── scrolling_dots_painter_with_fixed_center.dart │ ├── swap_painter.dart │ ├── indicator_painter.dart │ ├── scrolling_dots_painter.dart │ └── customizable_painter.dart │ └── theme_defaults.dart ├── pubspec.yaml ├── .github └── workflows │ └── coverage.yml ├── LICENSE ├── .gitignore ├── run_coverage.sh ├── CHANGELOG.md ├── pubspec.lock └── README.md /dart_test.yaml: -------------------------------------------------------------------------------- 1 | tags: 2 | golden: 3 | 4 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /demo/scale.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/scale.gif -------------------------------------------------------------------------------- /demo/slide.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/slide.gif -------------------------------------------------------------------------------- /demo/swap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/swap.gif -------------------------------------------------------------------------------- /demo/worm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/worm.gif -------------------------------------------------------------------------------- /demo/pub_thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/pub_thumb.png -------------------------------------------------------------------------------- /demo/worm-thin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/worm-thin.gif -------------------------------------------------------------------------------- /demo/jumping-dot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/jumping-dot.gif -------------------------------------------------------------------------------- /demo/custimizable-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/custimizable-1.gif -------------------------------------------------------------------------------- /demo/customizable-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/customizable-2.gif -------------------------------------------------------------------------------- /demo/customizable-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/customizable-3.gif -------------------------------------------------------------------------------- /demo/customizable-4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/customizable-4.gif -------------------------------------------------------------------------------- /demo/expanding-dot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/expanding-dot.gif -------------------------------------------------------------------------------- /demo/swap-yrotation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/swap-yrotation.gif -------------------------------------------------------------------------------- /demo/vertical_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/vertical_demo.gif -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/web/favicon.png -------------------------------------------------------------------------------- /demo/color-transition.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/color-transition.gif -------------------------------------------------------------------------------- /demo/four_squares_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/four_squares_demo.gif -------------------------------------------------------------------------------- /demo/scrolling-dots-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/scrolling-dots-2.gif -------------------------------------------------------------------------------- /demo/slide_under_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/slide_under_demo.gif -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | linter: 4 | rules: 5 | public_member_api_docs: true -------------------------------------------------------------------------------- /demo/worm_underground_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/worm_underground_demo.gif -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /demo/thin_worm_underground_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/thin_worm_underground_demo.gif -------------------------------------------------------------------------------- /demo/smooth_page_indicator_demo_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/smooth_page_indicator_demo_1.gif -------------------------------------------------------------------------------- /demo/smooth_page_indicator_demo_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/smooth_page_indicator_demo_2.gif -------------------------------------------------------------------------------- /demo/smooth_page_indicator_demo_3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/smooth_page_indicator_demo_3.gif -------------------------------------------------------------------------------- /demo/smooth_page_indicator_demo_4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/smooth_page_indicator_demo_4.gif -------------------------------------------------------------------------------- /demo/jumping-dot-effect-with-voffset.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/jumping-dot-effect-with-voffset.gif -------------------------------------------------------------------------------- /demo/smooth_page_indicator_demo_loop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/demo/smooth_page_indicator_demo_loop.gif -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/slide_types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/slide_types.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/swap_types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/swap_types.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/worm_colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/worm_colors.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/worm_types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/worm_types.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/scale_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/scale_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/scale_values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/scale_values.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/slide_colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/slide_colors.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/slide_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/slide_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/swap_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/swap_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/worm_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/worm_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/swap_types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/swap_types.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/worm_types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/worm_types.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/scale_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/scale_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/scale_values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/scale_values.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/slide_colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/slide_colors.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/slide_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/slide_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/slide_types.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/slide_types.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/swap_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/swap_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/worm_colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/worm_colors.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/worm_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/worm_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/jumping_dot_scales.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/jumping_dot_scales.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/scale_paint_styles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/scale_paint_styles.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/swap_types_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/swap_types_offsets.png -------------------------------------------------------------------------------- /example/ios/Flutter/ephemeral/flutter_lldbinit: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | command script import --relative-to-command-file flutter_lldb_helper.py 6 | -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/customizable_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/customizable_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/customizable_rotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/customizable_rotation.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/expanding_dots_colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/expanding_dots_colors.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/jumping_dot_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/jumping_dot_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/jumping_dot_scales.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/jumping_dot_scales.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/scale_paint_styles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/scale_paint_styles.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/swap_types_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/swap_types_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/customizable_dot_sizes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/customizable_dot_sizes.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/expanding_dots_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/expanding_dots_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/worm_types_high_offset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/worm_types_high_offset.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/customizable_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/customizable_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/jumping_dot_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/jumping_dot_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/color_transition_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/color_transition_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/customizable_border_radius.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/customizable_border_radius.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/customizable_dot_sizes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/customizable_dot_sizes.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/customizable_rotation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/customizable_rotation.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/expanding_dots_colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/expanding_dots_colors.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/expanding_dots_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/expanding_dots_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/worm_types_high_offset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/worm_types_high_offset.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/color_transition_dimensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/color_transition_dimensions.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/color_transition_dot_counts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/color_transition_dot_counts.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/customizable_vertical_offset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/customizable_vertical_offset.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/jumping_dot_vertical_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/jumping_dot_vertical_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/color_transition_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/color_transition_offsets.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/color_transition_custom_colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/color_transition_custom_colors.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/color_transition_stroke_style.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/color_transition_stroke_style.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/color_transition_dimensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/color_transition_dimensions.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/color_transition_dot_counts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/color_transition_dot_counts.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/customizable_border_radius.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/customizable_border_radius.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/customizable_vertical_offset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/customizable_vertical_offset.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/jumping_dot_vertical_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/jumping_dot_vertical_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/expanding_dots_expansion_factors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/expanding_dots_expansion_factors.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/color_transition_custom_colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/color_transition_custom_colors.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/color_transition_stroke_style.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/color_transition_stroke_style.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/scrolling_dots_fixed_center_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/scrolling_dots_fixed_center_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/scrolling_dots_fixed_center_scales.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/scrolling_dots_fixed_center_scales.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/expanding_dots_expansion_factors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/expanding_dots_expansion_factors.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/scrolling_dots_fixed_center_offsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/scrolling_dots_fixed_center_offsets.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/scrolling_dots_fixed_center_scales.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/scrolling_dots_fixed_center_scales.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /test/src/painters/goldens/ci/scrolling_dots_fixed_center_max_visible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/ci/scrolling_dots_fixed_center_max_visible.png -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /test/src/painters/goldens/macos/scrolling_dots_fixed_center_max_visible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/test/src/painters/goldens/macos/scrolling_dots_fixed_center_max_visible.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Milad-Akarie/smooth_page_indicator/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 68587a0916366e9512a78df22c44163d041dd5f3 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: a706cd211240f27be3b61f06d70f958c7a4156fe 8 | channel: dev 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - integration_test (0.0.1): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `Flutter`) 8 | - integration_test (from `.symlinks/plugins/integration_test/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: Flutter 13 | integration_test: 14 | :path: ".symlinks/plugins/integration_test/ios" 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 18 | integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 19 | 20 | PODFILE CHECKSUM: 0dbd5a87e0ace00c9610d2037ac22083a01f861d 21 | 22 | COCOAPODS: 1.16.2 23 | -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | # Test files (not needed by package users, available on GitHub) 2 | test/ 3 | 4 | # Demo GIFs (shown in README via GitHub links, keep only four_squares_demo.gif for pub.dev) 5 | demo/* 6 | !demo/four_squares_demo.gif 7 | 8 | # Example app build artifacts (keep example/lib for pub.dev Example tab) 9 | example/android/ 10 | example/ios/ 11 | example/web/ 12 | example/build/ 13 | example/pubspec.lock 14 | 15 | # Coverage reports 16 | coverage/ 17 | 18 | # Build artifacts 19 | build/ 20 | 21 | # Development files 22 | .dart_tool/ 23 | .idea/ 24 | *.iml 25 | .vscode/ 26 | 27 | # Scripts 28 | run_coverage.sh 29 | 30 | 31 | -------------------------------------------------------------------------------- /lib/smooth_page_indicator.dart: -------------------------------------------------------------------------------- 1 | export 'src/effects/color_transition_effect.dart'; 2 | export 'src/effects/expanding_dots_effect.dart'; 3 | export 'src/effects/indicator_effect.dart'; 4 | export 'src/effects/jumping_dot_effect.dart'; 5 | export 'src/effects/scale_effect.dart'; 6 | export 'src/effects/scrolling_dots_effect.dart'; 7 | export 'src/effects/slide_effect.dart'; 8 | export 'src/effects/swap_effect.dart'; 9 | export 'src/effects/worm_effect.dart'; 10 | export 'src/effects/customizable_effect.dart'; 11 | export 'src/painters/indicator_painter.dart'; 12 | export 'src/smooth_page_indicator.dart'; 13 | export 'src/theme_defaults.dart'; 14 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: smooth_page_indicator 2 | description: Customizable animated page indicator with a set of built-in effects. 3 | version: 2.0.1 4 | homepage: https://github.com/Milad-Akarie/smooth_page_indicator 5 | screenshots: 6 | - description: 'Example of smooth page indicator.' 7 | path: demo/four_squares_demo.gif 8 | topics: 9 | - page-indicator 10 | - dot-indicator 11 | - indicator 12 | - page-view 13 | 14 | environment: 15 | sdk: ">=3.0.0 <4.0.0" 16 | 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | 21 | 22 | dev_dependencies: 23 | flutter_test: 24 | sdk: flutter 25 | flutter_lints: ^6.0.0 26 | alchemist: ^0.13.0 27 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 13.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | coverage: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup Flutter 18 | uses: subosito/flutter-action@v2 19 | with: 20 | channel: 'stable' 21 | cache: true 22 | 23 | - name: Install dependencies 24 | run: flutter pub get 25 | 26 | - name: Run tests with coverage 27 | run: flutter test --coverage --exclude-tags=golden 28 | 29 | - name: Upload coverage to Codecov 30 | uses: codecov/codecov-action@v4 31 | with: 32 | token: ${{ secrets.CODECOV_TOKEN }} 33 | files: coverage/lcov.info 34 | fail_ci_if_error: true 35 | verbose: true 36 | exclude: | 37 | **/example/** 38 | 39 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Milad Akarie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test/src/effects/indicator_effect_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 3 | 4 | void main() { 5 | group('BasicIndicatorEffect (via WormEffect)', () { 6 | test('calculateSize is implemented correctly', () { 7 | const effect = WormEffect( 8 | dotWidth: 10, 9 | dotHeight: 10, 10 | spacing: 5, 11 | ); 12 | 13 | final size = effect.calculateSize(4); 14 | expect(size.width, 10 * 4 + 5 * 3); 15 | expect(size.height, 10); 16 | }); 17 | 18 | test('hitTestDots returns -1 for out of bounds', () { 19 | const effect = WormEffect( 20 | dotWidth: 16, 21 | spacing: 8, 22 | ); 23 | 24 | expect(effect.hitTestDots(1000, 3, 0), -1); 25 | }); 26 | 27 | test('hitTestDots returns correct index for valid positions', () { 28 | const effect = WormEffect( 29 | dotWidth: 16, 30 | spacing: 8, 31 | ); 32 | 33 | // First dot: 0 to 20 (16 + 8/2 from start, considering spacing/2 offset) 34 | expect(effect.hitTestDots(5, 5, 0), 0); 35 | // Second dot 36 | expect(effect.hitTestDots(25, 5, 0), 1); 37 | // Third dot 38 | expect(effect.hitTestDots(50, 5, 0), 2); 39 | }); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /example/ios/Flutter/ephemeral/flutter_lldb_helper.py: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | import lldb 6 | 7 | def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): 8 | """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" 9 | base = frame.register["x0"].GetValueAsAddress() 10 | page_len = frame.register["x1"].GetValueAsUnsigned() 11 | 12 | # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the 13 | # first page to see if handled it correctly. This makes diagnosing 14 | # misconfiguration (e.g. missing breakpoint) easier. 15 | data = bytearray(page_len) 16 | data[0:8] = b'IHELPED!' 17 | 18 | error = lldb.SBError() 19 | frame.GetThread().GetProcess().WriteMemory(base, data, error) 20 | if not error.Success(): 21 | print(f'Failed to write into {base}[+{page_len}]', error) 22 | return 23 | 24 | def __lldb_init_module(debugger: lldb.SBDebugger, _): 25 | target = debugger.GetDummyTarget() 26 | # Caveat: must use BreakpointCreateByRegEx here and not 27 | # BreakpointCreateByName. For some reasons callback function does not 28 | # get carried over from dummy target for the later. 29 | bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") 30 | bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) 31 | bp.SetAutoContinue(True) 32 | print("-- LLDB integration loaded --") 33 | -------------------------------------------------------------------------------- /test/src/effects/color_transition_effect_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 4 | 5 | void main() { 6 | group('ColorTransitionEffect', () { 7 | test('buildPainter returns IndicatorPainter', () { 8 | const effect = ColorTransitionEffect(); 9 | final painter = 10 | effect.buildPainter(5, 0, DefaultIndicatorColors.defaults); 11 | 12 | expect(painter, isA()); 13 | }); 14 | 15 | test('calculateSize returns correct size', () { 16 | const effect = ColorTransitionEffect( 17 | dotWidth: 16, 18 | dotHeight: 16, 19 | spacing: 8, 20 | ); 21 | 22 | final size = effect.calculateSize(5); 23 | expect(size.width, 16 * 5 + 8 * 4); 24 | expect(size.height, 16); 25 | }); 26 | 27 | testWidgets('paints correctly', (tester) async { 28 | const effect = ColorTransitionEffect(); 29 | 30 | await tester.pumpWidget( 31 | MaterialApp( 32 | home: Scaffold( 33 | body: CustomPaint( 34 | size: effect.calculateSize(5), 35 | painter: 36 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 37 | ), 38 | ), 39 | ), 40 | ); 41 | 42 | expect(find.byType(CustomPaint), findsWidgets); 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '13.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | example 30 | 31 | 32 | 33 | 36 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | build/ 31 | coverage/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Generated.xcconfig 62 | **/ios/Flutter/app.flx 63 | **/ios/Flutter/app.zip 64 | **/ios/Flutter/flutter_assets/ 65 | **/ios/Flutter/flutter_export_environment.sh 66 | **/ios/ServiceDefinitions.json 67 | **/ios/Runner/GeneratedPluginRegistrant.* 68 | 69 | # Exceptions to above rules. 70 | !**/ios/**/default.mode1v3 71 | !**/ios/**/default.mode2v3 72 | !**/ios/**/default.pbxuser 73 | !**/ios/**/default.perspectivev3 74 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 75 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test/src/effects/jumping_dot_effect_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 4 | 5 | void main() { 6 | group('JumpingDotEffect', () { 7 | test('calculateSize accounts for jump scale', () { 8 | const effect = JumpingDotEffect( 9 | dotWidth: 16, 10 | dotHeight: 16, 11 | spacing: 8, 12 | jumpScale: 1.5, 13 | verticalOffset: 0, 14 | ); 15 | 16 | final size = effect.calculateSize(5); 17 | expect(size.width, 16 * 5 + 8 * 4); 18 | expect(size.height, 16 * 1.5); // max(dotHeight, dotHeight * jumpScale) 19 | }); 20 | 21 | test('calculateSize accounts for vertical offset', () { 22 | const effect = JumpingDotEffect( 23 | dotWidth: 16, 24 | dotHeight: 16, 25 | jumpScale: 1.0, 26 | verticalOffset: 10.0, 27 | ); 28 | 29 | final size = effect.calculateSize(3); 30 | expect(size.height, 16 + 10); // dotHeight + verticalOffset.abs() 31 | }); 32 | 33 | test('buildPainter returns IndicatorPainter', () { 34 | const effect = JumpingDotEffect(); 35 | final painter = 36 | effect.buildPainter(5, 0, DefaultIndicatorColors.defaults); 37 | 38 | expect(painter, isA()); 39 | }); 40 | 41 | testWidgets('paints correctly', (tester) async { 42 | const effect = JumpingDotEffect(); 43 | 44 | await tester.pumpWidget( 45 | MaterialApp( 46 | home: Scaffold( 47 | body: CustomPaint( 48 | size: effect.calculateSize(5), 49 | painter: 50 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 51 | ), 52 | ), 53 | ), 54 | ); 55 | 56 | expect(find.byType(CustomPaint), findsWidgets); 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 30 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | defaultConfig { 36 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 37 | applicationId "com.example.example" 38 | minSdkVersion 16 39 | targetSdkVersion 30 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 59 | } 60 | -------------------------------------------------------------------------------- /test/src/effects/expanding_dots_effect_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 4 | 5 | void main() { 6 | group('ExpandingDotsEffect', () { 7 | test('calculateSize accounts for expansion', () { 8 | const effect = ExpandingDotsEffect( 9 | dotWidth: 16, 10 | dotHeight: 16, 11 | spacing: 8, 12 | expansionFactor: 3, 13 | ); 14 | 15 | final size = effect.calculateSize(5); 16 | // ((dotWidth + spacing) * (count - 1)) + (expansionFactor * dotWidth) 17 | expect(size.width, (16 + 8) * 4 + 3 * 16); 18 | expect(size.height, 16); 19 | }); 20 | 21 | test('hitTestDots accounts for expanded dot', () { 22 | const effect = ExpandingDotsEffect( 23 | dotWidth: 16, 24 | spacing: 8, 25 | expansionFactor: 3, 26 | ); 27 | 28 | // First dot is expanded (current = 0) 29 | expect(effect.hitTestDots(10, 5, 0), 0); 30 | expect(effect.hitTestDots(50, 5, 0), 0); // Still within expanded dot 31 | expect(effect.hitTestDots(70, 5, 0), 1); 32 | }); 33 | 34 | test('buildPainter returns IndicatorPainter', () { 35 | const effect = ExpandingDotsEffect(); 36 | final painter = 37 | effect.buildPainter(5, 0, DefaultIndicatorColors.defaults); 38 | 39 | expect(painter, isA()); 40 | }); 41 | 42 | testWidgets('paints correctly', (tester) async { 43 | const effect = ExpandingDotsEffect(); 44 | 45 | await tester.pumpWidget( 46 | MaterialApp( 47 | home: Scaffold( 48 | body: CustomPaint( 49 | size: effect.calculateSize(5), 50 | painter: 51 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 52 | ), 53 | ), 54 | ), 55 | ); 56 | 57 | expect(find.byType(CustomPaint), findsWidgets); 58 | }); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/effects/color_transition_effect.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../painters/color_transition_painter.dart'; 4 | import '../painters/indicator_painter.dart'; 5 | import '../theme_defaults.dart'; 6 | import 'indicator_effect.dart'; 7 | 8 | /// Holds painting configuration to be used by [TransitionPainter] 9 | class ColorTransitionEffect extends BasicIndicatorEffect { 10 | /// The active dot strokeWidth 11 | final double activeStrokeWidth; 12 | 13 | /// Default constructor 14 | const ColorTransitionEffect({ 15 | this.activeStrokeWidth = 1.5, 16 | double offset = 16.0, 17 | super.dotWidth = 16.0, 18 | super.dotHeight = 16.0, 19 | super.spacing = 8.0, 20 | super.radius = 16, 21 | super.dotColor, 22 | super.activeDotColor, 23 | super.strokeWidth = 1.0, 24 | super.paintStyle = PaintingStyle.fill, 25 | }); 26 | 27 | @override 28 | IndicatorPainter buildPainter( 29 | int count, double offset, DefaultIndicatorColors indicatorColors) { 30 | return TransitionPainter( 31 | count: count, 32 | offset: offset, 33 | effect: this, 34 | indicatorColors: indicatorColors, 35 | ); 36 | } 37 | 38 | @override 39 | ColorTransitionEffect lerp(covariant ColorTransitionEffect? other, double t) { 40 | if (other == null) return this; 41 | return ColorTransitionEffect( 42 | activeStrokeWidth: BasicIndicatorEffect.lerpDouble( 43 | activeStrokeWidth, other.activeStrokeWidth, t)!, 44 | dotWidth: BasicIndicatorEffect.lerpDouble(dotWidth, other.dotWidth, t)!, 45 | dotHeight: 46 | BasicIndicatorEffect.lerpDouble(dotHeight, other.dotHeight, t)!, 47 | spacing: BasicIndicatorEffect.lerpDouble(spacing, other.spacing, t)!, 48 | radius: BasicIndicatorEffect.lerpDouble(radius, other.radius, t)!, 49 | dotColor: Color.lerp(dotColor, other.dotColor, t), 50 | activeDotColor: Color.lerp(activeDotColor, other.activeDotColor, t), 51 | strokeWidth: 52 | BasicIndicatorEffect.lerpDouble(strokeWidth, other.strokeWidth, t)!, 53 | paintStyle: t < 0.5 ? paintStyle : other.paintStyle, 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/src/effects/slide_effect_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 4 | 5 | void main() { 6 | group('SlideEffect', () { 7 | test('SlideType.slideUnder works correctly', () { 8 | const effect = SlideEffect(type: SlideType.slideUnder); 9 | expect(effect.type, SlideType.slideUnder); 10 | }); 11 | 12 | test('buildPainter returns IndicatorPainter', () { 13 | const effect = SlideEffect(); 14 | final painter = 15 | effect.buildPainter(5, 0, DefaultIndicatorColors.defaults); 16 | 17 | expect(painter, isA()); 18 | }); 19 | 20 | test('calculateSize returns correct size', () { 21 | const effect = SlideEffect( 22 | dotWidth: 16, 23 | dotHeight: 16, 24 | spacing: 8, 25 | ); 26 | 27 | final size = effect.calculateSize(5); 28 | expect(size.width, 16 * 5 + 8 * 4); 29 | expect(size.height, 16); 30 | }); 31 | 32 | testWidgets('paints correctly', (tester) async { 33 | const effect = SlideEffect(); 34 | 35 | await tester.pumpWidget( 36 | MaterialApp( 37 | home: Scaffold( 38 | body: CustomPaint( 39 | size: effect.calculateSize(5), 40 | painter: 41 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 42 | ), 43 | ), 44 | ), 45 | ); 46 | 47 | expect(find.byType(CustomPaint), findsWidgets); 48 | }); 49 | 50 | testWidgets('slideUnder type paints correctly', (tester) async { 51 | const effect = SlideEffect(type: SlideType.slideUnder); 52 | 53 | await tester.pumpWidget( 54 | MaterialApp( 55 | home: Scaffold( 56 | body: CustomPaint( 57 | size: effect.calculateSize(5), 58 | painter: 59 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 60 | ), 61 | ), 62 | ), 63 | ); 64 | 65 | expect(find.byType(CustomPaint), findsWidgets); 66 | }); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /test/src/effects/scale_effect_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 4 | 5 | void main() { 6 | group('ScaleEffect', () { 7 | test('calculateSize accounts for scale', () { 8 | const effect = ScaleEffect( 9 | dotWidth: 16, 10 | dotHeight: 16, 11 | spacing: 10, 12 | scale: 1.5, 13 | ); 14 | 15 | final size = effect.calculateSize(5); 16 | // (dotWidth * nonActiveCount) + (spacing * nonActiveCount) + activeDotWidth 17 | final activeDotWidth = 16 * 1.5; 18 | expect(size.width, (16 * 4) + (10 * 4) + activeDotWidth); 19 | expect(size.height, activeDotWidth); 20 | }); 21 | 22 | test('buildPainter returns IndicatorPainter', () { 23 | const effect = ScaleEffect(); 24 | final painter = 25 | effect.buildPainter(5, 0, DefaultIndicatorColors.defaults); 26 | 27 | expect(painter, isA()); 28 | }); 29 | 30 | testWidgets('paints correctly', (tester) async { 31 | const effect = ScaleEffect(); 32 | 33 | await tester.pumpWidget( 34 | MaterialApp( 35 | home: Scaffold( 36 | body: CustomPaint( 37 | size: effect.calculateSize(5), 38 | painter: 39 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 40 | ), 41 | ), 42 | ), 43 | ); 44 | 45 | expect(find.byType(CustomPaint), findsWidgets); 46 | }); 47 | 48 | testWidgets('with stroke active paint style', (tester) async { 49 | await tester.pumpWidget( 50 | const MaterialApp( 51 | home: Scaffold( 52 | body: SmoothIndicator( 53 | offset: 0, 54 | count: 5, 55 | size: Size(150, 30), 56 | effect: ScaleEffect( 57 | activePaintStyle: PaintingStyle.stroke, 58 | activeStrokeWidth: 2.0, 59 | ), 60 | ), 61 | ), 62 | ), 63 | ); 64 | 65 | expect(find.byType(SmoothIndicator), findsOneWidget); 66 | }); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/painters/slide_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/src/effects/slide_effect.dart'; 3 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 4 | 5 | import 'indicator_painter.dart'; 6 | 7 | /// Paints a sliding transition effect between active 8 | /// and non-active dots 9 | /// 10 | /// Live demo at 11 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/slide.gif 12 | class SlidePainter extends BasicIndicatorPainter { 13 | /// The painting configuration 14 | final SlideEffect effect; 15 | 16 | /// Default constructor 17 | SlidePainter({ 18 | required this.effect, 19 | required int count, 20 | required double offset, 21 | required DefaultIndicatorColors indicatorColors, 22 | }) : super(offset, count, effect, indicatorColors); 23 | 24 | @override 25 | void paint(Canvas canvas, Size size) { 26 | // paint still dots 27 | 28 | paintStillDots(canvas, size); 29 | 30 | final activeDotPainter = Paint()..color = effectiveActiveColor; 31 | final dotOffset = offset - offset.toInt(); 32 | // handle dot travel from end to start (for infinite pager support) 33 | if (offset > count - 1) { 34 | final startDot = calcPortalTravel(size, effect.dotWidth / 2, dotOffset); 35 | canvas.drawRRect(startDot, activeDotPainter); 36 | 37 | final endDot = calcPortalTravel( 38 | size, 39 | ((count - 1) * distance) + (effect.dotWidth / 2), 40 | 1 - dotOffset, 41 | ); 42 | canvas.drawRRect(endDot, activeDotPainter); 43 | return; 44 | } 45 | 46 | final xPos = offset * distance; 47 | final yPos = size.height / 2; 48 | final rRect = RRect.fromLTRBR( 49 | xPos, 50 | yPos - effect.dotHeight / 2, 51 | xPos + effect.dotWidth, 52 | yPos + effect.dotHeight / 2, 53 | dotRadius, 54 | ); 55 | 56 | if (effect.type == SlideType.slideUnder) { 57 | canvas.saveLayer(Rect.largest, Paint()); 58 | canvas.drawRRect(rRect, activeDotPainter); 59 | maskStillDots(size, canvas); 60 | canvas.restore(); 61 | } else { 62 | canvas.drawRRect(rRect, activeDotPainter); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 13 | 17 | 21 | 26 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /lib/src/effects/slide_effect.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/src/painters/indicator_painter.dart'; 3 | import 'package:smooth_page_indicator/src/painters/slide_painter.dart'; 4 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 5 | 6 | import 'indicator_effect.dart'; 7 | 8 | /// Holds painting configuration to be used by [SlidePainter] 9 | class SlideEffect extends BasicIndicatorEffect { 10 | /// The effect variant 11 | /// 12 | /// defaults to [SlideType.normal] 13 | final SlideType type; 14 | 15 | /// Default constructor 16 | const SlideEffect({ 17 | super.activeDotColor, 18 | double offset = 16.0, 19 | super.dotWidth = 16.0, 20 | super.dotHeight = 16.0, 21 | super.spacing = 8.0, 22 | super.radius = 16, 23 | super.dotColor, 24 | super.strokeWidth = 1.0, 25 | super.paintStyle = PaintingStyle.fill, 26 | this.type = SlideType.normal, 27 | }); 28 | 29 | @override 30 | IndicatorPainter buildPainter( 31 | int count, double offset, DefaultIndicatorColors indicatorColors) { 32 | return SlidePainter( 33 | count: count, 34 | offset: offset, 35 | effect: this, 36 | indicatorColors: indicatorColors); 37 | } 38 | 39 | @override 40 | SlideEffect lerp(covariant SlideEffect? other, double t) { 41 | if (other == null) return this; 42 | return SlideEffect( 43 | type: t < 0.5 ? type : other.type, 44 | dotWidth: BasicIndicatorEffect.lerpDouble(dotWidth, other.dotWidth, t)!, 45 | dotHeight: 46 | BasicIndicatorEffect.lerpDouble(dotHeight, other.dotHeight, t)!, 47 | spacing: BasicIndicatorEffect.lerpDouble(spacing, other.spacing, t)!, 48 | radius: BasicIndicatorEffect.lerpDouble(radius, other.radius, t)!, 49 | dotColor: Color.lerp(dotColor, other.dotColor, t), 50 | activeDotColor: Color.lerp(activeDotColor, other.activeDotColor, t), 51 | strokeWidth: 52 | BasicIndicatorEffect.lerpDouble(strokeWidth, other.strokeWidth, t)!, 53 | paintStyle: t < 0.5 ? paintStyle : other.paintStyle, 54 | ); 55 | } 56 | } 57 | 58 | /// The Slide effect variants 59 | enum SlideType { 60 | /// Paints regular dot sliding animation 61 | normal, 62 | 63 | /// Paints masked (under-layered) dot sliding animation 64 | slideUnder 65 | } 66 | -------------------------------------------------------------------------------- /run_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Test Coverage Script for smooth_page_indicator 4 | # This script runs Flutter tests with coverage and generates a report 5 | 6 | set -e 7 | 8 | echo "🧪 Running Flutter tests with coverage..." 9 | echo "============================================" 10 | 11 | # Clean previous coverage data 12 | rm -rf coverage 13 | 14 | # Run tests with coverage 15 | flutter test --coverage 16 | 17 | echo "" 18 | echo "📊 Coverage data generated!" 19 | echo "============================================" 20 | 21 | # Check if lcov is installed 22 | if command -v lcov &> /dev/null; then 23 | echo "📈 Generating HTML coverage report..." 24 | 25 | # Generate HTML report 26 | genhtml coverage/lcov.info -o coverage/html --quiet 27 | 28 | echo "" 29 | echo "✅ HTML coverage report generated at: coverage/html/index.html" 30 | echo "" 31 | 32 | # Open the report in the default browser (macOS) 33 | if [[ "$OSTYPE" == "darwin"* ]]; then 34 | echo "🌐 Opening coverage report in browser..." 35 | open coverage/html/index.html 36 | elif [[ "$OSTYPE" == "linux-gnu"* ]]; then 37 | echo "🌐 Opening coverage report in browser..." 38 | xdg-open coverage/html/index.html 39 | fi 40 | else 41 | echo "" 42 | echo "⚠️ lcov is not installed. To generate HTML reports:" 43 | echo " macOS: brew install lcov" 44 | echo " Linux: sudo apt-get install lcov" 45 | echo "" 46 | echo "📄 Raw coverage data is available at: coverage/lcov.info" 47 | fi 48 | 49 | echo "" 50 | echo "📋 Coverage Summary:" 51 | echo "============================================" 52 | 53 | # Show coverage summary if lcov is available 54 | if command -v lcov &> /dev/null; then 55 | lcov --summary coverage/lcov.info 2>/dev/null || true 56 | else 57 | # Alternative: show line count from lcov.info 58 | if [ -f coverage/lcov.info ]; then 59 | total_lines=$(grep -c "^DA:" coverage/lcov.info 2>/dev/null || echo "0") 60 | covered_lines=$(grep "^DA:" coverage/lcov.info 2>/dev/null | grep -v ",0$" | wc -l || echo "0") 61 | if [ "$total_lines" -gt 0 ]; then 62 | percentage=$((covered_lines * 100 / total_lines)) 63 | echo "Lines: $covered_lines / $total_lines ($percentage%)" 64 | fi 65 | fi 66 | fi 67 | 68 | echo "" 69 | echo "🎉 Done!" 70 | 71 | -------------------------------------------------------------------------------- /lib/src/painters/color_transition_painter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 5 | 6 | /// Paints a color change transition effect between active 7 | /// and non-active dots 8 | /// 9 | /// Live demo at 10 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/color-transition.gif 11 | class TransitionPainter extends BasicIndicatorPainter { 12 | /// The painting configuration 13 | final ColorTransitionEffect effect; 14 | 15 | /// Default constructor 16 | TransitionPainter({ 17 | required this.effect, 18 | required int count, 19 | required double offset, 20 | required DefaultIndicatorColors indicatorColors, 21 | }) : super(offset, count, effect, indicatorColors); 22 | 23 | @override 24 | void paint(Canvas canvas, Size size) { 25 | final current = offset.floor(); 26 | final dotPaint = Paint() 27 | ..strokeWidth = effect.strokeWidth 28 | ..style = effect.paintStyle; 29 | 30 | final dotOffset = offset - current; 31 | for (var i = 0; i < count; i++) { 32 | var color = effectiveInactiveColor; 33 | if (i == current) { 34 | // ! Both a and b are non nullable 35 | color = Color.lerp( 36 | effectiveActiveColor, effectiveInactiveColor, dotOffset)!; 37 | dotPaint.strokeWidth = 38 | max(effect.activeStrokeWidth * (1 - dotOffset), effect.strokeWidth); 39 | } else if (i - 1 == current || (i == 0 && offset > count - 1)) { 40 | // ! Both a and b are non nullable 41 | dotPaint.strokeWidth = 42 | max(effect.activeStrokeWidth * dotOffset, effect.strokeWidth); 43 | color = Color.lerp( 44 | effectiveActiveColor, effectiveInactiveColor, 1.0 - dotOffset)!; 45 | } else { 46 | dotPaint.strokeWidth = effect.strokeWidth; 47 | color = effectiveInactiveColor; 48 | } 49 | 50 | final xPos = (i * distance); 51 | final yPos = size.height / 2; 52 | final rRect = RRect.fromLTRBR( 53 | xPos, 54 | yPos - effect.dotHeight / 2, 55 | xPos + effect.dotWidth, 56 | yPos + effect.dotHeight / 2, 57 | dotRadius, 58 | ); 59 | canvas.drawRRect(rRect, dotPaint..color = color); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/painters/expanding_dots_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/src/effects/expanding_dots_effect.dart'; 3 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 4 | 5 | import 'indicator_painter.dart'; 6 | 7 | /// Paints an expanding dot transition effect between active 8 | /// and non-active dot 9 | /// 10 | /// Live demo at 11 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/expanding-dot.gif 12 | class ExpandingDotsPainter extends BasicIndicatorPainter { 13 | /// The painting configuration 14 | final ExpandingDotsEffect effect; 15 | 16 | /// Default constructor 17 | ExpandingDotsPainter({ 18 | required double offset, 19 | required this.effect, 20 | required int count, 21 | required DefaultIndicatorColors indicatorColors, 22 | }) : super(offset, count, effect, indicatorColors); 23 | 24 | @override 25 | void paint(Canvas canvas, Size size) { 26 | final current = offset.floor(); 27 | var drawingOffset = -effect.spacing; 28 | final dotOffset = offset - current; 29 | 30 | for (var i = 0; i < count; i++) { 31 | var color = effectiveInactiveColor; 32 | final activeDotWidth = effect.dotWidth * effect.expansionFactor; 33 | final expansion = 34 | (dotOffset / 2 * ((activeDotWidth - effect.dotWidth) / .5)); 35 | final xPos = drawingOffset + effect.spacing; 36 | var width = effect.dotWidth; 37 | if (i == current) { 38 | // ! Both a and b are non nullable 39 | color = Color.lerp( 40 | effectiveActiveColor, effectiveInactiveColor, dotOffset)!; 41 | width = activeDotWidth - expansion; 42 | } else if (i - 1 == current || (i == 0 && offset > count - 1)) { 43 | width = effect.dotWidth + expansion; 44 | // ! Both a and b are non nullable 45 | color = Color.lerp( 46 | effectiveActiveColor, effectiveInactiveColor, 1.0 - dotOffset)!; 47 | } 48 | final yPos = size.height / 2; 49 | final rRect = RRect.fromLTRBR( 50 | xPos, 51 | yPos - effect.dotHeight / 2, 52 | xPos + width, 53 | yPos + effect.dotHeight / 2, 54 | dotRadius, 55 | ); 56 | drawingOffset = rRect.right; 57 | canvas.drawRRect(rRect, dotPaint..color = color); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/effects/worm_effect.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/src/painters/indicator_painter.dart'; 3 | import 'package:smooth_page_indicator/src/painters/worm_painter.dart'; 4 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 5 | 6 | import 'indicator_effect.dart'; 7 | 8 | /// Holds painting configuration to be used by [WormPainter] 9 | class WormEffect extends BasicIndicatorEffect { 10 | /// The effect variant 11 | /// 12 | /// defaults to [WormType.normal] 13 | final WormType type; 14 | 15 | /// Default constructor 16 | const WormEffect({ 17 | double offset = 16.0, 18 | super.dotWidth = 16.0, 19 | super.dotHeight = 16.0, 20 | super.spacing = 8.0, 21 | super.radius = 16, 22 | super.dotColor, 23 | super.activeDotColor, 24 | super.strokeWidth = 1.0, 25 | super.paintStyle = PaintingStyle.fill, 26 | this.type = WormType.normal, 27 | }); 28 | 29 | @override 30 | IndicatorPainter buildPainter( 31 | int count, double offset, DefaultIndicatorColors indicatorColors) { 32 | return WormPainter( 33 | count: count, 34 | offset: offset, 35 | effect: this, 36 | indicatorColors: indicatorColors); 37 | } 38 | 39 | @override 40 | WormEffect lerp(covariant WormEffect? other, double t) { 41 | if (other == null) return this; 42 | return WormEffect( 43 | type: t < 0.5 ? type : other.type, 44 | dotWidth: BasicIndicatorEffect.lerpDouble(dotWidth, other.dotWidth, t)!, 45 | dotHeight: 46 | BasicIndicatorEffect.lerpDouble(dotHeight, other.dotHeight, t)!, 47 | spacing: BasicIndicatorEffect.lerpDouble(spacing, other.spacing, t)!, 48 | radius: BasicIndicatorEffect.lerpDouble(radius, other.radius, t)!, 49 | dotColor: Color.lerp(dotColor, other.dotColor, t), 50 | activeDotColor: Color.lerp(activeDotColor, other.activeDotColor, t), 51 | strokeWidth: 52 | BasicIndicatorEffect.lerpDouble(strokeWidth, other.strokeWidth, t)!, 53 | paintStyle: t < 0.5 ? paintStyle : other.paintStyle, 54 | ); 55 | } 56 | } 57 | 58 | /// The Worm effect variants 59 | enum WormType { 60 | /// Draws normal worm animation 61 | normal, 62 | 63 | /// Draws a thin worm animation 64 | thin, 65 | 66 | /// Draws normal worm animation that looks like 67 | /// it's under the background 68 | underground, 69 | 70 | /// Draws a thing worm animation that looks like 71 | /// it's under the background 72 | thinUnderground, 73 | } 74 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/src/effects/jumping_dot_effect.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:smooth_page_indicator/src/painters/indicator_painter.dart'; 5 | import 'package:smooth_page_indicator/src/painters/jumping_dot_painter.dart'; 6 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 7 | 8 | import 'indicator_effect.dart'; 9 | 10 | /// Holds painting configuration to be used by [JumpingDotPainter] 11 | class JumpingDotEffect extends BasicIndicatorEffect { 12 | /// The maximum scale the dot will hit while jumping 13 | final double jumpScale; 14 | 15 | /// The vertical offset of the jumping dot 16 | final double verticalOffset; 17 | 18 | /// Default constructor 19 | const JumpingDotEffect({ 20 | super.activeDotColor, 21 | this.jumpScale = 1.4, 22 | this.verticalOffset = 0.0, 23 | double offset = 16.0, 24 | super.dotWidth = 16.0, 25 | super.dotHeight = 16.0, 26 | super.spacing = 8.0, 27 | super.radius = 16, 28 | super.dotColor, 29 | super.strokeWidth = 1.0, 30 | super.paintStyle = PaintingStyle.fill, 31 | }); 32 | 33 | @override 34 | Size calculateSize(int count) { 35 | return Size( 36 | dotWidth * count + (spacing * (count - 1)), 37 | max(dotHeight, dotHeight * jumpScale) + verticalOffset.abs(), 38 | ); 39 | } 40 | 41 | @override 42 | IndicatorPainter buildPainter( 43 | int count, double offset, DefaultIndicatorColors indicatorColors) { 44 | return JumpingDotPainter( 45 | count: count, 46 | offset: offset, 47 | effect: this, 48 | indicatorColors: indicatorColors); 49 | } 50 | 51 | @override 52 | JumpingDotEffect lerp(covariant JumpingDotEffect? other, double t) { 53 | if (other == null) return this; 54 | return JumpingDotEffect( 55 | jumpScale: 56 | BasicIndicatorEffect.lerpDouble(jumpScale, other.jumpScale, t)!, 57 | verticalOffset: BasicIndicatorEffect.lerpDouble( 58 | verticalOffset, other.verticalOffset, t)!, 59 | dotWidth: BasicIndicatorEffect.lerpDouble(dotWidth, other.dotWidth, t)!, 60 | dotHeight: 61 | BasicIndicatorEffect.lerpDouble(dotHeight, other.dotHeight, t)!, 62 | spacing: BasicIndicatorEffect.lerpDouble(spacing, other.spacing, t)!, 63 | radius: BasicIndicatorEffect.lerpDouble(radius, other.radius, t)!, 64 | dotColor: Color.lerp(dotColor, other.dotColor, t), 65 | activeDotColor: Color.lerp(activeDotColor, other.activeDotColor, t), 66 | strokeWidth: 67 | BasicIndicatorEffect.lerpDouble(strokeWidth, other.strokeWidth, t)!, 68 | paintStyle: t < 0.5 ? paintStyle : other.paintStyle, 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/src/effects/swap_effect.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/src/painters/indicator_painter.dart'; 3 | import 'package:smooth_page_indicator/src/painters/swap_painter.dart'; 4 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 5 | 6 | import 'indicator_effect.dart'; 7 | 8 | /// Holds painting configuration to be used by [SwapPainter] 9 | class SwapEffect extends BasicIndicatorEffect { 10 | /// The effect variant 11 | /// 12 | /// defaults to [SwapType.normal] 13 | final SwapType type; 14 | 15 | /// Default constructor 16 | const SwapEffect({ 17 | super.activeDotColor, 18 | double offset = 16.0, 19 | super.dotWidth = 16.0, 20 | super.dotHeight = 16.0, 21 | super.spacing = 8.0, 22 | super.radius = 16, 23 | super.dotColor, 24 | super.strokeWidth = 1.0, 25 | this.type = SwapType.normal, 26 | super.paintStyle = PaintingStyle.fill, 27 | }); 28 | 29 | @override 30 | Size calculateSize(int count) { 31 | var height = dotHeight; 32 | if (type == SwapType.zRotation) { 33 | height += height * .2; 34 | } else if (type == SwapType.yRotation) { 35 | height += dotWidth + spacing; 36 | } 37 | return Size(dotWidth * count + (spacing * count), height); 38 | } 39 | 40 | @override 41 | IndicatorPainter buildPainter( 42 | int count, double offset, DefaultIndicatorColors indicatorColors) { 43 | return SwapPainter( 44 | count: count, 45 | offset: offset, 46 | effect: this, 47 | indicatorColors: indicatorColors); 48 | } 49 | 50 | @override 51 | SwapEffect lerp(covariant SwapEffect? other, double t) { 52 | if (other == null) return this; 53 | return SwapEffect( 54 | type: t < 0.5 ? type : other.type, 55 | dotWidth: BasicIndicatorEffect.lerpDouble(dotWidth, other.dotWidth, t)!, 56 | dotHeight: 57 | BasicIndicatorEffect.lerpDouble(dotHeight, other.dotHeight, t)!, 58 | spacing: BasicIndicatorEffect.lerpDouble(spacing, other.spacing, t)!, 59 | radius: BasicIndicatorEffect.lerpDouble(radius, other.radius, t)!, 60 | dotColor: Color.lerp(dotColor, other.dotColor, t), 61 | activeDotColor: Color.lerp(activeDotColor, other.activeDotColor, t), 62 | strokeWidth: 63 | BasicIndicatorEffect.lerpDouble(strokeWidth, other.strokeWidth, t)!, 64 | paintStyle: t < 0.5 ? paintStyle : other.paintStyle, 65 | ); 66 | } 67 | } 68 | 69 | /// The swap effect variants 70 | enum SwapType { 71 | /// Swaps dots in the x axi (flat) 72 | normal, 73 | 74 | /// Swaps dots in the y axi with a rotation effect 75 | yRotation, 76 | 77 | /// Swaps dots in the x axi and scales active-dot (3d-ish) 78 | zRotation 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/painters/scale_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/src/effects/scale_effect.dart'; 3 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 4 | 5 | import 'indicator_painter.dart'; 6 | 7 | /// Paints a scale-dot transition effect between active 8 | /// and non-active dots 9 | /// 10 | /// Live demo at 11 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/scale.gif 12 | class ScalePainter extends BasicIndicatorPainter { 13 | /// The painting configuration 14 | final ScaleEffect effect; 15 | 16 | /// Default constructor 17 | ScalePainter({ 18 | required double offset, 19 | required this.effect, 20 | required int count, 21 | required DefaultIndicatorColors indicatorColors, 22 | }) : super(offset, count, effect, indicatorColors); 23 | 24 | @override 25 | void paint(Canvas canvas, Size size) { 26 | var current = offset.floor(); 27 | var activePaint = Paint() 28 | ..color = effectiveInactiveColor 29 | ..style = effect.activePaintStyle 30 | ..strokeWidth = effect.activeStrokeWidth; 31 | 32 | var dotOffset = offset - current; 33 | var activeScale = effect.scale - 1.0; 34 | for (var index = 0; index < count; index++) { 35 | var dot = _calcBounds(size.height, index); 36 | canvas.drawRRect(dot.inflate(effect.activeStrokeWidth / 2), dotPaint); 37 | var color = effectiveInactiveColor; 38 | var scale = 0.0; 39 | if (index == current) { 40 | scale = (effect.scale) - (activeScale * dotOffset); 41 | // ! Both a and b are non nullable 42 | color = Color.lerp( 43 | effectiveActiveColor, effectiveInactiveColor, dotOffset)!; 44 | } else if (index - 1 == current || (index == 0 && offset > count - 1)) { 45 | scale = 1.0 + (activeScale * dotOffset); 46 | // ! Both a and b are non nullable 47 | color = Color.lerp( 48 | effectiveInactiveColor, effectiveActiveColor, dotOffset)!; 49 | } 50 | canvas.drawRRect( 51 | _calcBounds(size.height, index, scale), activePaint..color = color); 52 | } 53 | } 54 | 55 | RRect _calcBounds(double canvasHeight, num offset, [double scale = 1.0]) { 56 | final startingPoint = effect.dotWidth * effect.scale / 2; 57 | final width = effect.dotWidth * scale; 58 | final height = effect.dotHeight * scale; 59 | final xPos = startingPoint - 60 | width / 2 + 61 | (offset * (effect.dotWidth + effect.spacing)); 62 | final yPos = canvasHeight / 2; 63 | 64 | return RRect.fromLTRBR( 65 | xPos, 66 | yPos - height / 2, 67 | xPos + width, 68 | yPos + height / 2, 69 | dotRadius * scale, 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/painters/jumping_dot_painter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:smooth_page_indicator/src/effects/jumping_dot_effect.dart'; 5 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 6 | 7 | import 'indicator_painter.dart'; 8 | 9 | /// Paints a jumping dot transition effect between active 10 | /// and non-active dots 11 | /// 12 | /// Live demo at 13 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/jumping-dot.gif 14 | class JumpingDotPainter extends BasicIndicatorPainter { 15 | /// The painting configuration 16 | final JumpingDotEffect effect; 17 | 18 | /// Default constructor 19 | JumpingDotPainter({ 20 | required this.effect, 21 | required int count, 22 | required double offset, 23 | required DefaultIndicatorColors indicatorColors, 24 | }) : super(offset, count, effect, indicatorColors); 25 | 26 | @override 27 | void paint(Canvas canvas, Size size) { 28 | // paint still dots 29 | if (effect.verticalOffset != 0) { 30 | canvas.translate(0, effect.verticalOffset / 2); 31 | } 32 | paintStillDots(canvas, size); 33 | final activeDotPainter = Paint()..color = effectiveActiveColor; 34 | final dotOffset = offset - offset.toInt(); 35 | 36 | // handle dot travel from end to start (for infinite pager support) 37 | if (offset > count - 1) { 38 | final startDot = calcPortalTravel(size, effect.dotWidth / 2, dotOffset); 39 | canvas.drawRRect(startDot, activeDotPainter); 40 | 41 | final endDot = calcPortalTravel( 42 | size, 43 | ((count - 1) * distance) + (effect.dotWidth / 2), 44 | 1 - dotOffset, 45 | ); 46 | canvas.drawRRect(endDot, activeDotPainter); 47 | return; 48 | } 49 | 50 | var scale = 1.0; 51 | var targetScale = effect.jumpScale - 1.0; 52 | 53 | if (dotOffset < .5) { 54 | scale = 1.0 + (dotOffset * 2) * targetScale; 55 | } else { 56 | scale = effect.jumpScale + ((1 - (dotOffset * 2)) * targetScale); 57 | } 58 | final piFactor = (dotOffset * math.pi); 59 | var yPos = size.height / 2; 60 | var xPos = offset.floor() * distance; 61 | var x = (1 - ((math.cos(piFactor) + 1) / 2)) * distance; 62 | var y = -math.sin(piFactor) * effect.verticalOffset; 63 | xPos += x; 64 | yPos += y; 65 | 66 | final height = effect.dotHeight * scale; 67 | final width = effect.dotWidth * scale; 68 | final scaleRatio = width / effect.dotWidth; 69 | final rRect = RRect.fromLTRBR( 70 | xPos, 71 | yPos - height / 2, 72 | xPos + width, 73 | yPos + height / 2, 74 | dotRadius * scaleRatio, 75 | ); 76 | 77 | canvas.drawRRect(rRect, activeDotPainter); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/painters/worm_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/src/effects/worm_effect.dart'; 3 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 4 | 5 | import 'indicator_painter.dart'; 6 | 7 | /// Paints a warm-like transition effect between active 8 | /// and non-active dots 9 | /// 10 | /// Live demo at 11 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/worm.gif 12 | class WormPainter extends BasicIndicatorPainter { 13 | /// The painting configuration 14 | final WormEffect effect; 15 | 16 | /// Default constructor 17 | WormPainter({ 18 | required this.effect, 19 | required int count, 20 | required double offset, 21 | required DefaultIndicatorColors indicatorColors, 22 | }) : super(offset, count, effect, indicatorColors); 23 | 24 | @override 25 | void paint(Canvas canvas, Size size) { 26 | // paint still dots 27 | paintStillDots(canvas, size); 28 | 29 | final activeDotPaint = Paint()..color = effectiveActiveColor; 30 | final dotOffset = (offset - offset.toInt()); 31 | 32 | // handle dot travel from end to start (for infinite pager support) 33 | if (offset > count - 1) { 34 | final startDot = calcPortalTravel(size, effect.dotWidth / 2, dotOffset); 35 | canvas.drawRRect(startDot, activeDotPaint); 36 | 37 | final endDot = calcPortalTravel( 38 | size, 39 | ((count - 1) * distance) + (effect.dotWidth / 2), 40 | 1 - dotOffset, 41 | ); 42 | canvas.drawRRect(endDot, activeDotPaint); 43 | return; 44 | } 45 | 46 | final wormOffset = dotOffset * 2; 47 | final xPos = (offset.floor() * distance); 48 | final yPos = size.height / 2; 49 | var head = xPos; 50 | var tail = xPos + effect.dotWidth + (wormOffset * distance); 51 | var halfHeight = effect.dotHeight / 2; 52 | final thinWorm = 53 | effect.type == WormType.thin || effect.type == WormType.thinUnderground; 54 | var dotHeight = thinWorm 55 | ? halfHeight + (halfHeight * (1 - wormOffset)) 56 | : effect.dotHeight; 57 | 58 | if (wormOffset > 1) { 59 | tail = xPos + effect.dotWidth + (1 * distance); 60 | head = xPos + distance * (wormOffset - 1); 61 | if (thinWorm) { 62 | dotHeight = halfHeight + (halfHeight * (wormOffset - 1)); 63 | } 64 | } 65 | final worm = RRect.fromLTRBR( 66 | head, 67 | yPos - dotHeight / 2, 68 | tail, 69 | yPos + dotHeight / 2, 70 | dotRadius, 71 | ); 72 | if (effect.type == WormType.underground || 73 | effect.type == WormType.thinUnderground) { 74 | canvas.saveLayer(Rect.largest, Paint()); 75 | canvas.drawRRect(worm, activeDotPaint); 76 | maskStillDots(size, canvas); 77 | canvas.restore(); 78 | } else { 79 | canvas.drawRRect(worm, activeDotPaint); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/src/effects/expanding_dots_effect.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/src/painters/expanding_dots_painter.dart'; 3 | import 'package:smooth_page_indicator/src/painters/indicator_painter.dart'; 4 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 5 | 6 | import 'indicator_effect.dart'; 7 | 8 | /// Holds painting configuration to be used by [ExpandingDotsPainter] 9 | class ExpandingDotsEffect extends BasicIndicatorEffect { 10 | /// This is multiplied by [dotWidth] to calculate 11 | /// the width of the expanded dot. 12 | final double expansionFactor; 13 | 14 | /// Default constructor 15 | const ExpandingDotsEffect({ 16 | this.expansionFactor = 3, 17 | double offset = 16.0, 18 | super.dotWidth = 16.0, 19 | super.dotHeight = 16.0, 20 | super.spacing = 8.0, 21 | super.radius = 16.0, 22 | super.activeDotColor, 23 | super.dotColor, 24 | super.strokeWidth = 1.0, 25 | super.paintStyle = PaintingStyle.fill, 26 | }) : assert(expansionFactor > 1); 27 | 28 | @override 29 | Size calculateSize(int count) { 30 | /// Add the expanded dot width to our size calculation 31 | return Size( 32 | ((dotWidth + spacing) * (count - 1)) + (expansionFactor * dotWidth), 33 | dotHeight); 34 | } 35 | 36 | @override 37 | IndicatorPainter buildPainter( 38 | int count, double offset, DefaultIndicatorColors indicatorColors) { 39 | return ExpandingDotsPainter( 40 | count: count, 41 | offset: offset, 42 | effect: this, 43 | indicatorColors: indicatorColors); 44 | } 45 | 46 | @override 47 | int hitTestDots(double dx, int count, double current) { 48 | var anchor = -spacing / 2; 49 | for (var index = 0; index < count; index++) { 50 | var widthBound = 51 | (index == current ? (dotWidth * expansionFactor) : dotWidth) + 52 | spacing; 53 | if (dx <= (anchor += widthBound)) { 54 | return index; 55 | } 56 | } 57 | return -1; 58 | } 59 | 60 | @override 61 | ExpandingDotsEffect lerp(covariant ExpandingDotsEffect? other, double t) { 62 | if (other == null) return this; 63 | return ExpandingDotsEffect( 64 | expansionFactor: BasicIndicatorEffect.lerpDouble( 65 | expansionFactor, other.expansionFactor, t)!, 66 | dotWidth: BasicIndicatorEffect.lerpDouble(dotWidth, other.dotWidth, t)!, 67 | dotHeight: 68 | BasicIndicatorEffect.lerpDouble(dotHeight, other.dotHeight, t)!, 69 | spacing: BasicIndicatorEffect.lerpDouble(spacing, other.spacing, t)!, 70 | radius: BasicIndicatorEffect.lerpDouble(radius, other.radius, t)!, 71 | dotColor: Color.lerp(dotColor, other.dotColor, t), 72 | activeDotColor: Color.lerp(activeDotColor, other.activeDotColor, t), 73 | strokeWidth: 74 | BasicIndicatorEffect.lerpDouble(strokeWidth, other.strokeWidth, t)!, 75 | paintStyle: t < 0.5 ? paintStyle : other.paintStyle, 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/effects/scale_effect.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/src/painters/indicator_painter.dart'; 3 | import 'package:smooth_page_indicator/src/painters/scale_painter.dart'; 4 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 5 | 6 | import 'indicator_effect.dart'; 7 | 8 | /// Holds painting configuration to be used by [ScalePainter] 9 | class ScaleEffect extends BasicIndicatorEffect { 10 | /// Inactive dots paint style (fill|stroke) defaults to fill. 11 | final PaintingStyle activePaintStyle; 12 | 13 | /// This is ignored if [activePaintStyle] is PaintStyle.fill 14 | final double activeStrokeWidth; 15 | 16 | /// [scale] is multiplied by [dotWidth] to resolve 17 | /// active dot scaling 18 | final double scale; 19 | 20 | /// Default constructor 21 | const ScaleEffect({ 22 | super.activeDotColor, 23 | this.activePaintStyle = PaintingStyle.fill, 24 | this.scale = 1.4, 25 | this.activeStrokeWidth = 1.0, 26 | double offset = 16.0, 27 | super.dotWidth = 16.0, 28 | super.dotHeight = 16.0, 29 | super.spacing = 10.0, 30 | super.radius = 16, 31 | super.dotColor, 32 | super.strokeWidth = 1.0, 33 | super.paintStyle = PaintingStyle.fill, 34 | }); 35 | 36 | @override 37 | Size calculateSize(int count) { 38 | /// Add the scaled dot width to our size calculation 39 | final activeDotWidth = dotWidth * scale; 40 | final nonActiveCount = count - 1; 41 | return Size( 42 | (dotWidth * nonActiveCount) + (spacing * nonActiveCount) + activeDotWidth, 43 | activeDotWidth, 44 | ); 45 | } 46 | 47 | @override 48 | IndicatorPainter buildPainter( 49 | int count, double offset, DefaultIndicatorColors indicatorColors) { 50 | return ScalePainter( 51 | count: count, 52 | offset: offset, 53 | effect: this, 54 | indicatorColors: indicatorColors); 55 | } 56 | 57 | @override 58 | ScaleEffect lerp(covariant ScaleEffect? other, double t) { 59 | if (other == null) return this; 60 | return ScaleEffect( 61 | activePaintStyle: t < 0.5 ? activePaintStyle : other.activePaintStyle, 62 | activeStrokeWidth: BasicIndicatorEffect.lerpDouble( 63 | activeStrokeWidth, other.activeStrokeWidth, t)!, 64 | scale: BasicIndicatorEffect.lerpDouble(scale, other.scale, t)!, 65 | dotWidth: BasicIndicatorEffect.lerpDouble(dotWidth, other.dotWidth, t)!, 66 | dotHeight: 67 | BasicIndicatorEffect.lerpDouble(dotHeight, other.dotHeight, t)!, 68 | spacing: BasicIndicatorEffect.lerpDouble(spacing, other.spacing, t)!, 69 | radius: BasicIndicatorEffect.lerpDouble(radius, other.radius, t)!, 70 | dotColor: Color.lerp(dotColor, other.dotColor, t), 71 | activeDotColor: Color.lerp(activeDotColor, other.activeDotColor, t), 72 | strokeWidth: 73 | BasicIndicatorEffect.lerpDouble(strokeWidth, other.strokeWidth, t)!, 74 | paintStyle: t < 0.5 ? paintStyle : other.paintStyle, 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | ## [2.0.1] 3 | - No actual changes, added golden tests to pubignore 4 | 5 | ## [2.0.0] [Breaking change] 6 | - **Breaking**: Default colors (`dotColor`, `activeDotColor`) are now derived from the app's theme (`primaryColor` and `unselectedWidgetColor`) instead of hardcoded values. This may affect existing implementations that relied on the previous default colors. 7 | - Add `SmoothPageIndicatorTheme` - a `ThemeExtension` that allows configuring default effect and colors app-wide 8 | - Add `DefaultIndicatorColors` class for managing indicator color defaults 9 | 10 | ## [1.2.1] 11 | - Add infinite loop support to SwapEffect 12 | - Allow small dot size in ScrollingDotEffect 13 | ## [1.2.0+3] 14 | - no changes, just to update readme file 15 | - 16 | ## [1.2.0] 17 | 18 | - Fix Missing infinite loop support in AnimatedSmoothIndicator #79 19 | - export indicator_painter 20 | 21 | ## [1.1.0] 22 | 23 | - Optimize size and rotation so they're not calculated on every frame 24 | - Add WormEffect variants (WormType.underground,WormType.thinUnderground) 25 | - Add SlideEffect variant (SlideType.slideUnder 26 | - Add missing public api docs 27 | 28 | ## [1.0.1] 29 | 30 | - Fix scaleEffect in correct margins 31 | 32 | ## [1.0.0+2] 33 | 34 | - fix readme file (attempt 2) 35 | 36 | ## [1.0.0+1] 37 | 38 | - fix readme file 39 | 40 | ## [1.0.0] [Breaking change in JumpingDotEffect] 41 | 42 | - Fix ignored active dot stroke in ColorTransitionEffect #40 43 | - Fix crash when last item is removed #21 44 | - Add loop support 45 | - Add variants to SwapEffect (zRotation, YRotation) 46 | - Add variants to WormEffect (thin worm) 47 | - Rename [elevation] property from JumpingDotEffect [Breaking] 48 | - Add customization params to JumpingDotEffect (jumpScale, verticalOffset) 49 | - Add CustomizableEffect 50 | 51 | ## [0.3.0-nullsafety.0] 52 | 53 | - Move to null safety 54 | - Add runnable example 55 | - change default offset value to 16.0 56 | 57 | ## [0.2.0] 58 | 59 | - Add support for vertical direction 60 | - Add on dot clicked callback 61 | - Add AnimatedSmoothIndicator which works without a PageController 62 | 63 | ## [0.1.5] 64 | 65 | - Add off-canvas scrolling effect ScrollingDotsEffect 66 | - Add Active color with transition to ScaleEffect 67 | 68 | ## [0.1.4] 69 | 70 | - Add Active color with transition to ExpendingDotEffect 71 | 72 | ## [0.1.3] 73 | 74 | - Fix indicator always starts at zero index regardless of the controller's initial page 75 | 76 | ## [0.1.2+1] 77 | 78 | - Add individual demos for each effect to README file 79 | 80 | ## [0.1.2] 81 | 82 | - Add Color Transition effect 83 | 84 | ### Breaking change! 85 | 86 | - Replace isRTL with textDirection 87 | - Directionality is now handled the flutter way instead of manually passing a bool value to isRTL 88 | property 89 | 90 | ## [0.1.1] 91 | 92 | - Add documentation 93 | - Edit README file 94 | 95 | ## [0.1.0+1] 96 | 97 | - Edit README file. 98 | 99 | ## [0.1.0] 100 | 101 | - Initial release. 102 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: A new Flutter project. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.12.0 <3.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | smooth_page_indicator: 27 | path: ../ 28 | loop_page_view: ^1.0.11 29 | 30 | # The following adds the Cupertino Icons font to your application. 31 | # Use with the CupertinoIcons class for iOS style icons. 32 | cupertino_icons: ^1.0.2 33 | 34 | dev_dependencies: 35 | flutter_test: 36 | sdk: flutter 37 | integration_test: 38 | sdk: flutter 39 | 40 | # For information on the generic Dart part of this file, see the 41 | # following page: https://dart.dev/tools/pub/pubspec 42 | 43 | # The following section is specific to Flutter. 44 | flutter: 45 | 46 | # The following line ensures that the Material Icons font is 47 | # included with your application, so that you can use the icons in 48 | # the material Icons class. 49 | uses-material-design: true 50 | 51 | # To add assets to your application, add an assets section, like this: 52 | # assets: 53 | # - images/a_dot_burr.jpeg 54 | # - images/a_dot_ham.jpeg 55 | 56 | # An image asset can refer to one or more resolution-specific "variants", see 57 | # https://flutter.dev/assets-and-images/#resolution-aware. 58 | 59 | # For details regarding adding assets from package dependencies, see 60 | # https://flutter.dev/assets-and-images/#from-packages 61 | 62 | # To add custom fonts to your application, add a fonts section here, 63 | # in this "flutter" section. Each entry in this list should have a 64 | # "family" key with the font family name, and a "fonts" key with a 65 | # list giving the asset and other descriptors for the font. For 66 | # example: 67 | # fonts: 68 | # - family: Schyler 69 | # fonts: 70 | # - asset: fonts/Schyler-Regular.ttf 71 | # - asset: fonts/Schyler-Italic.ttf 72 | # style: italic 73 | # - family: Trajan Pro 74 | # fonts: 75 | # - asset: fonts/TrajanPro.ttf 76 | # - asset: fonts/TrajanPro_Bold.ttf 77 | # weight: 700 78 | # 79 | # For details regarding fonts from package dependencies, 80 | # see https://flutter.dev/custom-fonts/#from-packages 81 | -------------------------------------------------------------------------------- /test/src/effects/swap_effect_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 4 | 5 | void main() { 6 | group('SwapEffect', () { 7 | test('calculateSize for normal type', () { 8 | const effect = SwapEffect( 9 | dotWidth: 16, 10 | dotHeight: 16, 11 | spacing: 8, 12 | type: SwapType.normal, 13 | ); 14 | 15 | final size = effect.calculateSize(5); 16 | expect(size.width, 16 * 5 + 8 * 5); 17 | expect(size.height, 16); 18 | }); 19 | 20 | test('calculateSize for zRotation type', () { 21 | const effect = SwapEffect( 22 | dotWidth: 16, 23 | dotHeight: 16, 24 | spacing: 8, 25 | type: SwapType.zRotation, 26 | ); 27 | 28 | final size = effect.calculateSize(5); 29 | expect(size.height, 16 + 16 * 0.2); 30 | }); 31 | 32 | test('calculateSize for yRotation type', () { 33 | const effect = SwapEffect( 34 | dotWidth: 16, 35 | dotHeight: 16, 36 | spacing: 8, 37 | type: SwapType.yRotation, 38 | ); 39 | 40 | final size = effect.calculateSize(5); 41 | expect(size.height, 16 + 16 + 8); 42 | }); 43 | 44 | test('buildPainter returns IndicatorPainter', () { 45 | const effect = SwapEffect(); 46 | final painter = 47 | effect.buildPainter(5, 0, DefaultIndicatorColors.defaults); 48 | 49 | expect(painter, isA()); 50 | }); 51 | 52 | testWidgets('paints correctly', (tester) async { 53 | const effect = SwapEffect(); 54 | 55 | await tester.pumpWidget( 56 | MaterialApp( 57 | home: Scaffold( 58 | body: CustomPaint( 59 | size: effect.calculateSize(5), 60 | painter: 61 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 62 | ), 63 | ), 64 | ), 65 | ); 66 | 67 | expect(find.byType(CustomPaint), findsWidgets); 68 | }); 69 | 70 | testWidgets('yRotation type paints correctly', (tester) async { 71 | const effect = SwapEffect(type: SwapType.yRotation); 72 | 73 | await tester.pumpWidget( 74 | MaterialApp( 75 | home: Scaffold( 76 | body: CustomPaint( 77 | size: effect.calculateSize(5), 78 | painter: 79 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 80 | ), 81 | ), 82 | ), 83 | ); 84 | 85 | expect(find.byType(CustomPaint), findsWidgets); 86 | }); 87 | 88 | testWidgets('zRotation type paints correctly', (tester) async { 89 | const effect = SwapEffect(type: SwapType.zRotation); 90 | 91 | await tester.pumpWidget( 92 | MaterialApp( 93 | home: Scaffold( 94 | body: CustomPaint( 95 | size: effect.calculateSize(5), 96 | painter: 97 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 98 | ), 99 | ), 100 | ), 101 | ); 102 | 103 | expect(find.byType(CustomPaint), findsWidgets); 104 | }); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /lib/src/effects/indicator_effect.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui show lerpDouble; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:smooth_page_indicator/src/painters/indicator_painter.dart'; 5 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 6 | 7 | /// An Abstraction for a dot-indicator animation effect 8 | abstract class IndicatorEffect { 9 | /// Const constructor 10 | const IndicatorEffect(); 11 | 12 | /// Builds a new painter every time the page offset changes 13 | /// 14 | /// [indicatorColors] is used to resolve null dot colors 15 | IndicatorPainter buildPainter( 16 | int count, double offset, DefaultIndicatorColors indicatorColors); 17 | 18 | /// Calculates the size of canvas based on 19 | /// dots count, size and spacing 20 | /// 21 | /// Implementers can override this function 22 | /// to calculate their own size 23 | Size calculateSize(int count); 24 | 25 | /// Returns the index of the section that contains [dx]. 26 | /// 27 | /// Sections or hit-targets are calculated differently 28 | /// in some effects 29 | int hitTestDots(double dx, int count, double current); 30 | 31 | /// Linearly interpolates between two effects. 32 | /// Returns [this] if [other] is null or not the same type. 33 | IndicatorEffect lerp(covariant IndicatorEffect? other, double t); 34 | } 35 | 36 | /// Basic implementation of [IndicatorEffect] that holds some shared 37 | /// properties and behaviors between different effects 38 | abstract class BasicIndicatorEffect extends IndicatorEffect { 39 | /// Singe dot width 40 | final double dotWidth; 41 | 42 | /// Singe dot height 43 | final double dotHeight; 44 | 45 | /// The horizontal space between dots 46 | final double spacing; 47 | 48 | /// Single dot radius 49 | final double radius; 50 | 51 | /// Inactive dots color or all dots in some effects 52 | /// If null, defaults to the app's primary color with reduced opacity 53 | final Color? dotColor; 54 | 55 | /// The active dot color 56 | /// If null, defaults to the app's primary color 57 | final Color? activeDotColor; 58 | 59 | /// Inactive dots paint style (fill|stroke) defaults to fill. 60 | final PaintingStyle paintStyle; 61 | 62 | /// This is ignored if [paintStyle] is PaintStyle.fill 63 | final double strokeWidth; 64 | 65 | /// Default construe 66 | const BasicIndicatorEffect({ 67 | required this.strokeWidth, 68 | required this.dotWidth, 69 | required this.dotHeight, 70 | required this.spacing, 71 | required this.radius, 72 | required this.dotColor, 73 | required this.paintStyle, 74 | required this.activeDotColor, 75 | }) : assert(dotWidth >= 0 && 76 | dotHeight >= 0 && 77 | spacing >= 0 && 78 | strokeWidth >= 0); 79 | 80 | @override 81 | Size calculateSize(int count) { 82 | return Size(dotWidth * count + (spacing * (count - 1)), dotHeight); 83 | } 84 | 85 | @override 86 | int hitTestDots(double dx, int count, double current) { 87 | var offset = -spacing / 2; 88 | for (var index = 0; index < count; index++) { 89 | if (dx <= (offset += dotWidth + spacing)) { 90 | return index; 91 | } 92 | } 93 | return -1; 94 | } 95 | 96 | /// Helper method for lerping double values 97 | @protected 98 | static double? lerpDouble(double? a, double? b, double t) => 99 | ui.lerpDouble(a, b, t); 100 | } 101 | -------------------------------------------------------------------------------- /lib/src/painters/scrolling_dots_painter_with_fixed_center.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 3 | 4 | /// Paints dots scrolling transition effect and considers 5 | /// active dot to always be in the center 6 | /// Live demo at 7 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/scrolling-dots-2.gif 8 | class ScrollingDotsWithFixedCenterPainter extends BasicIndicatorPainter { 9 | /// The painting configuration 10 | final ScrollingDotsEffect effect; 11 | 12 | /// Default constructor 13 | ScrollingDotsWithFixedCenterPainter({ 14 | required this.effect, 15 | required int count, 16 | required double offset, 17 | required DefaultIndicatorColors indicatorColors, 18 | }) : super(offset, count, effect, indicatorColors); 19 | 20 | @override 21 | void paint(Canvas canvas, Size size) { 22 | var current = offset.floor(); 23 | var dotOffset = offset - current; 24 | var dotPaint = Paint() 25 | ..strokeWidth = effect.strokeWidth 26 | ..style = effect.paintStyle; 27 | 28 | for (var index = 0; index < count; index++) { 29 | var color = effectiveInactiveColor; 30 | if (index == current) { 31 | // ! Both a and b are non nullable 32 | color = Color.lerp( 33 | effectiveActiveColor, effectiveInactiveColor, dotOffset)!; 34 | } else if (index - 1 == current) { 35 | // ! Both a and b are non nullable 36 | color = Color.lerp( 37 | effectiveActiveColor, effectiveInactiveColor, 1 - dotOffset)!; 38 | } 39 | 40 | var scale = 1.0; 41 | final smallDotScale = effect.smallDotScale; 42 | final revDotOffset = 1 - dotOffset; 43 | final switchPoint = (effect.maxVisibleDots - 1) / 2; 44 | 45 | if (count > effect.maxVisibleDots) { 46 | if (index >= current - switchPoint && 47 | index <= current + (switchPoint + 1)) { 48 | if (index == (current + switchPoint)) { 49 | scale = smallDotScale + ((1 - smallDotScale) * dotOffset); 50 | } else if (index == current - (switchPoint - 1)) { 51 | scale = 1 - (1 - smallDotScale) * dotOffset; 52 | } else if (index == current - switchPoint) { 53 | scale = (smallDotScale * revDotOffset); 54 | } else if (index == current + (switchPoint + 1)) { 55 | scale = (smallDotScale * dotOffset); 56 | } 57 | } else { 58 | continue; 59 | } 60 | } 61 | 62 | final rRect = _calcBounds( 63 | size.height, 64 | size.width / 2 - (offset * (effect.dotWidth + effect.spacing)), 65 | index, 66 | scale, 67 | ); 68 | 69 | canvas.drawRRect(rRect, dotPaint..color = color); 70 | } 71 | 72 | final rRect = 73 | _calcBounds(size.height, size.width / 2, 0, effect.activeDotScale); 74 | canvas.drawRRect( 75 | rRect, 76 | Paint() 77 | ..color = effectiveActiveColor 78 | ..strokeWidth = effect.activeStrokeWidth 79 | ..style = PaintingStyle.stroke); 80 | } 81 | 82 | RRect _calcBounds(double canvasHeight, double startingPoint, num i, 83 | [double scale = 1.0]) { 84 | final scaledWidth = effect.dotWidth * scale; 85 | final scaledHeight = effect.dotHeight * scale; 86 | 87 | final xPos = startingPoint + (effect.dotWidth + effect.spacing) * i; 88 | final yPos = canvasHeight / 2; 89 | return RRect.fromLTRBR( 90 | xPos - scaledWidth / 2, 91 | yPos - scaledHeight / 2, 92 | xPos + scaledWidth / 2, 93 | yPos + scaledHeight / 2, 94 | dotRadius * scale, 95 | ); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/src/effects/scrolling_dots_effect_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 4 | 5 | void main() { 6 | group('ScrollingDotsEffect', () { 7 | test('calculateSize for normal mode', () { 8 | const effect = ScrollingDotsEffect( 9 | dotWidth: 16, 10 | dotHeight: 16, 11 | spacing: 8, 12 | activeDotScale: 1.3, 13 | maxVisibleDots: 5, 14 | ); 15 | 16 | final size = effect.calculateSize(10); 17 | expect(size.width, (16 + 8) * 5); 18 | expect(size.height, 16 * 1.3); 19 | }); 20 | 21 | test('calculateSize when count is less than maxVisibleDots', () { 22 | const effect = ScrollingDotsEffect( 23 | dotWidth: 16, 24 | dotHeight: 16, 25 | spacing: 8, 26 | maxVisibleDots: 5, 27 | ); 28 | 29 | final size = effect.calculateSize(3); 30 | expect(size.width, (16 + 8) * 3); 31 | }); 32 | 33 | test('hitTestDots for non-fixed center', () { 34 | const effect = ScrollingDotsEffect( 35 | dotWidth: 16, 36 | spacing: 8, 37 | maxVisibleDots: 5, 38 | ); 39 | 40 | expect(effect.hitTestDots(10, 10, 0), 0); 41 | }); 42 | 43 | test('fixedCenter mode', () { 44 | const effect = ScrollingDotsEffect( 45 | fixedCenter: true, 46 | maxVisibleDots: 5, 47 | ); 48 | 49 | expect(effect.fixedCenter, true); 50 | }); 51 | 52 | test('hitTestDots with fixedCenter', () { 53 | const effect = ScrollingDotsEffect( 54 | dotWidth: 16, 55 | spacing: 8, 56 | maxVisibleDots: 5, 57 | fixedCenter: true, 58 | ); 59 | 60 | final result = effect.hitTestDots(10, 10, 2); 61 | expect(result, isA()); 62 | }); 63 | 64 | test('calculateSize with fixedCenter and count <= maxVisibleDots', () { 65 | const effect = ScrollingDotsEffect( 66 | dotWidth: 16, 67 | dotHeight: 16, 68 | spacing: 8, 69 | maxVisibleDots: 5, 70 | fixedCenter: true, 71 | ); 72 | 73 | final size = effect.calculateSize(3); 74 | // ((count * 2) - 1) * (dotWidth + spacing) 75 | expect(size.width, (3 * 2 - 1) * (16 + 8)); 76 | }); 77 | 78 | test('buildPainter returns BasicIndicatorPainter', () { 79 | const effect = ScrollingDotsEffect(); 80 | final painter = 81 | effect.buildPainter(10, 0, DefaultIndicatorColors.defaults); 82 | 83 | expect(painter, isA()); 84 | }); 85 | 86 | testWidgets('paints correctly', (tester) async { 87 | const effect = ScrollingDotsEffect(); 88 | 89 | await tester.pumpWidget( 90 | MaterialApp( 91 | home: Scaffold( 92 | body: CustomPaint( 93 | size: effect.calculateSize(10), 94 | painter: 95 | effect.buildPainter(10, 3.5, DefaultIndicatorColors.defaults), 96 | ), 97 | ), 98 | ), 99 | ); 100 | 101 | expect(find.byType(CustomPaint), findsWidgets); 102 | }); 103 | 104 | testWidgets('fixedCenter painter paints correctly', (tester) async { 105 | const effect = ScrollingDotsEffect(fixedCenter: true); 106 | 107 | await tester.pumpWidget( 108 | MaterialApp( 109 | home: Scaffold( 110 | body: CustomPaint( 111 | size: effect.calculateSize(10), 112 | painter: 113 | effect.buildPainter(10, 3, DefaultIndicatorColors.defaults), 114 | ), 115 | ), 116 | ), 117 | ); 118 | 119 | expect(find.byType(CustomPaint), findsWidgets); 120 | }); 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 57 | 59 | 65 | 66 | 67 | 68 | 69 | 70 | 76 | 78 | 84 | 85 | 86 | 87 | 89 | 90 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /lib/src/painters/swap_painter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:smooth_page_indicator/src/effects/swap_effect.dart'; 5 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 6 | 7 | import 'indicator_painter.dart'; 8 | 9 | /// Paints a swapping transition effect between active 10 | /// and non-active dots 11 | /// 12 | /// Live demo at 13 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/swap.gif 14 | class SwapPainter extends BasicIndicatorPainter { 15 | /// The painting configuration 16 | final SwapEffect effect; 17 | 18 | /// Default constructor 19 | SwapPainter({ 20 | required double offset, 21 | required this.effect, 22 | required int count, 23 | required DefaultIndicatorColors indicatorColors, 24 | }) : super(offset, count, effect, indicatorColors); 25 | 26 | @override 27 | void paint(Canvas canvas, Size size) { 28 | final current = offset.floor(); 29 | final dotOffset = offset - offset.floor(); 30 | final activePaint = Paint()..color = effectiveActiveColor; 31 | var dotScale = effect.dotWidth * .2; 32 | final yPos = size.height / 2; 33 | final xAnchor = effect.spacing / 2; 34 | 35 | final isGoingThroughPortal = offset > count - 1; 36 | if (isGoingThroughPortal) { 37 | final startDot = calcPortalTravel( 38 | size, 39 | (effect.dotWidth / 2) + xAnchor - ((1 - dotOffset) * distance), 40 | dotOffset); 41 | canvas.drawRRect(startDot, activePaint); 42 | 43 | final endDot = calcPortalTravel( 44 | size, 45 | ((count - 1) * distance) + 46 | (effect.dotWidth / 2) + 47 | xAnchor + 48 | (dotOffset * distance), 49 | 1 - dotOffset, 50 | ); 51 | canvas.drawRRect(endDot, activePaint); 52 | } 53 | 54 | void drawDot(double xPos, double yPos, Paint paint, [double scale = 0]) { 55 | final rRect = RRect.fromLTRBR( 56 | xPos, 57 | yPos - effect.dotHeight / 2, 58 | xPos + effect.dotWidth, 59 | yPos + effect.dotHeight / 2, 60 | dotRadius, 61 | ).inflate(scale); 62 | 63 | canvas.drawRRect(rRect, paint); 64 | } 65 | 66 | for (var i = count - 1; i >= 0; i--) { 67 | // if current or next 68 | if ((i == current || (i - 1 == current)) && !isGoingThroughPortal) { 69 | if (effect.type == SwapType.yRotation) { 70 | final piFactor = (dotOffset * math.pi); 71 | if (i == current) { 72 | var x = (1 - ((math.cos(piFactor) + 1) / 2)) * distance; 73 | var y = -math.sin(piFactor) * distance / 2; 74 | drawDot(xAnchor + distance * i + x, yPos + y, activePaint); 75 | } else { 76 | var x = -(1 - ((math.cos(piFactor) + 1) / 2)) * distance; 77 | var y = (math.sin(piFactor) * distance / 2); 78 | drawDot(xAnchor + distance * i + x, yPos + y, dotPaint); 79 | } 80 | } else { 81 | var posOffset = i.toDouble(); 82 | var scale = 0.0; 83 | if (effect.type == SwapType.zRotation) { 84 | scale = dotScale * dotOffset; 85 | if (dotOffset > .5) { 86 | scale = dotScale - (dotScale * dotOffset); 87 | } 88 | } 89 | if (i == current) { 90 | posOffset = offset; 91 | drawDot(xAnchor + posOffset * distance, yPos, activePaint, scale); 92 | } else { 93 | posOffset = i - dotOffset; 94 | drawDot(xAnchor + posOffset * distance, yPos, dotPaint, -scale); 95 | } 96 | } 97 | } else { 98 | if (isGoingThroughPortal && i == count - 1) { 99 | continue; 100 | } 101 | // draw still dots 102 | var xPos = xAnchor + i * distance; 103 | if (isGoingThroughPortal) { 104 | xPos = xPos + (dotOffset * distance); 105 | } 106 | drawDot(xPos, yPos, dotPaint); 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/src/painters/indicator_painter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/src/effects/indicator_effect.dart'; 3 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 4 | 5 | /// Basic implementation of [IndicatorPainter] that holds some shared 6 | /// properties and behaviors between different painters 7 | abstract class BasicIndicatorPainter extends IndicatorPainter { 8 | /// The count of pages 9 | final int count; 10 | 11 | /// The provided effect is passed to this super class 12 | /// to make some calculations and paint still dots 13 | final BasicIndicatorEffect _effect; 14 | 15 | /// The resolved indicator colors 16 | final DefaultIndicatorColors indicatorColors; 17 | 18 | /// The resolved dot color (inactive dots) 19 | final Color effectiveInactiveColor; 20 | 21 | /// The resolved active dot color 22 | final Color effectiveActiveColor; 23 | 24 | /// Inactive dot paint or base paint in one-color effects. 25 | final Paint dotPaint; 26 | 27 | /// The Radius of all dots 28 | final Radius dotRadius; 29 | 30 | /// Default constructor 31 | BasicIndicatorPainter( 32 | super.offset, 33 | this.count, 34 | this._effect, 35 | this.indicatorColors, 36 | ) : dotRadius = Radius.circular(_effect.radius), 37 | effectiveInactiveColor = indicatorColors.resolveInactiveColor(_effect), 38 | effectiveActiveColor = indicatorColors.resolveActiveColor(_effect), 39 | dotPaint = Paint() 40 | ..color = indicatorColors.resolveInactiveColor(_effect) 41 | ..style = _effect.paintStyle 42 | ..strokeWidth = _effect.strokeWidth; 43 | 44 | /// The distance between dot lefts 45 | double get distance => _effect.dotWidth + _effect.spacing; 46 | 47 | /// Paints [count] number of dots with no animation 48 | /// 49 | /// Meant to be used by effects that only 50 | /// animate the active dot 51 | void paintStillDots(Canvas canvas, Size size) { 52 | for (var i = 0; i < count; i++) { 53 | final rect = buildStillDot(i, size); 54 | canvas.drawRRect(rect, dotPaint); 55 | } 56 | } 57 | 58 | /// Builds a single still dot 59 | RRect buildStillDot(int i, Size size) { 60 | final xPos = (i * distance); 61 | final yPos = size.height / 2; 62 | final bounds = Rect.fromLTRB( 63 | xPos, 64 | yPos - _effect.dotHeight / 2, 65 | xPos + _effect.dotWidth, 66 | yPos + _effect.dotHeight / 2, 67 | ); 68 | var rect = RRect.fromRectAndRadius(bounds, dotRadius); 69 | return rect; 70 | } 71 | 72 | /// Masks spaces between dots 73 | /// 74 | /// used by under-layer effects like WormType.underground 75 | void maskStillDots(Size size, Canvas canvas) { 76 | var path = Path()..addRect((const Offset(0, 0) & size)); 77 | for (var i = 0; i < count; i++) { 78 | path = Path.combine(PathOperation.difference, path, 79 | Path()..addRRect(buildStillDot(i, size))); 80 | } 81 | canvas.drawPath(path, Paint()..blendMode = BlendMode.clear); 82 | } 83 | 84 | /// Calculates the shape of a dot while portal-traveling 85 | /// form last-to-first dot or form first-to-last dot 86 | RRect calcPortalTravel(Size size, double offset, double dotOffset) { 87 | final yPos = size.height / 2; 88 | var width = dotOffset * (_effect.dotHeight / 2); 89 | var height = dotOffset * (_effect.dotWidth / 2); 90 | var xPos = offset; 91 | return RRect.fromLTRBR( 92 | xPos - height, 93 | yPos - width, 94 | xPos + height, 95 | yPos + width, 96 | Radius.circular(dotRadius.x * dotOffset), 97 | ); 98 | } 99 | } 100 | 101 | /// A basic abstract implementation of [customPainter] 102 | /// to avoid overriding [shouldRepaint] in every painter 103 | abstract class IndicatorPainter extends CustomPainter { 104 | /// The raw offset from the [PageController].page 105 | final double offset; 106 | 107 | /// Default constructor 108 | const IndicatorPainter(this.offset); 109 | 110 | @override 111 | bool shouldRepaint(IndicatorPainter oldDelegate) { 112 | /// only repaint if the offset changes 113 | return oldDelegate.offset != offset; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/src/painters/scrolling_dots_painter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 6 | 7 | /// Paints a scale-dot transition effect between active 8 | /// and non-active dots 9 | /// 10 | /// Good for big pages count because it can show 11 | /// only [ScrollingDotsEffect.maxVisibleDots] at once 12 | /// and scrolls as needed 13 | /// 14 | /// Live demo at 15 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/scrolling-dots-2.gif 16 | class ScrollingDotsPainter extends BasicIndicatorPainter { 17 | /// The painting configuration 18 | final ScrollingDotsEffect effect; 19 | 20 | /// Default constructor 21 | ScrollingDotsPainter({ 22 | required this.effect, 23 | required int count, 24 | required double offset, 25 | required DefaultIndicatorColors indicatorColors, 26 | }) : super(offset, count, effect, indicatorColors); 27 | 28 | @override 29 | void paint(Canvas canvas, Size size) { 30 | final current = super.offset.floor(); 31 | final switchPoint = (effect.maxVisibleDots / 2).floor(); 32 | final firstVisibleDot = 33 | (current < switchPoint || count - 1 < effect.maxVisibleDots) 34 | ? 0 35 | : min(current - switchPoint, count - effect.maxVisibleDots); 36 | final lastVisibleDot = 37 | min(firstVisibleDot + effect.maxVisibleDots, count - 1); 38 | final inPreScrollRange = current < switchPoint; 39 | final inAfterScrollRange = current >= (count - 1) - switchPoint; 40 | final willStartScrolling = (current + 1) == switchPoint + 1; 41 | final willStopScrolling = current + 1 == (count - 1) - switchPoint; 42 | 43 | final dotOffset = offset - offset.toInt(); 44 | final dotPaint = Paint() 45 | ..strokeWidth = effect.strokeWidth 46 | ..style = effect.paintStyle; 47 | 48 | final drawingAnchor = (inPreScrollRange || inAfterScrollRange) 49 | ? -(firstVisibleDot * distance) 50 | : -((offset - switchPoint) * distance); 51 | 52 | final smallDotScale = effect.smallDotScale; 53 | final activeScale = effect.activeDotScale - 1.0; 54 | for (var index = firstVisibleDot; index <= lastVisibleDot; index++) { 55 | var color = effectiveInactiveColor; 56 | 57 | var scale = 1.0; 58 | 59 | if (index == current) { 60 | // ! Both a and b are non nullable 61 | color = Color.lerp( 62 | effectiveActiveColor, effectiveInactiveColor, dotOffset)!; 63 | if (offset > count - 1 && count > effect.maxVisibleDots) { 64 | scale = effect.activeDotScale - (smallDotScale * dotOffset); 65 | } else { 66 | scale = effect.activeDotScale - (activeScale * dotOffset); 67 | } 68 | } else if ((index == firstVisibleDot && offset > count - 1)) { 69 | color = Color.lerp( 70 | effectiveInactiveColor, effectiveActiveColor, dotOffset)!; 71 | if (count <= effect.maxVisibleDots) { 72 | scale = 1 + (activeScale * dotOffset); 73 | } else { 74 | scale = 75 | smallDotScale + (((1 - smallDotScale) + activeScale) * dotOffset); 76 | } 77 | } else if (index - 1 == current) { 78 | // ! Both a and b are non nullable 79 | color = Color.lerp( 80 | effectiveInactiveColor, effectiveActiveColor, dotOffset)!; 81 | scale = 1.0 + (activeScale * dotOffset); 82 | } else if (count - 1 < effect.maxVisibleDots) { 83 | scale = 1.0; 84 | } else if (index == firstVisibleDot) { 85 | if (willStartScrolling) { 86 | scale = (1.0 * (1.0 - dotOffset)); 87 | } else if (inAfterScrollRange) { 88 | scale = smallDotScale; 89 | } else if (!inPreScrollRange) { 90 | scale = smallDotScale * (1.0 - dotOffset); 91 | } 92 | } else if (index == firstVisibleDot + 1 && 93 | !(inPreScrollRange || inAfterScrollRange)) { 94 | scale = 1.0 - (dotOffset * (1.0 - smallDotScale)); 95 | } else if (index == lastVisibleDot - 1.0) { 96 | if (inPreScrollRange) { 97 | scale = smallDotScale; 98 | } else if (!inAfterScrollRange) { 99 | scale = smallDotScale + ((1.0 - smallDotScale) * dotOffset); 100 | } 101 | } else if (index == lastVisibleDot) { 102 | if (inPreScrollRange) { 103 | scale = 0.0; 104 | } else if (willStopScrolling) { 105 | scale = dotOffset; 106 | } else if (!inAfterScrollRange) { 107 | scale = smallDotScale * dotOffset; 108 | } 109 | } 110 | 111 | final scaledWidth = (effect.dotWidth * scale); 112 | final scaledHeight = effect.dotHeight * scale; 113 | final yPos = size.height / 2; 114 | final xPos = effect.dotWidth / 2 + drawingAnchor + (index * distance); 115 | 116 | final rRect = RRect.fromLTRBR( 117 | xPos - scaledWidth / 2 + effect.spacing / 2, 118 | yPos - scaledHeight / 2, 119 | xPos + scaledWidth / 2 + effect.spacing / 2, 120 | yPos + scaledHeight / 2, 121 | dotRadius * scale, 122 | ); 123 | 124 | canvas.drawRRect(rRect, dotPaint..color = color); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/src/effects/scrolling_dots_effect.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:smooth_page_indicator/src/painters/indicator_painter.dart'; 5 | import 'package:smooth_page_indicator/src/painters/scrolling_dots_painter.dart'; 6 | import 'package:smooth_page_indicator/src/painters/scrolling_dots_painter_with_fixed_center.dart'; 7 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 8 | 9 | import 'indicator_effect.dart'; 10 | 11 | /// Holds painting configuration to be used by [ScrollingDotsPainter] 12 | /// or [ScrollingDotsWithFixedCenterPainter] 13 | class ScrollingDotsEffect extends BasicIndicatorEffect { 14 | /// The active dot strokeWidth 15 | /// this is ignored if [fixedCenter] is false 16 | final double activeStrokeWidth; 17 | 18 | /// [activeDotScale] is multiplied by [dotWidth] to resolve 19 | /// active dot scaling 20 | final double activeDotScale; 21 | 22 | /// [smallDotScale] is multiplied by [dotWidth] to resolve 23 | /// side dots 24 | final double smallDotScale; 25 | 26 | /// The max number of dots to display at a time 27 | /// if count is <= [maxVisibleDots] [maxVisibleDots] = count 28 | /// must be an odd number that's >= 5 29 | final int maxVisibleDots; 30 | 31 | /// if True the old center dot style will be used 32 | final bool fixedCenter; 33 | 34 | /// Default constructor 35 | const ScrollingDotsEffect({ 36 | this.activeStrokeWidth = 1.5, 37 | this.activeDotScale = 1.3, 38 | this.smallDotScale = 0.66, 39 | this.maxVisibleDots = 5, 40 | this.fixedCenter = false, 41 | double offset = 16.0, 42 | super.dotWidth = 16.0, 43 | super.dotHeight = 16.0, 44 | super.spacing = 8.0, 45 | super.radius = 16, 46 | super.dotColor, 47 | super.activeDotColor, 48 | super.strokeWidth = 1.0, 49 | super.paintStyle = PaintingStyle.fill, 50 | }) : assert(activeDotScale >= 0.0), 51 | assert(maxVisibleDots >= 5 && maxVisibleDots % 2 != 0); 52 | 53 | @override 54 | Size calculateSize(int count) { 55 | /// Add the scaled dot width to our size calculation 56 | var width = (dotWidth + spacing) * (min(count, maxVisibleDots)); 57 | if (fixedCenter && count <= maxVisibleDots) { 58 | width = ((count * 2) - 1) * (dotWidth + spacing); 59 | } 60 | return Size(width, dotHeight * activeDotScale); 61 | } 62 | 63 | @override 64 | int hitTestDots(double dx, int count, double current) { 65 | final switchPoint = (maxVisibleDots / 2).floor(); 66 | if (fixedCenter) { 67 | return super.hitTestDots(dx, count, current) - 68 | switchPoint + 69 | current.floor(); 70 | } else { 71 | final firstVisibleDot = 72 | (current < switchPoint || count - 1 < maxVisibleDots) 73 | ? 0 74 | : min(current - switchPoint, count - maxVisibleDots).floor(); 75 | final lastVisibleDot = 76 | min(firstVisibleDot + maxVisibleDots, count - 1).floor(); 77 | var offset = 0.0; 78 | for (var index = firstVisibleDot; index <= lastVisibleDot; index++) { 79 | if (dx <= (offset += dotWidth + spacing)) { 80 | return index; 81 | } 82 | } 83 | } 84 | return -1; 85 | } 86 | 87 | @override 88 | BasicIndicatorPainter buildPainter( 89 | int count, double offset, DefaultIndicatorColors indicatorColors) { 90 | if (fixedCenter) { 91 | assert( 92 | offset.ceil() < count, 93 | 'ScrollingDotsWithFixedCenterPainter does not support infinite looping.', 94 | ); 95 | return ScrollingDotsWithFixedCenterPainter( 96 | count: count, 97 | offset: offset, 98 | effect: this, 99 | indicatorColors: indicatorColors, 100 | ); 101 | } else { 102 | return ScrollingDotsPainter( 103 | count: count, 104 | offset: offset, 105 | effect: this, 106 | indicatorColors: indicatorColors, 107 | ); 108 | } 109 | } 110 | 111 | @override 112 | ScrollingDotsEffect lerp(covariant ScrollingDotsEffect? other, double t) { 113 | if (other == null) return this; 114 | return ScrollingDotsEffect( 115 | activeStrokeWidth: BasicIndicatorEffect.lerpDouble( 116 | activeStrokeWidth, other.activeStrokeWidth, t)!, 117 | activeDotScale: BasicIndicatorEffect.lerpDouble( 118 | activeDotScale, other.activeDotScale, t)!, 119 | smallDotScale: BasicIndicatorEffect.lerpDouble( 120 | smallDotScale, other.smallDotScale, t)!, 121 | maxVisibleDots: t < 0.5 ? maxVisibleDots : other.maxVisibleDots, 122 | fixedCenter: t < 0.5 ? fixedCenter : other.fixedCenter, 123 | dotWidth: BasicIndicatorEffect.lerpDouble(dotWidth, other.dotWidth, t)!, 124 | dotHeight: 125 | BasicIndicatorEffect.lerpDouble(dotHeight, other.dotHeight, t)!, 126 | spacing: BasicIndicatorEffect.lerpDouble(spacing, other.spacing, t)!, 127 | radius: BasicIndicatorEffect.lerpDouble(radius, other.radius, t)!, 128 | dotColor: Color.lerp(dotColor, other.dotColor, t), 129 | activeDotColor: Color.lerp(activeDotColor, other.activeDotColor, t), 130 | strokeWidth: 131 | BasicIndicatorEffect.lerpDouble(strokeWidth, other.strokeWidth, t)!, 132 | paintStyle: t < 0.5 ? paintStyle : other.paintStyle, 133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /lib/src/theme_defaults.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 3 | 4 | /// Holds theme-derived default colors for the indicator 5 | class DefaultIndicatorColors { 6 | /// The color for active dots (defaults to theme's primary color) 7 | final Color active; 8 | 9 | /// The color for inactive dots (defaults to theme's primary color with reduced opacity) 10 | final Color inactive; 11 | 12 | /// Creates an [DefaultIndicatorColors] instance 13 | const DefaultIndicatorColors({ 14 | required this.active, 15 | required this.inactive, 16 | }); 17 | 18 | /// Resolves [dotColor] using the provided [DefaultIndicatorColors] if null 19 | Color resolveInactiveColor(BasicIndicatorEffect effect) { 20 | return effect.dotColor ?? inactive; 21 | } 22 | 23 | /// Resolves [activeDotColor] using the provided [DefaultIndicatorColors] if null 24 | Color resolveActiveColor(BasicIndicatorEffect effect) { 25 | return effect.activeDotColor ?? active; 26 | } 27 | 28 | /// Creates [DefaultIndicatorColors] from the given [BuildContext] 29 | /// using the app's primary color 30 | factory DefaultIndicatorColors.fromContext(BuildContext context) { 31 | final theme = Theme.of(context); 32 | return DefaultIndicatorColors( 33 | active: theme.primaryColor, 34 | inactive: theme.unselectedWidgetColor.withValues(alpha: .1), 35 | ); 36 | } 37 | 38 | /// Default indicator colors 39 | static const defaults = DefaultIndicatorColors( 40 | active: Colors.indigo, 41 | inactive: Colors.grey, 42 | ); 43 | 44 | /// Linearly interpolates between two [DefaultIndicatorColors]. 45 | DefaultIndicatorColors lerp(DefaultIndicatorColors? other, double t) { 46 | if (other == null) return this; 47 | return DefaultIndicatorColors( 48 | active: Color.lerp(active, other.active, t)!, 49 | inactive: Color.lerp(inactive, other.inactive, t)!, 50 | ); 51 | } 52 | } 53 | 54 | /// A [ThemeExtension] that provides default configuration for [SmoothPageIndicator] 55 | /// and [AnimatedSmoothIndicator] widgets. 56 | /// 57 | /// Usage: 58 | /// ```dart 59 | /// MaterialApp( 60 | /// theme: ThemeData.light().copyWith( 61 | /// extensions: [ 62 | /// SmoothPageIndicatorTheme( 63 | /// effect: ExpandingDotsEffect(), 64 | /// colors: IndicatorColors( 65 | /// active: Colors.blue, 66 | /// inactive: Colors.grey, 67 | /// ), 68 | /// ), 69 | /// ], 70 | /// ), 71 | /// ) 72 | /// ``` 73 | class SmoothPageIndicatorTheme 74 | extends ThemeExtension { 75 | /// The default effect to use when none is specified. 76 | /// If null, [WormEffect] will be used as the fallback. 77 | final IndicatorEffect? effect; 78 | 79 | /// Theme colors for the indicator. 80 | /// If null, colors will be derived from the app theme. 81 | final DefaultIndicatorColors? defaultColors; 82 | 83 | /// Creates a [SmoothPageIndicatorTheme] instance 84 | const SmoothPageIndicatorTheme({ 85 | this.effect, 86 | this.defaultColors, 87 | }); 88 | 89 | @override 90 | SmoothPageIndicatorTheme copyWith({ 91 | IndicatorEffect? effect, 92 | DefaultIndicatorColors? colors, 93 | }) { 94 | return SmoothPageIndicatorTheme( 95 | effect: effect ?? this.effect, 96 | defaultColors: colors ?? defaultColors, 97 | ); 98 | } 99 | 100 | @override 101 | SmoothPageIndicatorTheme lerp( 102 | covariant ThemeExtension? other, 103 | double t, 104 | ) { 105 | if (other is! SmoothPageIndicatorTheme) { 106 | return this; 107 | } 108 | 109 | // Lerp effects if both are non-null and same type 110 | IndicatorEffect? lerpedEffect; 111 | if (effect != null && 112 | other.effect != null && 113 | effect.runtimeType == other.effect.runtimeType) { 114 | lerpedEffect = effect!.lerp(other.effect, t); 115 | } else { 116 | lerpedEffect = t < 0.5 ? effect : other.effect; 117 | } 118 | 119 | // Lerp colors if both are non-null 120 | final lerpedColors = defaultColors?.lerp(other.defaultColors, t) ?? 121 | (t < 0.5 ? null : other.defaultColors); 122 | 123 | return SmoothPageIndicatorTheme( 124 | effect: lerpedEffect, 125 | defaultColors: lerpedColors, 126 | ); 127 | } 128 | 129 | /// Retrieves the [SmoothPageIndicatorTheme] from the given [BuildContext]. 130 | /// Returns null if no theme extension is found. 131 | static SmoothPageIndicatorTheme? of(BuildContext context) { 132 | return Theme.of(context).extension(); 133 | } 134 | 135 | /// Resolves the default [IndicatorEffect] and [DefaultIndicatorColors] 136 | /// from the theme or provides fallbacks. 137 | /// If no effect is specified in the theme, defaults to [WormEffect]. 138 | /// If no colors are specified, derives them from the app theme. 139 | /// Returns a record of (effect, colors). 140 | static (IndicatorEffect effect, DefaultIndicatorColors colors) 141 | resolveDefaults( 142 | BuildContext context, 143 | ) { 144 | final theme = SmoothPageIndicatorTheme.of(context); 145 | final effect = theme?.effect ?? const WormEffect(); 146 | final colors = 147 | theme?.defaultColors ?? DefaultIndicatorColors.fromContext(context); 148 | return (effect, colors); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /test/src/effects/worm_effect_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 4 | 5 | void main() { 6 | group('WormEffect', () { 7 | test('calculateSize returns correct size', () { 8 | const effect = WormEffect( 9 | dotWidth: 16, 10 | dotHeight: 16, 11 | spacing: 8, 12 | ); 13 | 14 | final size = effect.calculateSize(5); 15 | expect(size.width, 16 * 5 + 8 * 4); 16 | expect(size.height, 16); 17 | }); 18 | 19 | test('buildPainter returns IndicatorPainter', () { 20 | const effect = WormEffect(); 21 | final painter = 22 | effect.buildPainter(5, 0, DefaultIndicatorColors.defaults); 23 | 24 | expect(painter, isA()); 25 | }); 26 | 27 | test('hitTestDots returns correct index', () { 28 | const effect = WormEffect( 29 | dotWidth: 16, 30 | spacing: 8, 31 | ); 32 | 33 | expect(effect.hitTestDots(8, 5, 0), 0); 34 | expect(effect.hitTestDots(30, 5, 0), 1); 35 | expect(effect.hitTestDots(60, 5, 0), 2); 36 | }); 37 | 38 | test('WormType.thin works correctly', () { 39 | const effect = WormEffect(type: WormType.thin); 40 | expect(effect.type, WormType.thin); 41 | }); 42 | 43 | test('WormType.underground works correctly', () { 44 | const effect = WormEffect(type: WormType.underground); 45 | expect(effect.type, WormType.underground); 46 | }); 47 | 48 | test('WormType.thinUnderground works correctly', () { 49 | const effect = WormEffect(type: WormType.thinUnderground); 50 | expect(effect.type, WormType.thinUnderground); 51 | }); 52 | 53 | test('custom colors', () { 54 | const effect = WormEffect( 55 | dotColor: Colors.red, 56 | activeDotColor: Colors.blue, 57 | ); 58 | 59 | expect(effect.dotColor, Colors.red); 60 | expect(effect.activeDotColor, Colors.blue); 61 | }); 62 | 63 | test('custom dimensions', () { 64 | const effect = WormEffect( 65 | dotWidth: 20, 66 | dotHeight: 10, 67 | spacing: 15, 68 | radius: 5, 69 | ); 70 | 71 | expect(effect.dotWidth, 20); 72 | expect(effect.dotHeight, 10); 73 | expect(effect.spacing, 15); 74 | expect(effect.radius, 5); 75 | }); 76 | 77 | testWidgets('paints correctly', (tester) async { 78 | const effect = WormEffect(); 79 | 80 | await tester.pumpWidget( 81 | MaterialApp( 82 | home: Scaffold( 83 | body: CustomPaint( 84 | size: effect.calculateSize(5), 85 | painter: 86 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 87 | ), 88 | ), 89 | ), 90 | ); 91 | 92 | expect(find.byType(CustomPaint), findsWidgets); 93 | }); 94 | 95 | testWidgets('handles portal travel (offset > count - 1)', (tester) async { 96 | const effect = WormEffect(); 97 | 98 | await tester.pumpWidget( 99 | MaterialApp( 100 | home: Scaffold( 101 | body: CustomPaint( 102 | size: effect.calculateSize(5), 103 | painter: 104 | effect.buildPainter(5, 4.5, DefaultIndicatorColors.defaults), 105 | ), 106 | ), 107 | ), 108 | ); 109 | 110 | expect(find.byType(CustomPaint), findsWidgets); 111 | }); 112 | 113 | testWidgets('thin type paints correctly', (tester) async { 114 | const effect = WormEffect(type: WormType.thin); 115 | 116 | await tester.pumpWidget( 117 | MaterialApp( 118 | home: Scaffold( 119 | body: CustomPaint( 120 | size: effect.calculateSize(5), 121 | painter: 122 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 123 | ), 124 | ), 125 | ), 126 | ); 127 | 128 | expect(find.byType(CustomPaint), findsWidgets); 129 | }); 130 | 131 | testWidgets('underground type paints correctly', (tester) async { 132 | const effect = WormEffect(type: WormType.underground); 133 | 134 | await tester.pumpWidget( 135 | MaterialApp( 136 | home: Scaffold( 137 | body: CustomPaint( 138 | size: effect.calculateSize(5), 139 | painter: 140 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 141 | ), 142 | ), 143 | ), 144 | ); 145 | 146 | expect(find.byType(CustomPaint), findsWidgets); 147 | }); 148 | 149 | testWidgets('thinUnderground type paints correctly', (tester) async { 150 | const effect = WormEffect(type: WormType.thinUnderground); 151 | 152 | await tester.pumpWidget( 153 | MaterialApp( 154 | home: Scaffold( 155 | body: CustomPaint( 156 | size: effect.calculateSize(5), 157 | painter: 158 | effect.buildPainter(5, 1.5, DefaultIndicatorColors.defaults), 159 | ), 160 | ), 161 | ), 162 | ); 163 | 164 | expect(find.byType(CustomPaint), findsWidgets); 165 | }); 166 | 167 | testWidgets('stroke painting style renders correctly', (tester) async { 168 | await tester.pumpWidget( 169 | const MaterialApp( 170 | home: Scaffold( 171 | body: SmoothIndicator( 172 | offset: 0, 173 | count: 5, 174 | size: Size(120, 16), 175 | effect: WormEffect( 176 | paintStyle: PaintingStyle.stroke, 177 | strokeWidth: 2.0, 178 | ), 179 | ), 180 | ), 181 | ), 182 | ); 183 | 184 | expect(find.byType(SmoothIndicator), findsOneWidget); 185 | }); 186 | }); 187 | } 188 | -------------------------------------------------------------------------------- /lib/src/painters/customizable_painter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:smooth_page_indicator/src/effects/customizable_effect.dart'; 5 | 6 | import 'indicator_painter.dart'; 7 | 8 | /// Paints user-customizable transition effect between active 9 | /// and non-active dots 10 | /// 11 | /// Live demos at 12 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/custimizable-1.gif 13 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/customizable-2.gif 14 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/customizable-3.gif 15 | /// https://github.com/Milad-Akarie/smooth_page_indicator/blob/f7ee92e7413a31de77bfb487755d64a385d52a52/demo/customizable-4.gif 16 | class CustomizablePainter extends IndicatorPainter { 17 | /// The painting configuration 18 | final CustomizableEffect effect; 19 | 20 | /// The number of pages 21 | final int count; 22 | 23 | /// Default constructor 24 | CustomizablePainter({ 25 | required double offset, 26 | required this.effect, 27 | required this.count, 28 | }) : super(offset); 29 | 30 | @override 31 | void paint(Canvas canvas, Size size) { 32 | var activeDotDecoration = effect.activeDotDecoration; 33 | var dotDecoration = effect.dotDecoration; 34 | final current = offset.floor(); 35 | 36 | final dotOffset = offset - current; 37 | final maxVerticalOffset = max( 38 | activeDotDecoration.verticalOffset, 39 | dotDecoration.verticalOffset, 40 | ); 41 | 42 | var yTranslation = 0.0; 43 | if (activeDotDecoration.verticalOffset >= dotDecoration.verticalOffset) { 44 | yTranslation = 45 | activeDotDecoration.verticalOffset - dotDecoration.verticalOffset; 46 | } else { 47 | yTranslation = 48 | dotDecoration.verticalOffset - activeDotDecoration.verticalOffset; 49 | } 50 | canvas.translate(0, -maxVerticalOffset + yTranslation / 2); 51 | 52 | var drawingOffset = effect.spacing / 2; 53 | 54 | for (var i = 0; i < count; i++) { 55 | if (effect.inActiveColorOverride != null) { 56 | dotDecoration = dotDecoration.copyWith( 57 | color: effect.inActiveColorOverride!.call(i)); 58 | } 59 | if (effect.activeColorOverride != null) { 60 | activeDotDecoration = activeDotDecoration.copyWith( 61 | color: effect.activeColorOverride!.call(i)); 62 | } 63 | var decoration = dotDecoration; 64 | if (i == current) { 65 | decoration = 66 | DotDecoration.lerp(activeDotDecoration, dotDecoration, dotOffset); 67 | } else if (i - 1 == current || (i == 0 && offset > count - 1)) { 68 | decoration = 69 | DotDecoration.lerp(dotDecoration, activeDotDecoration, dotOffset); 70 | } 71 | 72 | final xPos = drawingOffset + decoration.dotBorder.neededSpace / 2; 73 | final yPos = (size.height / 2) + decoration.verticalOffset; 74 | 75 | final rRect = RRect.fromLTRBAndCorners( 76 | xPos, 77 | yPos - decoration.height / 2, 78 | xPos + decoration.width, 79 | yPos + decoration.height / 2, 80 | topLeft: decoration.borderRadius.topLeft, 81 | topRight: decoration.borderRadius.topRight, 82 | bottomLeft: decoration.borderRadius.bottomLeft, 83 | bottomRight: decoration.borderRadius.bottomRight, 84 | ); 85 | 86 | var scaledRect = rRect.outerRect.inflate(decoration.dotBorder.padding); 87 | final scaleRatioX = scaledRect.width / rRect.width; 88 | final scaleRatioY = scaledRect.height / rRect.height; 89 | 90 | final scaledRRect = RRect.fromRectAndCorners( 91 | scaledRect, 92 | topLeft: Radius.elliptical( 93 | rRect.tlRadiusX * scaleRatioX, rRect.tlRadiusY * scaleRatioY), 94 | topRight: Radius.elliptical( 95 | rRect.trRadiusX * scaleRatioX, rRect.trRadiusY * scaleRatioY), 96 | bottomRight: Radius.elliptical( 97 | rRect.brRadiusX * scaleRatioX, rRect.brRadiusY * scaleRatioY), 98 | bottomLeft: Radius.elliptical( 99 | rRect.blRadiusX * scaleRatioX, rRect.blRadiusY * scaleRatioY), 100 | ); 101 | 102 | drawingOffset = scaledRRect.right + effect.spacing; 103 | 104 | var path = Path()..addRRect(rRect); 105 | 106 | final matrix4 = Matrix4.identity(); 107 | if (decoration.rotationAngle != 0) { 108 | matrix4.rotateAngle( 109 | decoration.rotationAngle, 110 | origin: Offset(rRect.right - (rRect.width / 2), yPos), 111 | ); 112 | } 113 | 114 | canvas.drawPath( 115 | path.transform(matrix4.storage), 116 | Paint()..color = decoration.color, 117 | ); 118 | 119 | final borderPaint = Paint() 120 | ..strokeWidth = decoration.dotBorder.width 121 | ..style = PaintingStyle.stroke 122 | ..color = decoration.dotBorder.color; 123 | 124 | final borderPath = Path()..addRRect(scaledRRect); 125 | 126 | canvas.drawPath( 127 | borderPath.transform(matrix4.storage), 128 | borderPaint, 129 | ); 130 | } 131 | } 132 | } 133 | 134 | /// Adds [rotateAngle] functionality to [Matrix4] 135 | extension Matrix4X on Matrix4 { 136 | /// Rotates teh matrix by given [angle] 137 | Matrix4 rotateAngle(double angle, {Offset? origin}) { 138 | final angleRadians = angle * pi / 180; 139 | 140 | if (angleRadians == 0.0) { 141 | return this; 142 | } else if ((origin == null) || (origin.dx == 0.0 && origin.dy == 0.0)) { 143 | return this..rotateZ(angleRadians); 144 | } else { 145 | return this 146 | ..translateByDouble(origin.dx, origin.dy, 0, 1.0) 147 | ..multiply(Matrix4.rotationZ(angleRadians)) 148 | ..translateByDouble(-origin.dx, -origin.dy, 0, 1.0); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 3 | 4 | // ignore_for_file: public_member_api_docs 5 | void main() { 6 | runApp(MyApp()); 7 | } 8 | 9 | class MyApp extends StatelessWidget { 10 | const MyApp({Key? key}) : super(key: key); 11 | 12 | // This widget is the root of your application. 13 | @override 14 | Widget build(BuildContext context) { 15 | return MaterialApp( 16 | title: 'Smooth Page Indicator Demo', 17 | theme: ThemeData.from( 18 | colorScheme: ColorScheme.light(primary: Colors.indigo)), 19 | home: HomePage(), 20 | ); 21 | } 22 | } 23 | 24 | class HomePage extends StatefulWidget { 25 | const HomePage({Key? key}) : super(key: key); 26 | 27 | @override 28 | HomePageState createState() => HomePageState(); 29 | } 30 | 31 | class HomePageState extends State { 32 | final controller = PageController(viewportFraction: 0.8, keepPage: true); 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | final pages = List.generate( 37 | 6, 38 | (index) => Container( 39 | decoration: BoxDecoration( 40 | borderRadius: BorderRadius.circular(16), 41 | color: Colors.grey.shade300, 42 | ), 43 | margin: EdgeInsets.symmetric(horizontal: 10, vertical: 4), 44 | child: SizedBox( 45 | height: 280, 46 | child: Center( 47 | child: Text( 48 | "Page $index", 49 | style: TextStyle(color: Colors.indigo), 50 | )), 51 | ), 52 | ), 53 | ); 54 | 55 | return Scaffold( 56 | body: SafeArea( 57 | child: SingleChildScrollView( 58 | child: Column( 59 | crossAxisAlignment: CrossAxisAlignment.center, 60 | children: [ 61 | SizedBox(height: 16), 62 | SizedBox( 63 | height: 240, 64 | child: PageView.builder( 65 | controller: controller, 66 | // itemCount: pages.length, 67 | itemBuilder: (_, index) { 68 | return pages[index % pages.length]; 69 | }, 70 | ), 71 | ), 72 | Padding( 73 | padding: const EdgeInsets.only(top: 24, bottom: 12), 74 | child: Text( 75 | 'Worm', 76 | style: TextStyle(color: Colors.black54), 77 | ), 78 | ), 79 | SmoothPageIndicator( 80 | controller: controller, 81 | count: pages.length, 82 | // effect: const WormEffect( 83 | // dotHeight: 16, 84 | // dotWidth: 16, 85 | // type: WormType.thinUnderground, 86 | // ), 87 | ), 88 | Padding( 89 | padding: const EdgeInsets.only(top: 16, bottom: 8), 90 | child: Text( 91 | 'Jumping Dot', 92 | style: TextStyle(color: Colors.black54), 93 | ), 94 | ), 95 | SmoothPageIndicator( 96 | controller: controller, 97 | count: pages.length, 98 | effect: JumpingDotEffect( 99 | dotHeight: 16, 100 | dotWidth: 16, 101 | jumpScale: .7, 102 | verticalOffset: 15, 103 | ), 104 | ), 105 | Padding( 106 | padding: const EdgeInsets.only(top: 16, bottom: 12), 107 | child: Text( 108 | 'Scrolling Dots', 109 | style: TextStyle(color: Colors.black54), 110 | ), 111 | ), 112 | SmoothPageIndicator( 113 | controller: controller, 114 | count: pages.length, 115 | effect: ScrollingDotsEffect( 116 | activeStrokeWidth: 2.6, 117 | activeDotScale: 1.3, 118 | maxVisibleDots: 5, 119 | radius: 8, 120 | spacing: 10, 121 | dotHeight: 12, 122 | dotWidth: 12, 123 | )), 124 | Padding( 125 | padding: const EdgeInsets.only(top: 16, bottom: 16), 126 | child: Text( 127 | 'Customizable Effect', 128 | style: TextStyle(color: Colors.black54), 129 | ), 130 | ), 131 | SmoothPageIndicator( 132 | controller: controller, 133 | count: pages.length, 134 | effect: CustomizableEffect( 135 | activeDotDecoration: DotDecoration( 136 | width: 32, 137 | height: 12, 138 | color: Colors.indigo, 139 | rotationAngle: 180, 140 | verticalOffset: -10, 141 | borderRadius: BorderRadius.circular(24), 142 | // dotBorder: DotBorder( 143 | // padding: 2, 144 | // width: 2, 145 | // color: Colors.indigo, 146 | // ), 147 | ), 148 | dotDecoration: DotDecoration( 149 | width: 24, 150 | height: 12, 151 | color: Colors.grey, 152 | // dotBorder: DotBorder( 153 | // padding: 2, 154 | // width: 2, 155 | // color: Colors.grey, 156 | // ), 157 | // borderRadius: BorderRadius.only( 158 | // topLeft: Radius.circular(2), 159 | // topRight: Radius.circular(16), 160 | // bottomLeft: Radius.circular(16), 161 | // bottomRight: Radius.circular(2)), 162 | borderRadius: BorderRadius.circular(16), 163 | verticalOffset: 0, 164 | ), 165 | spacing: 6.0, 166 | // activeColorOverride: (i) => colors[i], 167 | inActiveColorOverride: (i) => colors[i], 168 | ), 169 | ), 170 | const SizedBox(height: 32.0), 171 | ], 172 | ), 173 | ), 174 | ), 175 | ); 176 | } 177 | } 178 | 179 | final colors = const [ 180 | Colors.red, 181 | Colors.green, 182 | Colors.greenAccent, 183 | Colors.amberAccent, 184 | Colors.blue, 185 | Colors.amber, 186 | ]; 187 | -------------------------------------------------------------------------------- /test/src/painters/indicator_painter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:smooth_page_indicator/smooth_page_indicator.dart'; 4 | import 'package:smooth_page_indicator/src/painters/worm_painter.dart'; 5 | 6 | void main() { 7 | group('BasicIndicatorPainter', () { 8 | test('distance calculation is correct', () { 9 | const effect = WormEffect( 10 | dotWidth: 16, 11 | spacing: 8, 12 | ); 13 | final painter = WormPainter( 14 | effect: effect, 15 | count: 5, 16 | offset: 0.0, 17 | indicatorColors: DefaultIndicatorColors.defaults, 18 | ); 19 | 20 | // distance = dotWidth + spacing = 16 + 8 = 24 21 | expect(painter.distance, 24.0); 22 | }); 23 | 24 | test('dotRadius is set correctly', () { 25 | const effect = WormEffect(radius: 8); 26 | final painter = WormPainter( 27 | effect: effect, 28 | count: 5, 29 | offset: 0.0, 30 | indicatorColors: DefaultIndicatorColors.defaults, 31 | ); 32 | 33 | expect(painter.dotRadius, const Radius.circular(8)); 34 | }); 35 | 36 | test('dotPaint is configured correctly with fill style', () { 37 | const effect = WormEffect( 38 | dotColor: Colors.red, 39 | paintStyle: PaintingStyle.fill, 40 | strokeWidth: 2.0, 41 | ); 42 | final painter = WormPainter( 43 | effect: effect, 44 | count: 5, 45 | offset: 0.0, 46 | indicatorColors: DefaultIndicatorColors.defaults, 47 | ); 48 | 49 | expect(painter.dotPaint.color.toARGB32(), Colors.red.toARGB32()); 50 | expect(painter.dotPaint.style, PaintingStyle.fill); 51 | expect(painter.dotPaint.strokeWidth, 2.0); 52 | }); 53 | 54 | test('dotPaint is configured correctly with stroke style', () { 55 | const effect = WormEffect( 56 | dotColor: Colors.blue, 57 | paintStyle: PaintingStyle.stroke, 58 | strokeWidth: 3.0, 59 | ); 60 | final painter = WormPainter( 61 | effect: effect, 62 | count: 5, 63 | offset: 0.0, 64 | indicatorColors: DefaultIndicatorColors.defaults, 65 | ); 66 | 67 | expect(painter.dotPaint.color.toARGB32(), Colors.blue.toARGB32()); 68 | expect(painter.dotPaint.style, PaintingStyle.stroke); 69 | expect(painter.dotPaint.strokeWidth, 3.0); 70 | }); 71 | 72 | testWidgets('paintStillDots renders correct number of dots', 73 | (tester) async { 74 | const effect = WormEffect(); 75 | 76 | await tester.pumpWidget( 77 | MaterialApp( 78 | home: Scaffold( 79 | body: CustomPaint( 80 | size: effect.calculateSize(5), 81 | painter: WormPainter( 82 | effect: effect, 83 | count: 5, 84 | offset: 0.0, 85 | indicatorColors: DefaultIndicatorColors.defaults, 86 | ), 87 | ), 88 | ), 89 | ), 90 | ); 91 | 92 | expect(find.byType(CustomPaint), findsWidgets); 93 | }); 94 | 95 | testWidgets('buildStillDot creates correct RRect', (tester) async { 96 | const effect = WormEffect( 97 | dotWidth: 16, 98 | dotHeight: 16, 99 | spacing: 8, 100 | radius: 8, 101 | ); 102 | 103 | await tester.pumpWidget( 104 | MaterialApp( 105 | home: Scaffold( 106 | body: CustomPaint( 107 | size: effect.calculateSize(5), 108 | painter: WormPainter( 109 | effect: effect, 110 | count: 5, 111 | offset: 0.0, 112 | indicatorColors: DefaultIndicatorColors.defaults, 113 | ), 114 | ), 115 | ), 116 | ), 117 | ); 118 | 119 | expect(find.byType(CustomPaint), findsWidgets); 120 | }); 121 | 122 | testWidgets('maskStillDots works with underground effects', (tester) async { 123 | const effect = WormEffect(type: WormType.underground); 124 | 125 | await tester.pumpWidget( 126 | MaterialApp( 127 | home: Scaffold( 128 | body: CustomPaint( 129 | size: effect.calculateSize(5), 130 | painter: WormPainter( 131 | effect: effect, 132 | count: 5, 133 | offset: 1.5, 134 | indicatorColors: DefaultIndicatorColors.defaults, 135 | ), 136 | ), 137 | ), 138 | ), 139 | ); 140 | 141 | expect(find.byType(CustomPaint), findsWidgets); 142 | }); 143 | 144 | testWidgets('calcPortalTravel renders portal travel animation', 145 | (tester) async { 146 | const effect = WormEffect(); 147 | 148 | await tester.pumpWidget( 149 | MaterialApp( 150 | home: Scaffold( 151 | body: CustomPaint( 152 | size: effect.calculateSize(5), 153 | painter: WormPainter( 154 | effect: effect, 155 | count: 5, 156 | offset: 4.5, // Triggers portal travel 157 | indicatorColors: DefaultIndicatorColors.defaults, 158 | ), 159 | ), 160 | ), 161 | ), 162 | ); 163 | 164 | expect(find.byType(CustomPaint), findsWidgets); 165 | }); 166 | }); 167 | 168 | group('IndicatorPainter', () { 169 | test('shouldRepaint returns true when offset changes', () { 170 | const effect = WormEffect(); 171 | final painter1 = WormPainter( 172 | effect: effect, 173 | count: 5, 174 | offset: 0.0, 175 | indicatorColors: DefaultIndicatorColors.defaults); 176 | final painter2 = WormPainter( 177 | effect: effect, 178 | count: 5, 179 | offset: 1.0, 180 | indicatorColors: DefaultIndicatorColors.defaults); 181 | 182 | expect(painter1.shouldRepaint(painter2), isTrue); 183 | }); 184 | 185 | test('shouldRepaint returns false when offset is same', () { 186 | const effect = WormEffect(); 187 | final painter1 = WormPainter( 188 | effect: effect, 189 | count: 5, 190 | offset: 0.0, 191 | indicatorColors: DefaultIndicatorColors.defaults); 192 | final painter2 = WormPainter( 193 | effect: effect, 194 | count: 5, 195 | offset: 0.0, 196 | indicatorColors: DefaultIndicatorColors.defaults); 197 | 198 | expect(painter1.shouldRepaint(painter2), isFalse); 199 | }); 200 | 201 | test('offset is stored correctly', () { 202 | const effect = WormEffect(); 203 | final painter = WormPainter( 204 | effect: effect, 205 | count: 5, 206 | offset: 2.5, 207 | indicatorColors: DefaultIndicatorColors.defaults); 208 | 209 | expect(painter.offset, 2.5); 210 | }); 211 | }); 212 | } 213 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | alchemist: 5 | dependency: "direct dev" 6 | description: 7 | name: alchemist 8 | sha256: "73e6ea7108897b51af27cf2e27f1a7281d0c7a296c31025ae0c367f09c7236c6" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "0.13.0" 12 | async: 13 | dependency: transitive 14 | description: 15 | name: async 16 | sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.12.0" 20 | boolean_selector: 21 | dependency: transitive 22 | description: 23 | name: boolean_selector 24 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.1.2" 28 | characters: 29 | dependency: transitive 30 | description: 31 | name: characters 32 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.4.0" 36 | clock: 37 | dependency: transitive 38 | description: 39 | name: clock 40 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.1.2" 44 | collection: 45 | dependency: transitive 46 | description: 47 | name: collection 48 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.19.1" 52 | equatable: 53 | dependency: transitive 54 | description: 55 | name: equatable 56 | sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "2.0.7" 60 | fake_async: 61 | dependency: transitive 62 | description: 63 | name: fake_async 64 | sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.3.3" 68 | flutter: 69 | dependency: "direct main" 70 | description: flutter 71 | source: sdk 72 | version: "0.0.0" 73 | flutter_lints: 74 | dependency: "direct dev" 75 | description: 76 | name: flutter_lints 77 | sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" 78 | url: "https://pub.dev" 79 | source: hosted 80 | version: "6.0.0" 81 | flutter_test: 82 | dependency: "direct dev" 83 | description: flutter 84 | source: sdk 85 | version: "0.0.0" 86 | leak_tracker: 87 | dependency: transitive 88 | description: 89 | name: leak_tracker 90 | sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "11.0.2" 94 | leak_tracker_flutter_testing: 95 | dependency: transitive 96 | description: 97 | name: leak_tracker_flutter_testing 98 | sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "3.0.10" 102 | leak_tracker_testing: 103 | dependency: transitive 104 | description: 105 | name: leak_tracker_testing 106 | sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" 107 | url: "https://pub.dev" 108 | source: hosted 109 | version: "3.0.2" 110 | lints: 111 | dependency: transitive 112 | description: 113 | name: lints 114 | sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "6.0.0" 118 | matcher: 119 | dependency: transitive 120 | description: 121 | name: matcher 122 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 123 | url: "https://pub.dev" 124 | source: hosted 125 | version: "0.12.17" 126 | material_color_utilities: 127 | dependency: transitive 128 | description: 129 | name: material_color_utilities 130 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 131 | url: "https://pub.dev" 132 | source: hosted 133 | version: "0.11.1" 134 | meta: 135 | dependency: transitive 136 | description: 137 | name: meta 138 | sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" 139 | url: "https://pub.dev" 140 | source: hosted 141 | version: "1.17.0" 142 | path: 143 | dependency: transitive 144 | description: 145 | name: path 146 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 147 | url: "https://pub.dev" 148 | source: hosted 149 | version: "1.9.1" 150 | sky_engine: 151 | dependency: transitive 152 | description: flutter 153 | source: sdk 154 | version: "0.0.0" 155 | source_span: 156 | dependency: transitive 157 | description: 158 | name: source_span 159 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "1.10.1" 163 | stack_trace: 164 | dependency: transitive 165 | description: 166 | name: stack_trace 167 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "1.12.1" 171 | stream_channel: 172 | dependency: transitive 173 | description: 174 | name: stream_channel 175 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "2.1.4" 179 | string_scanner: 180 | dependency: transitive 181 | description: 182 | name: string_scanner 183 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "1.4.1" 187 | term_glyph: 188 | dependency: transitive 189 | description: 190 | name: term_glyph 191 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 192 | url: "https://pub.dev" 193 | source: hosted 194 | version: "1.2.2" 195 | test_api: 196 | dependency: transitive 197 | description: 198 | name: test_api 199 | sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 200 | url: "https://pub.dev" 201 | source: hosted 202 | version: "0.7.7" 203 | vector_math: 204 | dependency: transitive 205 | description: 206 | name: vector_math 207 | sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b 208 | url: "https://pub.dev" 209 | source: hosted 210 | version: "2.2.0" 211 | vm_service: 212 | dependency: transitive 213 | description: 214 | name: vm_service 215 | sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" 216 | url: "https://pub.dev" 217 | source: hosted 218 | version: "14.3.1" 219 | sdks: 220 | dart: ">=3.8.0 <4.0.0" 221 | flutter: ">=3.32.0" 222 | -------------------------------------------------------------------------------- /lib/src/effects/customizable_effect.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:ui' as ui show lerpDouble; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:smooth_page_indicator/src/painters/customizable_painter.dart'; 6 | import 'package:smooth_page_indicator/src/painters/indicator_painter.dart'; 7 | import 'package:smooth_page_indicator/src/theme_defaults.dart'; 8 | 9 | import 'indicator_effect.dart'; 10 | 11 | /// Signature for a function that returns color 12 | /// for each [index] 13 | typedef ColorBuilder = Color Function(int index); 14 | 15 | /// Holds painting configuration to be used by [CustomizablePainter] 16 | class CustomizableEffect extends IndicatorEffect { 17 | /// Holds painting decoration for inactive dots 18 | final DotDecoration dotDecoration; 19 | 20 | /// Holds painting decoration for active dots 21 | final DotDecoration activeDotDecoration; 22 | 23 | /// Builds dynamic colors for active dot 24 | final ColorBuilder? activeColorOverride; 25 | 26 | /// Builds dynamic colors for inactive dots 27 | final ColorBuilder? inActiveColorOverride; 28 | 29 | /// The space between two dots 30 | final double spacing; 31 | 32 | /// Default constructor 33 | const CustomizableEffect({ 34 | required this.dotDecoration, 35 | required this.activeDotDecoration, 36 | this.activeColorOverride, 37 | this.spacing = 8, 38 | this.inActiveColorOverride, 39 | }); 40 | 41 | @override 42 | Size calculateSize(int count) { 43 | final activeDotWidth = 44 | activeDotDecoration.width + activeDotDecoration.dotBorder.neededSpace; 45 | final dotWidth = dotDecoration.width + dotDecoration.dotBorder.neededSpace; 46 | 47 | final maxWidth = 48 | dotWidth * (count - 1) + (spacing * count) + activeDotWidth; 49 | 50 | final offsetSpace = 51 | (dotDecoration.verticalOffset - activeDotDecoration.verticalOffset) 52 | .abs(); 53 | final maxHeight = max( 54 | dotDecoration.height + offsetSpace + dotDecoration.dotBorder.neededSpace, 55 | activeDotDecoration.height + 56 | offsetSpace + 57 | activeDotDecoration.dotBorder.neededSpace, 58 | ); 59 | return Size(maxWidth, maxHeight); 60 | } 61 | 62 | @override 63 | IndicatorPainter buildPainter( 64 | int count, double offset, DefaultIndicatorColors indicatorColors) { 65 | return CustomizablePainter(count: count, offset: offset, effect: this); 66 | } 67 | 68 | @override 69 | int hitTestDots(double dx, int count, double current) { 70 | var anchor = -spacing / 2; 71 | for (var index = 0; index < count; index++) { 72 | var dotWidth = dotDecoration.width + dotDecoration.dotBorder.neededSpace; 73 | if (index == current) { 74 | dotWidth = activeDotDecoration.width + 75 | activeDotDecoration.dotBorder.neededSpace; 76 | } 77 | 78 | var widthBound = dotWidth + spacing; 79 | if (dx <= (anchor += widthBound)) { 80 | return index; 81 | } 82 | } 83 | return -1; 84 | } 85 | 86 | @override 87 | CustomizableEffect lerp(covariant CustomizableEffect? other, double t) { 88 | if (other == null) return this; 89 | return CustomizableEffect( 90 | dotDecoration: DotDecoration.lerp(dotDecoration, other.dotDecoration, t), 91 | activeDotDecoration: 92 | DotDecoration.lerp(activeDotDecoration, other.activeDotDecoration, t), 93 | activeColorOverride: 94 | t < 0.5 ? activeColorOverride : other.activeColorOverride, 95 | inActiveColorOverride: 96 | t < 0.5 ? inActiveColorOverride : other.inActiveColorOverride, 97 | spacing: ui.lerpDouble(spacing, other.spacing, t)!, 98 | ); 99 | } 100 | } 101 | 102 | /// Holds dot painting specs 103 | class DotDecoration { 104 | /// The border radius of the dot 105 | final BorderRadius borderRadius; 106 | 107 | /// The color of the dot 108 | final Color color; 109 | 110 | /// The dotBorder configuration of the dot 111 | final DotBorder dotBorder; 112 | 113 | /// The vertical offset of the dot 114 | final double verticalOffset; 115 | 116 | /// The rotation angle of the dot 117 | final double rotationAngle; 118 | 119 | /// The width of the dot 120 | final double width; 121 | 122 | /// the height of the dot 123 | final double height; 124 | 125 | /// Default constructor 126 | const DotDecoration( 127 | {this.borderRadius = BorderRadius.zero, 128 | this.color = Colors.white, 129 | this.dotBorder = DotBorder.none, 130 | this.verticalOffset = 0.0, 131 | this.rotationAngle = 0.0, 132 | this.width = 8, 133 | this.height = 8}); 134 | 135 | /// Lerps the value between active dot and prev-active dot 136 | static DotDecoration lerp(DotDecoration a, DotDecoration b, double t) { 137 | return DotDecoration( 138 | borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t)!, 139 | width: ui.lerpDouble(a.width, b.width, t) ?? 0.0, 140 | height: ui.lerpDouble(a.height, b.height, t) ?? 0.0, 141 | color: Color.lerp(a.color, b.color, t)!, 142 | dotBorder: DotBorder.lerp(a.dotBorder, b.dotBorder, t), 143 | verticalOffset: 144 | ui.lerpDouble(a.verticalOffset, b.verticalOffset, t) ?? 0.0, 145 | rotationAngle: 146 | ui.lerpDouble(a.rotationAngle, b.rotationAngle, t) ?? 0.0); 147 | } 148 | 149 | /// Builds a new instance with the given 150 | /// override values 151 | DotDecoration copyWith({ 152 | BorderRadius? borderRadius, 153 | double? width, 154 | double? height, 155 | Color? color, 156 | DotBorder? dotBorder, 157 | double? verticalOffset, 158 | double? rotationAngle, 159 | }) { 160 | return DotDecoration( 161 | borderRadius: borderRadius ?? this.borderRadius, 162 | width: width ?? this.width, 163 | height: height ?? this.height, 164 | color: color ?? this.color, 165 | dotBorder: dotBorder ?? this.dotBorder, 166 | verticalOffset: verticalOffset ?? this.verticalOffset, 167 | rotationAngle: rotationAngle ?? this.rotationAngle, 168 | ); 169 | } 170 | } 171 | 172 | /// The variants of dot borders 173 | enum DotBorderType { 174 | /// Draw a sold border 175 | solid, 176 | 177 | /// Draw nothing 178 | none 179 | } 180 | 181 | /// Holds dot-border painting specs 182 | class DotBorder { 183 | /// The thinness of the border line 184 | final double width; 185 | 186 | /// The color of the border 187 | final Color color; 188 | 189 | /// The padding between the dot and the border 190 | final double padding; 191 | 192 | /// The border variant 193 | final DotBorderType type; 194 | 195 | /// Default constructor 196 | const DotBorder({ 197 | this.width = 1.0, 198 | this.color = Colors.black87, 199 | this.padding = 0.0, 200 | this.type = DotBorderType.solid, 201 | }); 202 | 203 | /// Calculates the needed gap based on [type] 204 | double get neededSpace => 205 | type == DotBorderType.none ? 0.0 : (width / 2 + (padding * 2)); 206 | 207 | /// Builds an instance with type [DotBorderType.none] 208 | static const none = DotBorder._none(); 209 | 210 | const DotBorder._none() 211 | : width = 0.0, 212 | color = Colors.transparent, 213 | padding = 0.0, 214 | type = DotBorderType.none; 215 | 216 | /// Lerps the value between active dot border and prev-active dot's border 217 | static DotBorder lerp(DotBorder a, DotBorder b, double t) { 218 | if (t == 0.0) { 219 | return a; 220 | } 221 | if (t == 1.0) { 222 | return b; 223 | } 224 | return DotBorder( 225 | color: Color.lerp(a.color, b.color, t)!, 226 | width: ui.lerpDouble(a.width, b.width, t)!, 227 | padding: ui.lerpDouble(a.padding, b.padding, t)!); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # smooth_page_indicator 3 |

4 | MIT License 6 | stars 7 | pub version 9 | 10 | 11 | 12 | Buy Me A Coffee 13 |

14 | 15 | ## Introduction 16 | Page indicators are a crucial part of any app that involves multiple pages. They help users to 17 | understand the number of pages and their current position. `SmoothPageIndicator` is a Flutter 18 | package that provides a set of animated page indicators with a variety of effects. 19 | 20 | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/four_squares_demo.gif?raw=true) 21 | 22 | 23 | ## Effects 24 | `SmoothPageIndicator` comes with a set of built-in effects that you can use to animate the active dot, 25 | you can also customize each effect to your liking. 26 | 27 | for more specific customization, try the `CustomizableEffect` which allows for more customization. 28 | 29 | | Effect | Preview | 30 | |:------------------------------------------|:------------------------------------------------------------------------------------------------------------------------:| 31 | | Worm | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/worm.gif?raw=true) | 32 | | Worm style = WormStyle.thin [v1.0.0] | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/worm-thin.gif?raw=true) | 33 | | Expanding Dots | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/expanding-dot.gif?raw=true) | 34 | | Jumping dot | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/jumping-dot.gif?raw=true) | 35 | | Jumping dot with vertical offset [v1.0.0] | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/jumping-dot-effect-with-voffset.gif?raw=true) | 36 | | Scrolling Dots | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/scrolling-dots-2.gif?raw=true) | 37 | | Slide | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/slide.gif?raw=true) | 38 | | Scale | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/scale.gif?raw=true) | 39 | | Swap | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/swap.gif?raw=true) | 40 | | Swap type = SwapType.yRotation [v1.0.0] | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/swap-yrotation.gif?raw=true) | 41 | | Color Transition [0.1.2] | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/color-transition.gif?raw=true) | 42 | | Customizable demo-1 [v1.0.0] | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/custimizable-1.gif?raw=true) | 43 | | Customizable demo-2 [v1.0.0] | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/customizable-2.gif?raw=true) | 44 | | Customizable demo-3 [v1.0.0] | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/customizable-3.gif?raw=true) | 45 | | Customizable demo-4 [v1.0.0] | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/customizable-4.gif?raw=true) | 46 | 47 | ## Usage 48 | `SmoothPageIndicator` uses the PageController's scroll offset to animate the active dot. 49 | 50 | ```dart 51 | SmoothPageIndicator( 52 | controller: controller, // PageController 53 | count: 6, 54 | effect: WormEffect(), // your preferred effect 55 | onDotClicked: (index){ 56 | } 57 | ) 58 | 59 | ``` 60 | ## Usage without a PageController 61 | Unlike `SmoothPageIndicator`, `AnimatedSmoothIndicator` is self animated and all it needs is the 62 | active index. 63 | 64 | ```dart 65 | AnimatedSmoothIndicator( 66 | activeIndex: yourActiveIndex, 67 | count: 6, 68 | effect: WormEffect(), 69 | ) 70 | ``` 71 | ## Vertical layout support 72 | Smooth page indicator supports both horizontal and vertical layouts. 73 | 74 | ```dart 75 | SmoothPageIndicator( 76 | controller: controller, // PageController 77 | count: 6, 78 | axisDirection: Axis.vertical, 79 | effect: WormEffect(), 80 | ) 81 | ``` 82 | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/vertical_demo.gif?raw=true) 83 | 84 | ## Scrolling dots effect 85 | Smooth page indicator comes with a shipped it scrolling dots effect, (similar to the one used in instagram), it's useful when you have a large number of pages. 86 | 87 | ![](https://github.com/Milad-Akarie/smooth_page_indicator/blob/master/demo/smooth_page_indicator_demo_4.gif?raw=true) 88 | 89 | ## Customization 90 | Each effect comes with its own set of properties that you can customize to your liking. 91 | for example, you can customize direction, width, height, radius, spacing, paint style, color and more... of `SlideEffect` like follows: 92 | 93 | ```dart 94 | SmoothPageIndicator( 95 | controller: controller, 96 | count: 6, 97 | axisDirection: Axis.vertical, 98 | effect: SlideEffect( 99 | spacing: 8.0, 100 | radius: 4.0, 101 | dotWidth: 24.0, 102 | dotHeight: 16.0, 103 | paintStyle: PaintingStyle.stroke, 104 | strokeWidth: 1.5, 105 | dotColor: Colors.grey, 106 | activeDotColor: Colors.indigo 107 | ), 108 | ) 109 | 110 | ``` 111 | 112 | ## Theme-based Colors 113 | By default, `SmoothPageIndicator` derives its colors from the app theme: 114 | - `activeDotColor` defaults to `Theme.of(context).primaryColor` 115 | - `dotColor` defaults to `Theme.of(context).unselectedWidgetColor` with reduced opacity 116 | 117 | This means the indicator automatically adapts to your app's color scheme. You can override these by explicitly setting `dotColor` and `activeDotColor` in any effect. 118 | 119 | ## SmoothPageIndicatorTheme 120 | You can configure default settings for all `SmoothPageIndicator` and `AnimatedSmoothIndicator` widgets app-wide using `SmoothPageIndicatorTheme`. 121 | 122 | ```dart 123 | MaterialApp( 124 | theme: ThemeData.light().copyWith( 125 | extensions: [ 126 | SmoothPageIndicatorTheme( 127 | effect: ExpandingDotsEffect(), // default effect when none is specified 128 | defaultColors: DefaultIndicatorColors( 129 | active: Colors.blue, 130 | inactive: Colors.grey, 131 | ), 132 | ), 133 | ], 134 | ), 135 | ) 136 | ``` 137 | 138 | - `defaultColors`: Applies to **all** indicator effects across the app (unless overridden by the effect itself) 139 | - `effect`: The default effect used when no effect is specified in the widget. Colors set within this effect (e.g., `activeDotColor`) only apply to this default effect. 140 | 141 | This is useful when you want consistent indicator styling across your entire app without repeating the same configuration. 142 | 143 | ## Support the Library 144 | 145 | You can support the library by liking it on pub, staring in on Github and reporting any bugs you 146 | encounter. --------------------------------------------------------------------------------