diff --git a/.gitignore b/.gitignore index 3f360a5..87b5ba5 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ migrate_working_dir/ .dart_tool/ .packages build/ +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 64c21a1..8dd8ed6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,3 @@ -## 0.0.1 +## 1.0.0 -* TODO: Describe initial release. +* Initial release. diff --git a/LICENSE b/LICENSE index 97cfb48..03985d0 100644 --- a/LICENSE +++ b/LICENSE @@ -1 +1,11 @@ -TODO: Add your license here. +Copyright 2024 RikitoNoto + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 48cdf78..b59ef0f 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,101 @@ -# flutter_animated_icon_button - -A new Flutter plugin project. - -## Getting Started - -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/developing-packages/), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. - -For help getting started with Flutter development, view the -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. - -The plugin project was generated without specifying the `--platforms` flag, no platforms are currently supported. -To add platforms, run `flutter create -t plugin --platforms .` in this directory. -You can also find a detailed instruction on how to add platforms in the `pubspec.yaml` at https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms. +# flutter_animated_icon_button +An animated icon plugin. +This plugin allows you to create an animated icon button easily. + + + +## Getting Started +To use this plugin, add ```flutter_animated_icon_button``` to a dependency in your ```pubspec.yaml```. +```yaml +flutter_animated_icon_button: ^1.0.0 +``` + +## Example +## TapFillIcon +The code below shows how to create the Good icon button that is often used in SNS apps. +```dart +TapFillIcon( + borderIcon: const Icon( + Icons.favorite_border, + color: Colors.grey, + ), + fillIcon: const Icon( + Icons.favorite, + color: Colors.red, + ), +), +``` + +This code builds the below icon button. + + + + +## TapFillIconWithParticle +The code below shows how to create the Favorite icon button with particles. +```dart +// Please initialize a controller in 'initState' of the class that mixin TickerProviderStateMixin + + late AnimationController controller; + @override + void initState() { + controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + lowerBound: 0.0, + upperBound: 1.0, + ); + controller.addListener(() { + if (mounted) { + setState(() {}); + } + }); + } + + @override + Widget build(BuildContext context) { + return TapParticle( + size: 50, + particleCount: 5, + particleLength: 10, + color: Colors.orange, + syncAnimation: controller, + duration: const Duration(milliseconds: 300), + child: TapFillIcon( + animationController: controller, + borderIcon: const Icon( + Icons.star_border, + color: Colors.grey, + size: 50, + ), + fillIcon: const Icon( + Icons.star, + color: Colors.yellow, + size: 50, + ), + ), + ), + } +``` + +This code builds the below icon button. + + + + +## AnimateChangeIcon +The code below shows how to create the icon button that can change its icon from Play to Stop with animation. +```dart +AnimateChangeIcon( + firstIcon: Icon( + Icons.play_arrow_rounded, + ), + secondIcon: Icon( + Icons.stop_rounded, + ), +), +``` + +This code builds the below icon button. + + diff --git a/all_lint_rules.yaml b/all_lint_rules.yaml new file mode 100644 index 0000000..d9f8acb --- /dev/null +++ b/all_lint_rules.yaml @@ -0,0 +1,218 @@ +linter: + rules: + - always_declare_return_types + - always_put_control_body_on_new_line + - always_put_required_named_parameters_first + - always_specify_types + - always_use_package_imports + - annotate_overrides + - avoid_annotating_with_dynamic + - avoid_bool_literals_in_conditional_expressions + - avoid_catches_without_on_clauses + - avoid_catching_errors + - avoid_classes_with_only_static_members + - avoid_double_and_int_checks + - avoid_dynamic_calls + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes + - avoid_field_initializers_in_const_classes + - avoid_final_parameters + - avoid_function_literals_in_foreach_calls + - avoid_implementing_value_types + - avoid_init_to_null + - avoid_js_rounded_ints + - avoid_multiple_declarations_per_line + - avoid_null_checks_in_equality_operators + - avoid_positional_boolean_parameters + - avoid_print + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_relative_lib_imports + - avoid_renaming_method_parameters + - avoid_return_types_on_setters + - avoid_returning_null_for_void + - avoid_returning_this + - avoid_setters_without_getters + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + - avoid_type_to_string + - avoid_types_as_parameter_names + - avoid_types_on_closure_parameters + - avoid_unnecessary_containers + - avoid_unused_constructor_parameters + - avoid_void_async + - avoid_web_libraries_in_flutter + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + - cascade_invocations + - cast_nullable_to_non_nullable + - close_sinks + - collection_methods_unrelated_type + - combinators_ordering + - comment_references + - conditional_uri_does_not_exist + - constant_identifier_names + - control_flow_in_finally + - curly_braces_in_flow_control_structures + - dangling_library_doc_comments + - depend_on_referenced_packages + - deprecated_consistency + - deprecated_member_use_from_same_package + - diagnostic_describe_all_properties + - directives_ordering + - discarded_futures + - do_not_use_environment + - empty_catches + - empty_constructor_bodies + - empty_statements + - eol_at_end_of_file + - exhaustive_cases + - file_names + - flutter_style_todos + - hash_and_equals + - implementation_imports + - implicit_call_tearoffs + - implicit_reopen + - invalid_case_patterns + - iterable_contains_unrelated_type + - join_return_with_assignment + - leading_newlines_in_multiline_strings + - library_annotations + - library_names + - library_prefixes + - library_private_types_in_public_api + - lines_longer_than_80_chars + - list_remove_unrelated_type + - literal_only_boolean_expressions + - matching_super_parameters + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_default_cases + - no_duplicate_case_values + - no_leading_underscores_for_library_prefixes + - no_leading_underscores_for_local_identifiers + - no_literal_bool_comparisons + - no_logic_in_create_state + - no_runtimeType_toString + - non_constant_identifier_names + - noop_primitive_operations + - null_check_on_nullable_type_parameter + - null_closures + - omit_local_variable_types + - one_member_abstracts + - only_throw_errors + - overridden_fields + - package_api_docs + - package_names + - package_prefixed_library_names + - parameter_assignments + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + - prefer_asserts_with_message + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + - prefer_constructors_over_static_methods + - prefer_contains + - prefer_double_quotes + - prefer_expression_function_bodies + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_final_parameters + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions + - prefer_if_null_operators + - prefer_initializing_formals + - prefer_inlined_adds + - prefer_int_literals + - prefer_interpolation_to_compose_strings + - prefer_is_empty + - prefer_is_not_empty + - prefer_is_not_operator + - prefer_iterable_whereType + - prefer_mixin + - prefer_null_aware_method_calls + - prefer_null_aware_operators + - prefer_relative_imports + - prefer_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + - provide_deprecation_message + - public_member_api_docs + - recursive_getters + - require_trailing_commas + - secure_pubspec_urls + - sized_box_for_whitespace + - sized_box_shrink_expand + - slash_for_doc_comments + - sort_child_properties_last + - sort_constructors_first + - sort_pub_dependencies + - sort_unnamed_constructors_first + - test_types_in_equals + - throw_in_finally + - tighten_type_of_initializing_formals + - type_annotate_public_apis + - type_init_formals + - type_literal_in_constant_pattern + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_brace_in_string_interps + - unnecessary_breaks + - unnecessary_const + - unnecessary_constructor_name + - unnecessary_final + - unnecessary_getters_setters + - unnecessary_lambdas + - unnecessary_late + - unnecessary_library_directive + - unnecessary_new + - unnecessary_null_aware_assignments + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_null_checks + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + - unnecessary_raw_strings + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unnecessary_to_list_in_spreads + - unreachable_from_main + - unrelated_type_equality_checks + - unsafe_html + - use_build_context_synchronously + - use_colored_box + - use_decorated_box + - use_enums + - use_full_hex_values_for_flutter_colors + - use_function_type_syntax_for_parameters + - use_if_null_to_convert_nulls_to_bools + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_named_constants + - use_raw_strings + - use_rethrow_when_possible + - use_setters_to_change_properties + - use_string_buffers + - use_string_in_part_of_directives + - use_super_parameters + - use_test_throws_matchers + - use_to_and_as_if_applicable + - valid_regexps + - void_checks diff --git a/analysis_options.yaml b/analysis_options.yaml index a66c323..f20ad37 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,37 @@ -include: package:flutter_lints/flutter.yaml +include: all_lint_rules.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + errors: + included_file_warning: ignore + invalid_annotation_target: ignore + invalid_use_of_visible_for_testing_member: error + exclude: + - lib/firebase_options.dart + - build/** + - lib/**.freezed.dart + - lib/**.g.dart + +linter: + rules: + do_not_use_environment: false + prefer_final_parameters: false + public_member_api_docs: false + discarded_futures: false + combinators_ordering: false + eol_at_end_of_file: false + no_leading_underscores_for_local_identifiers: false + one_member_abstracts: false + diagnostic_describe_all_properties: false + prefer_double_quotes: false + always_specify_types: false + unnecessary_final: false + prefer_expression_function_bodies: false + always_put_required_named_parameters_first: false + flutter_style_todos: false + avoid_annotating_with_dynamic: false + always_use_package_imports: false + no_default_cases: false diff --git a/doc/assets/animate_icon_favorite.gif b/doc/assets/animate_icon_favorite.gif new file mode 100644 index 0000000..62410f5 Binary files /dev/null and b/doc/assets/animate_icon_favorite.gif differ diff --git a/doc/assets/animate_icon_play_stop.gif b/doc/assets/animate_icon_play_stop.gif new file mode 100644 index 0000000..9cdb66b Binary files /dev/null and b/doc/assets/animate_icon_play_stop.gif differ diff --git a/doc/assets/animate_icon_push.gif b/doc/assets/animate_icon_push.gif new file mode 100644 index 0000000..45e98da Binary files /dev/null and b/doc/assets/animate_icon_push.gif differ diff --git a/doc/assets/animate_icon_star.gif b/doc/assets/animate_icon_star.gif new file mode 100644 index 0000000..60269ea Binary files /dev/null and b/doc/assets/animate_icon_star.gif differ diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..25d90ec --- /dev/null +++ b/example/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 796c8ef79279f9c774545b3771238c3098dbefab + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + - platform: android + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 0000000..5d99765 --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..4968fd2 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,72 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + namespace "com.example.example" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..8ffe024 --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..56b9291 --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt new file mode 100644 index 0000000..1efda7e --- /dev/null +++ b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..1cb7aa2 --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..8403758 --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..360a160 --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..5fac679 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..8ffe024 --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..0bd4dfd --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..46c1f16 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..eabb85f --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..33f0745 --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example/lib/main.dart b/example/lib/main.dart index d2ed6d8..80ace99 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,128 +1,132 @@ -import 'package:flutter/material.dart'; - -class FadeIconApp extends StatelessWidget { - const FadeIconApp({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - home: IconAnimation(), - ); - } -} - -class IconAnimation extends StatefulWidget { - const IconAnimation({super.key}); - - @override - _IconAnimationState createState() => _IconAnimationState(); -} - -class _IconAnimationState extends State { - bool _showFirstIcon = true; - - void _toggleIcon() { - setState(() { - _showFirstIcon = !_showFirstIcon; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Icon Animation')), - body: Center( - child: GestureDetector( - onTap: _toggleIcon, - child: AnimatedSwitcher( - duration: const Duration(seconds: 1), - child: _showFirstIcon - ? const Icon(Icons.star, key: ValueKey(1)) - : const Icon(Icons.favorite, key: ValueKey(2)), - transitionBuilder: (child, animation) { - return ScaleTransition( - scale: animation, - child: RotationTransition( - turns: animation, - child: child, - ), - ); - }, - ), - ), - ), - ); - } -} - -class AnimatedBulbApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - home: AnimatedBulb(), - ); - } -} - -class AnimatedBulb extends StatefulWidget { - @override - _AnimatedBulbState createState() => _AnimatedBulbState(); -} - -class _AnimatedBulbState extends State - with SingleTickerProviderStateMixin { - late AnimationController _controller; - - @override - void initState() { - super.initState(); - _controller = - AnimationController(vsync: this, duration: Duration(seconds: 2)); - _controller.repeat(); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text('Animated Bulb')), - body: Center( - child: CustomPaint( - size: Size(200, 200), - painter: BulbPainter(_controller), - ), - ), - ); - } -} - -class BulbPainter extends CustomPainter { - Animation animation; - - BulbPainter(this.animation) : super(repaint: animation); - - @override - void paint(Canvas canvas, Size size) { - Paint paint = Paint() - ..color = Colors.red - ..strokeWidth = 5; - - double startY = size.height * (1 - animation.value); - double endY = size.height; - - Offset start = Offset(size.width / 2, startY); - Offset end = Offset(size.width / 2, endY); - - canvas.drawLine(start, end, paint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return true; - } -} +import 'package:flutter/material.dart'; +import 'package:flutter_animated_icon_button/animate_change_icon.dart'; +import 'package:flutter_animated_icon_button/tap_fill_icon.dart'; +import 'package:flutter_animated_icon_button/tap_particle.dart'; + +void main() { + runApp(const IconSampleApp()); +} + +class IconSampleApp extends StatefulWidget { + const IconSampleApp({super.key}); + + @override + IconSampleAppState createState() => IconSampleAppState(); +} + +class IconSampleAppState extends State + with TickerProviderStateMixin { + late AnimationController _favoriteController; + late AnimationController _starController; + + @override + void initState() { + _favoriteController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + lowerBound: 0.0, + upperBound: 1.0, + ); + _favoriteController.addListener(() { + if (mounted) { + setState(() {}); + } + }); + + _starController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + lowerBound: 0.0, + upperBound: 1.0, + ); + _starController.addListener(() { + if (mounted) { + setState(() {}); + } + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Icon animations'), + ), + body: Center( + child: Column( + // crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Tap fill favorite button', + style: Theme.of(context).textTheme.titleLarge, + ), + TapFillIcon( + animationController: _favoriteController, + borderIcon: const Icon( + Icons.favorite_border, + color: Colors.grey, + size: 50, + ), + fillIcon: const Icon( + Icons.favorite, + color: Colors.red, + size: 50, + ), + initialPushed: false, + ), + const SizedBox( + height: 50, + ), + Text( + 'Tap fill start button with particle', + style: Theme.of(context).textTheme.titleLarge, + ), + TapParticle( + size: 50, + particleCount: 5, + particleLength: 10, + color: Colors.yellow, + syncAnimation: _starController, + duration: const Duration(milliseconds: 300), + child: TapFillIcon( + animationController: _starController, + borderIcon: const Icon( + Icons.star_border, + color: Colors.grey, + size: 50, + ), + fillIcon: const Icon( + Icons.star, + color: Colors.yellow, + size: 50, + ), + initialPushed: false, + ), + ), + const SizedBox( + height: 50, + ), + Text( + 'Tap change with animation button', + style: Theme.of(context).textTheme.titleLarge, + ), + const AnimateChangeIcon( + animateDuration: Duration(milliseconds: 300), + firstIcon: Icon( + Icons.play_arrow_rounded, + size: 50, + ), + secondIcon: Icon( + Icons.stop_rounded, + size: 50, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 1ae2e89..f5ea25e 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -76,7 +76,7 @@ packages: path: ".." relative: true source: path - version: "0.0.1" + version: "1.0.0" flutter_driver: dependency: transitive description: flutter diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index ed557bd..d3f5a12 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -1,27 +1 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:flutter_animated_icon_button_example/main.dart'; - -void main() { - testWidgets('Verify Platform version', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data!.startsWith('Running on:'), - ), - findsOneWidget, - ); - }); -} diff --git a/lib/animate_change_icon.dart b/lib/animate_change_icon.dart new file mode 100644 index 0000000..cc22b86 --- /dev/null +++ b/lib/animate_change_icon.dart @@ -0,0 +1,184 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +/// A [AnimateChangeIcon] is an icon button widget. +/// When this button tapped, it change the icon +/// from [firstIcon] to [secondIcon] with animations. +/// The animations exist rotaion and scale. +/// +/// +/// {@tool snippet} +/// This example shows how to create the button being able to change icons +/// from a play button to a stop button. +/// ```dart +/// AnimateChangeIcon( +/// firstIcon: Icon( +/// Icons.play_arrow_rounded, +/// size: 50, +/// ), +/// secondIcon: Icon( +/// Icons.stop_rounded, +/// size: 50, +/// ), +/// ), +/// ``` +/// {@end-tool} +class AnimateChangeIcon extends StatefulWidget { + const AnimateChangeIcon({ + required this.firstIcon, + required this.secondIcon, + this.animateDuration = const Duration(milliseconds: 300), + this.initialPushed = false, + this.animationController, + this.scaleAnimationCurve = Curves.linear, + this.rotateAnimationCurve = Curves.linear, + this.rotateBeginAngle = -pi / 2, + this.rotateEndAngle = 0.0, + this.onTap, + super.key, + }); + + /// An icon for show first and after [AnimateChangeIcon] was pushed odd time. + /// If [initialPushed] is true, + /// this icon display after [AnimateChangeIcon] was pushed even time. + final Icon firstIcon; + + /// An icon display after [AnimateChangeIcon] was pushed odd time. + /// If [initialPushed] is true, + /// this icon display after [AnimateChangeIcon] was pushed odd time or first. + final Icon secondIcon; + + /// An animation's duration. + final Duration animateDuration; + + /// The flg for decide the state of first. + final bool initialPushed; + + /// The controller for the animation of this button. + /// This controller control the animation of scale and rotate. + final AnimationController? animationController; + + /// The animation of this button for scale. + final Curve scaleAnimationCurve; + + /// The animation of this button for rotate. + final Curve rotateAnimationCurve; + + /// The callback function when on tap. + final void Function()? onTap; + + /// A first angle of a rotate animation. + /// A default value is -90 degree. + final double rotateBeginAngle; + + /// A final angle of a rotate animation. + /// A default value is 0 degree. + final double rotateEndAngle; + + @override + AnimateChangeIconState createState() => AnimateChangeIconState(); +} + +class AnimateChangeIconState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + bool _isPushed = false; + + @override + void initState() { + if (widget.animationController != null) { + _controller = widget.animationController!; + } else { + _controller = AnimationController( + vsync: this, + duration: widget.animateDuration, + ); + } + _controller.addListener(() { + if (mounted) { + setState(() {}); + } + }); + + if (widget.initialPushed) { + _isPushed = true; + _controller.value = _controller.upperBound; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + widget.onTap?.call(); + if (_isPushed) { + _controller.reverse(); + } else { + _controller.forward(); + } + + _isPushed = !_isPushed; + }, + child: Stack( + children: [ + Transform.scale( + scale: CurvedAnimation( + parent: _controller, + curve: widget.scaleAnimationCurve, + ) + .drive( + Tween( + begin: _controller.lowerBound, + end: _controller.upperBound, + ), + ) + .value, + child: Transform.rotate( + angle: CurvedAnimation( + parent: _controller, + curve: widget.rotateAnimationCurve, + ) + .drive( + Tween( + begin: _controller.upperBound * widget.rotateBeginAngle, + end: widget.rotateEndAngle, + ), + ) + .value, + child: widget.firstIcon, + ), + ), + Transform.scale( + scale: CurvedAnimation( + parent: _controller, + curve: widget.scaleAnimationCurve, + ) + .drive( + Tween( + begin: _controller.upperBound, + end: _controller.lowerBound, + ), + ) + .value, + child: Transform.rotate( + angle: CurvedAnimation( + parent: _controller, + curve: widget.rotateAnimationCurve, + ) + .drive( + Tween( + begin: widget.rotateEndAngle, + end: _controller.upperBound * widget.rotateBeginAngle, + ), + ) + .value, + child: widget.secondIcon, + ), + ), + ], + ), + ); + } +} diff --git a/lib/flutter_animated_icon_button.dart b/lib/flutter_animated_icon_button.dart index d3f5a12..8b13789 100644 --- a/lib/flutter_animated_icon_button.dart +++ b/lib/flutter_animated_icon_button.dart @@ -1 +1 @@ - + diff --git a/lib/tap_fill_icon.dart b/lib/tap_fill_icon.dart new file mode 100644 index 0000000..f59315a --- /dev/null +++ b/lib/tap_fill_icon.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; + +/// A TapFillIcon is an icon button widget. +/// When this button tapped, +/// it change the icon from [borderIcon] to [fillIcon] with animations. +/// If you push when this button's state is [fillIcon], +/// change from [fillIcon] to [borderIcon] without animations. +/// +/// +/// {@tool snippet} +/// This example shows how to create +/// a favorite button that is often used in SNS apps. +/// ```dart +/// TapFillIcon( +/// fillIcon: Icon(Icons.favorite, color: Colors.red), +/// borderIcon: Icon(Icons.favorite_border, color: Colors.grey), +/// initialPushed: false, +/// ), +/// ``` +/// {@end-tool} +class TapFillIcon extends StatefulWidget { + const TapFillIcon({ + required this.fillIcon, + required this.borderIcon, + this.animateDuration = const Duration(milliseconds: 300), + this.initialPushed = false, + this.animationController, + this.animationCurve = Curves.easeOutBack, + this.onTap, + this.onPush, + this.onPull, + super.key, + }); + + /// An icon after pushed this button. + final Widget fillIcon; + + /// An icon before pushed this button. + final Widget borderIcon; + + /// The duration of the animation from tap to filled icon. + final Duration animateDuration; + + /// The initial state of this button. + final bool initialPushed; + + /// An animation controller of this button. + final AnimationController? animationController; + + /// The animation curve when fill an icon. + final Curve animationCurve; + + /// A callback when tap this button. + final void Function()? onTap; + + /// A callback when tap this button from the state of [borderIcon]. + final void Function()? onPush; + + /// A callback when tap this button from the state of [fillIcon]. + final void Function()? onPull; + + @override + TapFillIconState createState() => TapFillIconState(); +} + +class TapFillIconState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + bool _isPushed = false; + + @override + void initState() { + if (widget.animationController != null) { + _controller = widget.animationController!; + } else { + _controller = AnimationController( + vsync: this, + duration: widget.animateDuration, + ); + } + _controller.addListener(() { + if (mounted) { + setState(() {}); + } + }); + + if (widget.initialPushed) { + _isPushed = true; + _controller.value = _controller.upperBound; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + widget.onTap?.call(); + if (_isPushed) { + widget.onPull?.call(); + _controller.value = _controller.lowerBound; + } else { + widget.onPush?.call(); + _controller.forward(); + } + + _isPushed = !_isPushed; + }, + child: Stack( + children: [ + widget.borderIcon, + Transform.scale( + scale: CurvedAnimation( + parent: _controller, + curve: widget.animationCurve, + ) + .drive( + Tween( + begin: _controller.lowerBound, + end: _controller.upperBound, + ), + ) + .value, + child: widget.fillIcon, + ), + ], + ), + ); + } +} diff --git a/lib/tap_particle.dart b/lib/tap_particle.dart new file mode 100644 index 0000000..91c3e18 --- /dev/null +++ b/lib/tap_particle.dart @@ -0,0 +1,227 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +/// A [TapParticle] display particles when push button. +/// If [syncAnimation] is set, display particles according to it. +/// +/// +/// {@tool snippet} +/// This example shows how to create the button that can display particle. +/// ```dart +///final controller = AnimationController( +/// vsync: this, +/// duration: Duration(milliseconds: 300), +/// lowerBound: 0.0, +/// upperBound: 1.0, +///); +///TapParticle( +/// size: 50, +/// particleCount: 5, +/// color: Colors.red, +/// syncAnimation: controller, +/// duration: const Duration(milliseconds: 500), +/// child: TapFillIcon( +/// animationController: controller, +/// borderIcon: const Icon( +/// Icons.star_border, +/// color: Colors.grey, +/// size: 50, +/// ), +/// fillIcon: const Icon( +/// Icons.star, +/// color: Colors.yellow, +/// size: 50, +/// ), +/// initialPushed: false, +/// ), +///), +/// ``` +/// {@end-tool} +class TapParticle extends StatefulWidget { + const TapParticle({ + this.size = 50, + this.child, + this.syncAnimation, + this.controller, + this.particleCount = 8, + this.duration = const Duration(milliseconds: 500), + this.particleLength, + this.color = Colors.yellow, + super.key, + }) : assert(!(syncAnimation != null && controller != null), ''); + + /// The size of [child] in this object. + /// It display empty space of the size. + final double size; + + /// The widget in the center of a particle. + final Widget? child; + + /// The Animation for synchronizing the particle animation. + /// The particle animation is starting + /// when [syncAnimation] called forward method. + /// If [syncAnimation] called reverse method, + /// the particle animation reset animation. + final AnimationController? syncAnimation; + + /// The animation controller for controlling the particle animation. + final AnimationController? controller; + + /// The animation's duration. + final Duration duration; + + /// The number of particle lines. + /// The particles display in a circle. + final int particleCount; + + /// The maximum length of a particle. + /// Particles are to this length when the animation value is just half. + final double? particleLength; + + /// The color of particles. + final Color color; + + @override + TapParticleState createState() => TapParticleState(); +} + +class TapParticleState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + + _controller = widget.controller != null + ? widget.controller! + : AnimationController(vsync: this, duration: widget.duration); + final syncAnimation = widget.syncAnimation; + if (syncAnimation != null) { + syncAnimation.addListener(() { + if (syncAnimation.status == AnimationStatus.forward) { + final startTrriger = + (syncAnimation.upperBound - syncAnimation.lowerBound) * 0.0; + if (syncAnimation.upperBound > syncAnimation.lowerBound && + syncAnimation.value > startTrriger) { + _controller.forward(); + } + if (syncAnimation.upperBound < syncAnimation.lowerBound && + syncAnimation.value < startTrriger) { + _controller.forward(); + } + } else if (syncAnimation.value == syncAnimation.lowerBound) { + _controller.value = _controller.lowerBound; + } + }); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + CircleParticle( + size: widget.size, + animation: _controller, + lineCount: widget.particleCount, + lineLength: widget.particleLength, + color: widget.color, + ), + if (widget.child != null) ...[ + widget.child!, + ], + ], + ); + } +} + +class CircleParticle extends StatelessWidget { + const CircleParticle({ + required this.animation, + required this.color, + this.lineCount = 8, + this.size = 50, + this.lineLength, + super.key, + }); + final Animation animation; + final int lineCount; + final double size; + final double? lineLength; + final Color color; + + @override + Widget build(BuildContext context) { + return Stack( + children: List.generate( + lineCount, + (i) => Transform.rotate( + angle: 2 * pi / lineCount * i, + child: CustomPaint( + size: Size(size, size), + painter: LineParticlePainter( + animation: animation, + color: color, + lineLength: lineLength != null ? lineLength! : size * 0.3, + ), + ), + ), + ), + ); + } +} + +class LineParticlePainter extends CustomPainter { + LineParticlePainter({ + required this.animation, + required this.lineLength, + required this.color, + }) : super(repaint: animation); + final Animation animation; + final double lineLength; + final Color color; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = 5; + + final startY = size.height / 2 - size.height * animation.value; + final endY = startY - lineLength * _lengthAnimation(animation.value); + + final start = Offset(size.width / 2, startY); + final end = Offset(size.width / 2, endY); + + canvas.drawLine(start, end, paint); + } + + /// This function convert from the value between 0.0 and 1.0 + /// to the cubic function that return the value below. + /// | animation | return value | + /// | -- | -- | + /// | 0.0 | 0.0 | + /// | 0.5 | 1.0 | + /// |1.0 | 0.0 | + double _lengthAnimation(double animation) { + var value = -15.8730158730158 * pow(animation, 3) + + 22.222222222222 * pow(animation, 2) + + -6.34920634920 * animation; + if (value < 0) { + value = 0; + } + return value; + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index cec719a..18279c7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,8 @@ name: flutter_animated_icon_button -description: A new Flutter plugin project. -version: 0.0.1 -homepage: +description: An animated icon button for Flutter apps. +version: 1.0.0 +homepage: https://github.com/RikitoNoto +repository: https://github.com/RikitoNoto/flutter_animated_icon_button environment: sdk: '>=3.0.5 <4.0.0' @@ -13,63 +14,11 @@ dependencies: plugin_platform_interface: ^2.0.2 dev_dependencies: + flutter_lints: ^2.0.0 flutter_test: sdk: flutter - flutter_lints: ^2.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - # This section identifies this Flutter project as a plugin project. - # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) - # which should be registered in the plugin registry. This is required for - # using method channels. - # The Android 'package' specifies package in which the registered class is. - # This is required for using method channels on Android. - # The 'ffiPlugin' specifies that native code should be built and bundled. - # This is required for using `dart:ffi`. - # All these are used by the tooling to maintain consistency when - # adding or updating assets for this project. plugin: platforms: - # This plugin project was generated without specifying any - # platforms with the `--platform` argument. If you see the `some_platform` map below, remove it and - # then add platforms following the instruction here: - # https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms - # ------------------- some_platform: pluginClass: somePluginClass - # ------------------- - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # To add custom fonts to your plugin package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages