├── example ├── lib │ ├── images │ │ └── themed.png │ └── main.dart ├── SponsoredByMyTextAi.png ├── android │ ├── 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 │ │ │ │ │ └── dev │ │ │ │ │ │ └── glasberg │ │ │ │ │ │ └── example_xaxa │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle │ └── settings.gradle ├── pubspec.yaml ├── README.md ├── .gitignore ├── .metadata ├── pubspec.lock └── analysis_options.yaml ├── lib ├── themed.dart └── src │ ├── const_theme_exception.dart │ ├── color_swatches.dart │ ├── themed_extensions.dart │ ├── color_util.dart │ ├── change_colors.dart │ └── themed.dart ├── .metadata ├── pubspec.yaml ├── LICENSE ├── CHANGELOG.md ├── .gitignore ├── pubspec.lock ├── analysis_options.yaml └── README.md /example/lib/images/themed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcglasberg/themed/HEAD/example/lib/images/themed.png -------------------------------------------------------------------------------- /example/SponsoredByMyTextAi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcglasberg/themed/HEAD/example/SponsoredByMyTextAi.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcglasberg/themed/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/marcglasberg/themed/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/marcglasberg/themed/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcglasberg/themed/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/marcglasberg/themed/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/dev/glasberg/example_xaxa/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.glasberg.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /lib/themed.dart: -------------------------------------------------------------------------------- 1 | export 'src/change_colors.dart'; 2 | export 'src/color_swatches.dart'; 3 | export 'src/color_util.dart'; 4 | export 'src/const_theme_exception.dart'; 5 | export 'src/themed.dart'; 6 | export 'src/themed_extensions.dart'; 7 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 6 | -------------------------------------------------------------------------------- /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/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /.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: d79295af24c3ed621c33713ecda14ad196fd9c31 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: Example for Themed 3 | publish_to: 'none' 4 | version: 1.0.0+1 5 | 6 | environment: 7 | sdk: '>=3.5.0 <4.0.0' 8 | 9 | dependencies: 10 | themed: 11 | path: ../ 12 | flutter: 13 | sdk: flutter 14 | 15 | dev_dependencies: 16 | flutter_test: 17 | sdk: flutter 18 | 19 | flutter: 20 | uses-material-design: true 21 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/src/const_theme_exception.dart: -------------------------------------------------------------------------------- 1 | class ConstThemeException { 2 | String msg; 3 | 4 | ConstThemeException(this.msg); 5 | 6 | @override 7 | String toString() => msg; 8 | 9 | @override 10 | bool operator ==(Object other) => 11 | identical(this, other) || 12 | other is ConstThemeException && runtimeType == other.runtimeType && msg == other.msg; 13 | 14 | @override 15 | int get hashCode => msg.hashCode; 16 | } 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: themed 2 | description: The themed package lets you define a theme with const values, and then, by using some dark Dart magic, go and change them dynamically anyway. 3 | version: 8.1.0 4 | # author: Marcelo Glasberg 5 | homepage: https://github.com/marcglasberg/themed 6 | topics: 7 | - theme 8 | - theming 9 | - ui 10 | - color 11 | - font 12 | 13 | environment: 14 | sdk: '>=3.5.0 <4.0.0' 15 | flutter: ">=3.16.0" 16 | 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | 21 | dev_dependencies: 22 | flutter_test: 23 | sdk: flutter 24 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | Themed example 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/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.1.0" apply false 22 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/app/debug 42 | /android/app/profile 43 | /android/app/release 44 | -------------------------------------------------------------------------------- /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: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 17 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 18 | - platform: android 19 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 20 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /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 | Copyright 2021 Parkside Technologies 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted 4 | provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 10 | and the following disclaimer in the documentation and/or other materials provided with the 11 | distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse 14 | or promote products derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 17 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 18 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 19 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 23 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | android { 9 | namespace = "dev.glasberg.example" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_1_8 15 | targetCompatibility = JavaVersion.VERSION_1_8 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_1_8 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "dev.glasberg.example" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = flutter.minSdkVersion 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.debug 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/color_swatches.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// You can create a [MaterialColor] color swatch from a single [primary] 4 | /// color (which you can later change using the Themed package): 5 | /// 6 | /// ``` 7 | /// static const MaterialColor myColorSwatch = MaterialColorRef( 8 | /// AppColors.primary, 9 | /// { 10 | /// 50: AppColors.primary, 11 | /// 100: AppColors.primary, 12 | /// 200: AppColors.primary, 13 | /// 300: AppColors.primary, 14 | /// 400: AppColors.primary, 15 | /// 500: AppColors.primary, 16 | /// 600: AppColors.primary, 17 | /// 700: AppColors.primary, 18 | /// 800: AppColors.primary, 19 | /// 900: AppColors.primary, 20 | /// }, 21 | /// ); 22 | /// ``` 23 | class MaterialColorSwatch extends MaterialColor { 24 | final Color primary; 25 | 26 | const MaterialColorSwatch(this.primary, Map swatch) : super(0, swatch); 27 | 28 | @override 29 | int get value => primary.value; 30 | } 31 | 32 | /// You can create a [MaterialAccentColorSwatch] color swatch from a 33 | /// single [primary] color (which you can later change using the Themed package): 34 | /// 35 | /// ``` 36 | /// static const MaterialColor myColorSwatch = MaterialAccentColorSwatch( 37 | /// AppColors.primary, 38 | /// { 39 | /// 50: AppColors.primary, 40 | /// 100: AppColors.primary, 41 | /// 200: AppColors.primary, 42 | /// 400: AppColors.primary, 43 | /// 700: AppColors.primary, 44 | /// }, 45 | /// ); 46 | /// ``` 47 | class MaterialAccentColorSwatch extends MaterialAccentColor { 48 | final Color primary; 49 | 50 | const MaterialAccentColorSwatch(this.primary, Map swatch) 51 | : super(0, swatch); 52 | 53 | @override 54 | int get value => primary.value; 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/themed_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension TextStyleExtension on TextStyle { 4 | // 5 | /// You can create a [TextStyle] by adding the [TextStyle] to one these types: 6 | /// [Color], [String] (fontFamily), [FontSize], [FontWeight], [FontStyle], 7 | /// [TextBaseline], [Locale], List<[Shadow]>, List<[FontFeature]>, [Decoration], 8 | /// [TextDecorationStyle], or [TextHeight]. 9 | /// 10 | /// For example: 11 | /// 12 | /// ``` 13 | /// Text('Hello', style: TextStyle(fontSize: 14.0) + "Roboto" + Colors.red + FontStyle.italic); 14 | /// ``` 15 | /// 16 | /// Note: If you add null, that's not an error. It will simply return the same TextStyle. 17 | /// However, if you add an invalid type it will throw an error in RUN TIME. 18 | /// 19 | TextStyle operator +(Object? obj) => // 20 | (obj == null) 21 | ? this 22 | : copyWith( 23 | color: obj is Color ? obj : null, 24 | fontFamily: obj is String ? obj : null, 25 | fontSize: obj is FontSize ? obj.fontSize : null, 26 | fontWeight: obj is FontWeight ? obj : null, 27 | fontStyle: obj is FontStyle ? obj : null, 28 | textBaseline: obj is TextBaseline ? obj : null, 29 | locale: obj is Locale ? obj : null, 30 | shadows: obj is List ? obj : null, 31 | fontFeatures: obj is List ? obj : null, 32 | decoration: obj is TextDecoration ? obj : null, 33 | decorationStyle: obj is TextDecorationStyle ? obj : null, 34 | height: obj is TextHeight ? obj.height : null, 35 | ); 36 | 37 | /// Instead of using [operator +] you can use the [add] method. 38 | /// If [apply] is false, the provided [obj] will not be added. 39 | /// 40 | TextStyle add(Object? obj, {bool apply = true}) => (apply) ? this + obj : this; 41 | } 42 | 43 | class TextHeight { 44 | final double height; 45 | 46 | const TextHeight(this.height); 47 | } 48 | 49 | class FontSize { 50 | final double fontSize; 51 | 52 | const FontSize(this.fontSize); 53 | } 54 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Sponsored by [MyText.ai](https://mytext.ai) 2 | 3 | [![](./example/SponsoredByMyTextAi.png)](https://mytext.ai) 4 | 5 | ## 8.1.0 6 | 7 | * `Themed.reset` 8 | 9 | ## 8.0.2 10 | 11 | * Version 8 and up are compatible with Flutter 3.27.0 and up. Note: Version 7.0.0 is 12 | not compatible with the new Flutter versions, but it will not throw any errors. It will 13 | just not work as expected. This means you MUST upgrade as soon as you upgrade your 14 | Flutter version: 15 | 16 | ```yaml 17 | dependencies: 18 | themed: ^8.0.1 19 | ``` 20 | 21 | ## 7.0.0 22 | 23 | * Theme change improvement: Now, when a theme is changed, it will make all color 24 | references different from themselves during exactly one frame. This will assure 25 | that all widgets that depend on the theme will be rebuilt with the new theme. 26 | While technically this is a breaking change, it's unlikely to affect you. 27 | 28 | * `ColorRef.sameColor()` method to compare the current color of two `ColorRef` objects, 29 | or with a `Color` object. Note that the compared color is the effective one, that 30 | depend on the current theme. 31 | 32 | * Fixed bug that affected Hot Reload. 33 | 34 | ## 5.1.1 35 | 36 | * Added `Color.removeOpacity()` extension method. 37 | Note methods `addOpacity()`, `darker()`, `lighter()`, `average()` and `decolorize` 38 | already existed. 39 | 40 | ## 5.0.3 41 | 42 | * Flutter 3.16.0 compatible. 43 | 44 | ## 4.0.0 45 | 46 | * Flutter 3.13.0 compatible. 47 | 48 | ## 3.0.2 49 | 50 | * Flutter 2.8.0 compatible. 51 | 52 | ## 2.4.0 53 | 54 | * `ChangeColors` widget to change the brightness, saturation and hue of any widget, 55 | including images. 56 | 57 | ## 2.3.0 58 | 59 | * Color extension: `darker`, `lighter`, `average`, `decolorize`, `addOpacity`, 60 | `rgbaToArgb` and `abgrToArgb` methods. 61 | 62 | ## 2.2.0 63 | 64 | * Improved `ColorRef.toString()` and `TextStyleRef.toString()` methods. 65 | 66 | ## 2.1.0 67 | 68 | * Saving and setting themes by key: `Themed.save()`, `Themed.setThemeByKey()` etc. 69 | 70 | * Fixed https://github.com/marcglasberg/themed/issues/1 71 | 72 | ## 2.0.5 73 | 74 | * Compatible with Flutter 2.5. 75 | 76 | ## 2.0.1 77 | 78 | * Breaking change: The `id` now must only be provided if it's necessary to differentiate 79 | constants. 80 | 81 | ## 1.0.0 82 | 83 | * Initial Commit. 84 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.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 | # Flutter/Dart/Pub related 19 | **/doc/api/ 20 | .dart_tool/ 21 | .flutter-plugins 22 | .flutter-plugins-dependencies 23 | .packages 24 | .pub-cache/ 25 | .pub/ 26 | build/ 27 | 28 | # Android related 29 | **/android/**/gradle-wrapper.jar 30 | **/android/.gradle 31 | **/android/captures/ 32 | **/android/gradlew 33 | **/android/gradlew.bat 34 | **/android/local.properties 35 | **/android/**/GeneratedPluginRegistrant.java 36 | 37 | # iOS/XCode related 38 | **/ios/**/*.mode1v3 39 | **/ios/**/*.mode2v3 40 | **/ios/**/*.moved-aside 41 | **/ios/**/*.pbxuser 42 | **/ios/**/*.perspectivev3 43 | **/ios/**/*sync/ 44 | **/ios/**/.sconsign.dblite 45 | **/ios/**/.tags* 46 | **/ios/**/.vagrant/ 47 | **/ios/**/DerivedData/ 48 | **/ios/**/Icon? 49 | **/ios/**/Pods/ 50 | **/ios/**/.symlinks/ 51 | **/ios/**/profile 52 | **/ios/**/xcuserdata 53 | **/ios/.generated/ 54 | **/ios/Flutter/App.framework 55 | **/ios/Flutter/Flutter.framework 56 | **/ios/Flutter/Flutter.podspec 57 | **/ios/Flutter/Generated.xcconfig 58 | **/ios/Flutter/ephemeral 59 | **/ios/Flutter/app.flx 60 | **/ios/Flutter/app.zip 61 | **/ios/Flutter/flutter_assets/ 62 | **/ios/Flutter/flutter_export_environment.sh 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | 72 | ### Intellij ### 73 | 74 | # For each user. 75 | .idea/**/workspace.xml 76 | .idea/**/tasks.xml 77 | .idea/**/usage.statistics.xml 78 | .idea/**/dictionaries 79 | .idea/**/shelf 80 | **/.idea/workspace.xml 81 | 82 | # Generated files. 83 | .idea/**/contentModel.xml 84 | 85 | # Sensitive information or files that change a lot. 86 | .idea/**/dataSources/ 87 | .idea/**/dataSources.ids 88 | .idea/**/dataSources.local.xml 89 | .idea/**/sqlDataSources.xml 90 | .idea/**/dynamic.xml 91 | .idea/**/uiDesigner.xml 92 | .idea/**/dbnavigator.xml 93 | 94 | # Gradle. 95 | .idea/**/gradle.xml 96 | .idea/**/libraries/ 97 | 98 | # IntelliJ. 99 | out/ 100 | 101 | # Plugin mpeltonen/sbt-idea. 102 | .idea_modules/ 103 | 104 | # Web related 105 | lib/generated_plugin_registrant.dart 106 | 107 | # Symbolication related 108 | app.*.symbols 109 | 110 | # Obfuscation related 111 | app.*.map.json 112 | 113 | # Android Studio will place build artifacts here 114 | /android/app/debug 115 | /android/app/profile 116 | /android/app/release 117 | 118 | # Android serialized cache of Android Studio 3.1+. 119 | .idea/caches/build_file_checksums.ser 120 | 121 | # VS Code 122 | .vscode/ 123 | 124 | .env 125 | -------------------------------------------------------------------------------- /lib/src/color_util.dart: -------------------------------------------------------------------------------- 1 | import "package:flutter/material.dart"; 2 | import 'package:themed/src/change_colors.dart'; 3 | 4 | extension ColorUtil on Color { 5 | // 6 | /// Makes the color lighter (more white), by the given [value], from `0` (no change) to `1` (white). 7 | /// If [value] is not provided, it will be 0.5 (50% change). 8 | /// If [value] is less than 0, it's 0. If more than 1, it's 1. 9 | /// Doesn't change the opacity. 10 | /// 11 | /// See also: [ChangeColors] 12 | /// 13 | Color lighter([double value = 0.5]) => 14 | Color.lerp(this, Colors.white, _limit(value))!.withAlpha(alpha); 15 | 16 | /// Makes the color darker (more black), by the given [value], from `0` (no change) to `1` (black). 17 | /// If [value] is not provided, it will be 0.5 (50% change). 18 | /// If [value] is less than 0, it's 0. If more than 1, it's 1. 19 | /// Doesn't change the opacity. 20 | /// 21 | /// See also: [ChangeColors] 22 | /// 23 | Color darker([double value = 0.5]) => 24 | Color.lerp(this, Colors.black, _limit(value))!.withAlpha(alpha); 25 | 26 | /// Makes the current color more similar to the given [color], by the given [value], 27 | /// from `0` (no change) to `1` (equal to [color]). 28 | /// If [value] is not provided, it will be 0.5 (50% change). 29 | /// If [value] is less than 0, it's 0. If more than 1, it's 1. 30 | /// Doesn't change the opacity. 31 | /// 32 | /// See also: [ChangeColors] 33 | /// 34 | Color average(Color color, [double value = 0.5]) => 35 | Color.lerp(this, color, _limit(value))!; 36 | 37 | /// Makes the current color more grey (aprox. keeping its luminance), by the given [value], 38 | /// from `0` (no change) to `1` (grey). 39 | /// If [value] is not provided, it will be 1 (100% change, no color at all). 40 | /// If [value] is less than 0, it's 0. If more than 1, it's 1. 41 | /// Doesn't change the opacity. 42 | /// 43 | /// See also: [ChangeColors] 44 | /// 45 | Color decolorize([double value = 1]) { 46 | int average = (red + green + blue) ~/ 3; 47 | var color = Color.fromARGB(alpha, average, average, average); 48 | return Color.lerp(this, color, _limit(value))!; 49 | } 50 | 51 | /// Makes the current color more transparent, by the given [value], 52 | /// from `0` (total transparency) to `1` (no change). 53 | /// If [value] is not provided, it will be 0.5 (50% change). 54 | /// If [value] is less than 0, it's 0. If more than 1, it's 1. 55 | /// Makes it more transparent if percent < 1. 56 | Color addOpacity([double value = 0.5]) => 57 | Color.fromARGB((alpha * _limit(value)).round(), red, green, blue); 58 | 59 | /// Makes the current color more opaque, by the given [value], 60 | /// from `0` (no change) to `1` (fully opaque). 61 | /// If [value] is not provided, it will be 0.5 (50% change). 62 | /// If [value] is less than 0, it's 0. If more than 1, it's 1. 63 | /// Makes it more opaque if percent < 1. 64 | Color removeOpacity([double value = 0.5]) { 65 | return Color.fromARGB( 66 | (alpha + ((255 - alpha) * _limit(value))).round(), red, green, blue); 67 | } 68 | 69 | /// Converts the RGBA color representation to ARGB. 70 | static int rgbaToArgb(int rgbaColor) { 71 | int a = rgbaColor & 0xFF; 72 | int rgb = rgbaColor >> 8; 73 | return rgb + (a << 24); 74 | } 75 | 76 | /// Converts the ABGR color representation to ARGB. 77 | static int abgrToArgb(int argbColor) { 78 | int r = (argbColor >> 16) & 0xFF; 79 | int b = argbColor & 0xFF; 80 | return (argbColor & 0xFF00FF00) | (b << 16) | r; 81 | } 82 | 83 | double _limit(double value) => (value < 0) ? 0 : (value > 1 ? 1 : value); 84 | } 85 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.19.0" 44 | fake_async: 45 | dependency: transitive 46 | description: 47 | name: fake_async 48 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.3.1" 52 | flutter: 53 | dependency: "direct main" 54 | description: flutter 55 | source: sdk 56 | version: "0.0.0" 57 | flutter_test: 58 | dependency: "direct dev" 59 | description: flutter 60 | source: sdk 61 | version: "0.0.0" 62 | leak_tracker: 63 | dependency: transitive 64 | description: 65 | name: leak_tracker 66 | sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" 67 | url: "https://pub.dev" 68 | source: hosted 69 | version: "10.0.7" 70 | leak_tracker_flutter_testing: 71 | dependency: transitive 72 | description: 73 | name: leak_tracker_flutter_testing 74 | sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" 75 | url: "https://pub.dev" 76 | source: hosted 77 | version: "3.0.8" 78 | leak_tracker_testing: 79 | dependency: transitive 80 | description: 81 | name: leak_tracker_testing 82 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "3.0.1" 86 | matcher: 87 | dependency: transitive 88 | description: 89 | name: matcher 90 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "0.12.16+1" 94 | material_color_utilities: 95 | dependency: transitive 96 | description: 97 | name: material_color_utilities 98 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "0.11.1" 102 | meta: 103 | dependency: transitive 104 | description: 105 | name: meta 106 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 107 | url: "https://pub.dev" 108 | source: hosted 109 | version: "1.15.0" 110 | path: 111 | dependency: transitive 112 | description: 113 | name: path 114 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "1.9.0" 118 | sky_engine: 119 | dependency: transitive 120 | description: flutter 121 | source: sdk 122 | version: "0.0.0" 123 | source_span: 124 | dependency: transitive 125 | description: 126 | name: source_span 127 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 128 | url: "https://pub.dev" 129 | source: hosted 130 | version: "1.10.0" 131 | stack_trace: 132 | dependency: transitive 133 | description: 134 | name: stack_trace 135 | sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" 136 | url: "https://pub.dev" 137 | source: hosted 138 | version: "1.12.0" 139 | stream_channel: 140 | dependency: transitive 141 | description: 142 | name: stream_channel 143 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "2.1.2" 147 | string_scanner: 148 | dependency: transitive 149 | description: 150 | name: string_scanner 151 | sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "1.3.0" 155 | term_glyph: 156 | dependency: transitive 157 | description: 158 | name: term_glyph 159 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "1.2.1" 163 | test_api: 164 | dependency: transitive 165 | description: 166 | name: test_api 167 | sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "0.7.3" 171 | vector_math: 172 | dependency: transitive 173 | description: 174 | name: vector_math 175 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "2.1.4" 179 | vm_service: 180 | dependency: transitive 181 | description: 182 | name: vm_service 183 | sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "14.3.0" 187 | sdks: 188 | dart: ">=3.5.0 <4.0.0" 189 | flutter: ">=3.18.0-18.0.pre.54" 190 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.19.0" 44 | fake_async: 45 | dependency: transitive 46 | description: 47 | name: fake_async 48 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.3.1" 52 | flutter: 53 | dependency: "direct main" 54 | description: flutter 55 | source: sdk 56 | version: "0.0.0" 57 | flutter_test: 58 | dependency: "direct dev" 59 | description: flutter 60 | source: sdk 61 | version: "0.0.0" 62 | leak_tracker: 63 | dependency: transitive 64 | description: 65 | name: leak_tracker 66 | sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" 67 | url: "https://pub.dev" 68 | source: hosted 69 | version: "10.0.7" 70 | leak_tracker_flutter_testing: 71 | dependency: transitive 72 | description: 73 | name: leak_tracker_flutter_testing 74 | sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" 75 | url: "https://pub.dev" 76 | source: hosted 77 | version: "3.0.8" 78 | leak_tracker_testing: 79 | dependency: transitive 80 | description: 81 | name: leak_tracker_testing 82 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "3.0.1" 86 | matcher: 87 | dependency: transitive 88 | description: 89 | name: matcher 90 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "0.12.16+1" 94 | material_color_utilities: 95 | dependency: transitive 96 | description: 97 | name: material_color_utilities 98 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "0.11.1" 102 | meta: 103 | dependency: transitive 104 | description: 105 | name: meta 106 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 107 | url: "https://pub.dev" 108 | source: hosted 109 | version: "1.15.0" 110 | path: 111 | dependency: transitive 112 | description: 113 | name: path 114 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "1.9.0" 118 | sky_engine: 119 | dependency: transitive 120 | description: flutter 121 | source: sdk 122 | version: "0.0.0" 123 | source_span: 124 | dependency: transitive 125 | description: 126 | name: source_span 127 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 128 | url: "https://pub.dev" 129 | source: hosted 130 | version: "1.10.0" 131 | stack_trace: 132 | dependency: transitive 133 | description: 134 | name: stack_trace 135 | sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" 136 | url: "https://pub.dev" 137 | source: hosted 138 | version: "1.12.0" 139 | stream_channel: 140 | dependency: transitive 141 | description: 142 | name: stream_channel 143 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 144 | url: "https://pub.dev" 145 | source: hosted 146 | version: "2.1.2" 147 | string_scanner: 148 | dependency: transitive 149 | description: 150 | name: string_scanner 151 | sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" 152 | url: "https://pub.dev" 153 | source: hosted 154 | version: "1.3.0" 155 | term_glyph: 156 | dependency: transitive 157 | description: 158 | name: term_glyph 159 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 160 | url: "https://pub.dev" 161 | source: hosted 162 | version: "1.2.1" 163 | test_api: 164 | dependency: transitive 165 | description: 166 | name: test_api 167 | sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "0.7.3" 171 | themed: 172 | dependency: "direct main" 173 | description: 174 | path: ".." 175 | relative: true 176 | source: path 177 | version: "8.1.0" 178 | vector_math: 179 | dependency: transitive 180 | description: 181 | name: vector_math 182 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 183 | url: "https://pub.dev" 184 | source: hosted 185 | version: "2.1.4" 186 | vm_service: 187 | dependency: transitive 188 | description: 189 | name: vm_service 190 | sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b 191 | url: "https://pub.dev" 192 | source: hosted 193 | version: "14.3.0" 194 | sdks: 195 | dart: ">=3.5.0 <4.0.0" 196 | flutter: ">=3.18.0-18.0.pre.54" 197 | -------------------------------------------------------------------------------- /lib/src/change_colors.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | // Based upon: https://stackoverflow.com/questions/64639589/how-to-adjust-hue-saturation-and-brightness-of-an-image-in-flutter 6 | // from BananaNeil: https://stackoverflow.com/users/937841/banananeil. 7 | // This is, in turn, based upon: https://stackoverflow.com/a/7917978/937841 8 | // All credit goes to the above authors. 9 | 10 | /// Use the [ChangeColors] widget to change the brightness, saturation 11 | /// and hue of any widget, including images. 12 | /// 13 | /// Example: 14 | /// 15 | /// ``` 16 | /// ChangeColors( 17 | /// hue: 0.55, 18 | /// brightness: 0.2, 19 | /// saturation: 0.1, 20 | /// child: Image.asset('myImage.png'), 21 | /// ); 22 | /// ``` 23 | /// 24 | /// To achieve a greyscale effect, you may also use the 25 | /// [ChangeColors.greyscale] constructor. 26 | /// 27 | class ChangeColors extends StatelessWidget { 28 | // 29 | 30 | /// Negative value will make it darker (-1 is darkest). 31 | /// Positive value will make it lighter (1 is the maximum, but you can go above it). 32 | /// Note: 0.0 is unchanged. 33 | final double brightness; 34 | 35 | /// Negative value will make it less saturated (-1 is greyscale). 36 | /// Positive value will make it more saturated (1 is the maximum, but you can go above it). 37 | /// Note: 0.0 is unchanged. 38 | final double saturation; 39 | 40 | /// From -1.0 to 1.0 (Note: 1.0 wraps into -1.0, such as 1.2 is the same as -0.8). 41 | /// Note: 0.0 is unchanged. Adding or subtracting multiples of 2.0 also keeps it unchanged. 42 | final double hue; 43 | 44 | final Widget child; 45 | 46 | ChangeColors({ 47 | Key? key, 48 | this.brightness = 0.0, 49 | double saturation = 0.0, 50 | this.hue = 0.0, 51 | required this.child, 52 | }) : saturation = _clampSaturation(saturation), 53 | super(key: key); 54 | 55 | ChangeColors.greyscale({ 56 | Key? key, 57 | this.brightness = 0.0, 58 | required this.child, 59 | }) : saturation = -1.0, 60 | hue = 0.0, 61 | super(key: key); 62 | 63 | static double _clampSaturation(double value) => value.clamp(-1.0, double.nan); 64 | 65 | @override 66 | Widget build(BuildContext context) { 67 | return ColorFiltered( 68 | colorFilter: ColorFilter.matrix(_ColorFilterGenerator.brightnessAdjustMatrix( 69 | value: brightness, 70 | )), 71 | child: ColorFiltered( 72 | colorFilter: ColorFilter.matrix(_ColorFilterGenerator.saturationAdjustMatrix( 73 | value: saturation, 74 | )), 75 | child: ColorFiltered( 76 | colorFilter: ColorFilter.matrix(_ColorFilterGenerator.hueAdjustMatrix( 77 | value: hue, 78 | )), 79 | child: child, 80 | ))); 81 | } 82 | } 83 | 84 | 85 | 86 | class _ColorFilterGenerator { 87 | // 88 | static List hueAdjustMatrix({required double value}) { 89 | value = value * pi; 90 | 91 | if (value == 0) 92 | return [ 93 | 1, 94 | 0, 95 | 0, 96 | 0, 97 | 0, 98 | 0, 99 | 1, 100 | 0, 101 | 0, 102 | 0, 103 | 0, 104 | 0, 105 | 1, 106 | 0, 107 | 0, 108 | 0, 109 | 0, 110 | 0, 111 | 1, 112 | 0, 113 | ]; 114 | 115 | double cosVal = cos(value); 116 | double sinVal = sin(value); 117 | double lumR = 0.213; 118 | double lumG = 0.715; 119 | double lumB = 0.072; 120 | 121 | return List.from([ 122 | (lumR + (cosVal * (1 - lumR))) + (sinVal * (-lumR)), 123 | (lumG + (cosVal * (-lumG))) + (sinVal * (-lumG)), 124 | (lumB + (cosVal * (-lumB))) + (sinVal * (1 - lumB)), 125 | 0, 126 | 0, 127 | (lumR + (cosVal * (-lumR))) + (sinVal * 0.143), 128 | (lumG + (cosVal * (1 - lumG))) + (sinVal * 0.14), 129 | (lumB + (cosVal * (-lumB))) + (sinVal * (-0.283)), 130 | 0, 131 | 0, 132 | (lumR + (cosVal * (-lumR))) + (sinVal * (-(1 - lumR))), 133 | (lumG + (cosVal * (-lumG))) + (sinVal * lumG), 134 | (lumB + (cosVal * (1 - lumB))) + (sinVal * lumB), 135 | 0, 136 | 0, 137 | 0, 138 | 0, 139 | 0, 140 | 1, 141 | 0, 142 | ]).map((i) => i.toDouble()).toList(); 143 | } 144 | 145 | static List brightnessAdjustMatrix({required double value}) { 146 | if (value <= 0) 147 | value = value * 255; 148 | else 149 | value = value * 100; 150 | 151 | if (value == 0) 152 | return [ 153 | 1, 154 | 0, 155 | 0, 156 | 0, 157 | 0, 158 | 0, 159 | 1, 160 | 0, 161 | 0, 162 | 0, 163 | 0, 164 | 0, 165 | 1, 166 | 0, 167 | 0, 168 | 0, 169 | 0, 170 | 0, 171 | 1, 172 | 0, 173 | ]; 174 | 175 | return List.from( 176 | [1, 0, 0, 0, value, 0, 1, 0, 0, value, 0, 0, 1, 0, value, 0, 0, 0, 1, 0]) 177 | .map((i) => i.toDouble()) 178 | .toList(); 179 | } 180 | 181 | static List saturationAdjustMatrix({required double value}) { 182 | value = value * 100; 183 | 184 | if (value == 0) 185 | return [ 186 | 1, 187 | 0, 188 | 0, 189 | 0, 190 | 0, 191 | 0, 192 | 1, 193 | 0, 194 | 0, 195 | 0, 196 | 0, 197 | 0, 198 | 1, 199 | 0, 200 | 0, 201 | 0, 202 | 0, 203 | 0, 204 | 1, 205 | 0, 206 | ]; 207 | 208 | double x = ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble(); 209 | double lumR = 0.3086; 210 | double lumG = 0.6094; 211 | double lumB = 0.082; 212 | 213 | return List.from([ 214 | (lumR * (1 - x)) + x, 215 | lumG * (1 - x), 216 | lumB * (1 - x), 217 | 0, 218 | 0, 219 | lumR * (1 - x), 220 | (lumG * (1 - x)) + x, 221 | lumB * (1 - x), 222 | 0, 223 | 0, 224 | lumR * (1 - x), 225 | lumG * (1 - x), 226 | (lumB * (1 - x)) + x, 227 | 0, 228 | 0, 229 | 0, 230 | 0, 231 | 0, 232 | 1, 233 | 0, 234 | ]).map((i) => i.toDouble()).toList(); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Specify analysis options. 2 | # 3 | # Until there are meta linter rules, each desired lint must be explicitly enabled. 4 | # See: https://github.com/dart-lang/linter/issues/288 5 | # 6 | # For a list of lints, see: http://dart-lang.github.io/linter/lints/ 7 | # See the configuration guide for more 8 | # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer 9 | # 10 | # There are four similar analysis options files in the flutter repos: 11 | # - analysis_options.yaml (this file) 12 | # - packages/flutter/lib/analysis_options_user.yaml 13 | # - https://github.com/flutter/plugins/blob/master/analysis_options.yaml 14 | # - https://github.com/flutter/engine/blob/master/analysis_options.yaml 15 | # 16 | # This file contains the analysis options used by Flutter tools, such as IntelliJ, 17 | # Android Studio, and the `flutter analyze` command. 18 | # 19 | # The flutter/plugins repo contains a copy of this file, which should be kept 20 | # in sync with this file. 21 | 22 | analyzer: 23 | # strong-mode: 24 | # implicit-casts: false 25 | # implicit-dynamic: false 26 | errors: 27 | # treat missing required parameters as a warning (not a hint) 28 | missing_required_param: warning 29 | # treat missing returns as a warning (not a hint) 30 | missing_return: error 31 | # allow having TODOs in the code 32 | todo: ignore 33 | exclude: 34 | - 'bin/cache/**' 35 | # the following two are relative to the stocks example and the flutter package respectively 36 | # see https://github.com/dart-lang/sdk/issues/28463 37 | - '**/i18n/string_messages_**.dart' 38 | - '/**/i18n/string_messages_**.dart' 39 | - '**/i18n/string_messages_***.dart' 40 | - '/**/i18n/string_messages_***.dart' 41 | - 'lib/src/http/**' 42 | 43 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 44 | linter: 45 | rules: 46 | - always_declare_return_types 47 | - always_require_non_null_named_parameters 48 | - annotate_overrides 49 | - avoid_empty_else 50 | - avoid_field_initializers_in_const_classes 51 | # - avoid_function_literals_in_foreach_calls 52 | - avoid_init_to_null 53 | - avoid_null_checks_in_equality_operators 54 | - avoid_relative_lib_imports 55 | - avoid_renaming_method_parameters 56 | - avoid_return_types_on_setters 57 | - avoid_slow_async_io 58 | - await_only_futures 59 | # - camel_case_types 60 | - cancel_subscriptions 61 | - control_flow_in_finally 62 | - directives_ordering 63 | - empty_catches 64 | - empty_constructor_bodies 65 | - empty_statements 66 | - hash_and_equals 67 | - implementation_imports 68 | - collection_methods_unrelated_type 69 | - library_names 70 | - library_prefixes 71 | - no_duplicate_case_values 72 | - overridden_fields 73 | - package_api_docs 74 | - package_names 75 | - package_prefixed_library_names 76 | - prefer_adjacent_string_concatenation 77 | - prefer_asserts_in_initializer_lists 78 | - prefer_collection_literals 79 | - prefer_conditional_assignment 80 | - prefer_const_constructors 81 | # - prefer_const_constructors_in_immutables 82 | - prefer_const_declarations 83 | - prefer_contains 84 | - prefer_final_fields 85 | # - prefer_foreach 86 | - prefer_generic_function_type_aliases 87 | - prefer_initializing_formals 88 | - prefer_is_empty 89 | - prefer_is_not_empty 90 | - prefer_typing_uninitialized_variables 91 | - recursive_getters 92 | - slash_for_doc_comments 93 | - sort_unnamed_constructors_first 94 | - test_types_in_equals 95 | - throw_in_finally 96 | - type_init_formals 97 | - unnecessary_brace_in_string_interps 98 | - unnecessary_getters_setters 99 | - unnecessary_null_aware_assignments 100 | - unnecessary_null_in_if_null_operators 101 | - unnecessary_overrides 102 | - unnecessary_this 103 | - unrelated_type_equality_checks 104 | - use_rethrow_when_possible 105 | - valid_regexps 106 | # - prefer_double_quotes 107 | # - unnecessary_new ➜ Isso aqui eu queria deixar, mas o linter ainda não reconhece. Tentar novamente no futuro. 108 | # - unnecessary_const ➜ Isso aqui eu queria deixar, mas o linter ainda não reconhece. Tentar novamente no futuro. 109 | # - non_constant_identifier_names 110 | # - always_put_control_body_on_new_line 111 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 112 | # - always_specify_types 113 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 114 | # - avoid_as 115 | # - avoid_bool_literals_in_conditional_expressions # not yet tested 116 | # - avoid_catches_without_on_clauses # we do this commonly 117 | # - avoid_catching_errors # we do this commonly 118 | # - avoid_classes_with_only_static_members 119 | # - avoid_double_and_int_checks # only useful when targeting JS runtime 120 | # - avoid_js_rounded_ints # only useful when targeting JS runtime 121 | # - avoid_positional_boolean_parameters # not yet tested 122 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 123 | # - avoid_returning_null # we do this commonly 124 | # - avoid_returning_this # https://github.com/dart-lang/linter/issues/842 125 | # - avoid_setters_without_getters # not yet tested 126 | # - avoid_single_cascade_in_expression_statements # not yet tested 127 | # - avoid_types_as_parameter_names # https://github.com/dart-lang/linter/pull/954/files 128 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 129 | # - avoid_unused_constructor_parameters # https://github.com/dart-lang/linter/pull/847 130 | # - cascade_invocations # not yet tested 131 | # - close_sinks # https://github.com/flutter/flutter/issues/5789 132 | # - comment_references # blocked on https://github.com/dart-lang/dartdoc/issues/1153 133 | # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 134 | # - invariant_booleans # https://github.com/flutter/flutter/issues/5790 135 | # - join_return_with_assignment # not yet tested 136 | # - literal_only_boolean_expressions # https://github.com/flutter/flutter/issues/5791 137 | # - no_adjacent_strings_in_list 138 | # - omit_local_variable_types # opposite of always_specify_types 139 | # - one_member_abstracts # too many false positives 140 | # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 141 | # - parameter_assignments # we do this commonly 142 | # - prefer_const_literals_to_create_immutables 143 | # - prefer_constructors_over_static_methods # not yet tested 144 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 145 | # - prefer_final_locals 146 | # - prefer_function_declarations_over_variables # not yet tested 147 | # - prefer_interpolation_to_compose_strings # not yet tested 148 | # - prefer_iterable_whereType # https://github.com/dart-lang/sdk/issues/32463 149 | # - prefer_single_quotes 150 | # - sort_constructors_first 151 | # - type_annotate_public_apis # subset of always_specify_types 152 | # - unawaited_futures # https://github.com/flutter/flutter/issues/5793 153 | # - unnecessary_lambdas # https://github.com/dart-lang/linter/issues/498 154 | # - unnecessary_parenthesis 155 | # - unnecessary_statements # not yet tested 156 | # - use_setters_to_change_properties # not yet tested 157 | # - use_string_buffers # https://github.com/dart-lang/linter/pull/664 158 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 159 | # - void_checks # not yet tested 160 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # Specify analysis options. 2 | # 3 | # Until there are meta linter rules, each desired lint must be explicitly enabled. 4 | # See: https://github.com/dart-lang/linter/issues/288 5 | # 6 | # For a list of lints, see: http://dart-lang.github.io/linter/lints/ 7 | # See the configuration guide for more 8 | # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer 9 | # 10 | # There are four similar analysis options files in the flutter repos: 11 | # - analysis_options.yaml (this file) 12 | # - packages/flutter/lib/analysis_options_user.yaml 13 | # - https://github.com/flutter/plugins/blob/master/analysis_options.yaml 14 | # - https://github.com/flutter/engine/blob/master/analysis_options.yaml 15 | # 16 | # This file contains the analysis options used by Flutter tools, such as IntelliJ, 17 | # Android Studio, and the `flutter analyze` command. 18 | # 19 | # The flutter/plugins repo contains a copy of this file, which should be kept 20 | # in sync with this file. 21 | 22 | analyzer: 23 | # strong-mode: 24 | # implicit-casts: false 25 | # implicit-dynamic: false 26 | errors: 27 | # treat missing required parameters as a warning (not a hint) 28 | missing_required_param: warning 29 | # treat missing returns as a warning (not a hint) 30 | missing_return: error 31 | # allow having TODOs in the code 32 | todo: ignore 33 | exclude: 34 | - 'bin/cache/**' 35 | # the following two are relative to the stocks example and the flutter package respectively 36 | # see https://github.com/dart-lang/sdk/issues/28463 37 | - '**/i18n/string_messages_**.dart' 38 | - '/**/i18n/string_messages_**.dart' 39 | - '**/i18n/string_messages_***.dart' 40 | - '/**/i18n/string_messages_***.dart' 41 | - 'lib/src/http/**' 42 | 43 | # https://github.com/dart-lang/linter/blob/master/example/all.yaml 44 | linter: 45 | rules: 46 | - always_declare_return_types 47 | - always_require_non_null_named_parameters 48 | - annotate_overrides 49 | - avoid_empty_else 50 | - avoid_field_initializers_in_const_classes 51 | # - avoid_function_literals_in_foreach_calls 52 | - avoid_init_to_null 53 | - avoid_null_checks_in_equality_operators 54 | - avoid_relative_lib_imports 55 | - avoid_renaming_method_parameters 56 | - avoid_return_types_on_setters 57 | - avoid_slow_async_io 58 | - await_only_futures 59 | # - camel_case_types 60 | - cancel_subscriptions 61 | - control_flow_in_finally 62 | - directives_ordering 63 | - empty_catches 64 | - empty_constructor_bodies 65 | - empty_statements 66 | - hash_and_equals 67 | - implementation_imports 68 | - collection_methods_unrelated_type 69 | - library_names 70 | - library_prefixes 71 | - no_duplicate_case_values 72 | - overridden_fields 73 | - package_api_docs 74 | - package_names 75 | - package_prefixed_library_names 76 | - prefer_adjacent_string_concatenation 77 | - prefer_asserts_in_initializer_lists 78 | - prefer_collection_literals 79 | - prefer_conditional_assignment 80 | - prefer_const_constructors 81 | # - prefer_const_constructors_in_immutables 82 | - prefer_const_declarations 83 | - prefer_contains 84 | - prefer_equal_for_default_values 85 | - prefer_final_fields 86 | # - prefer_foreach 87 | - prefer_generic_function_type_aliases 88 | - prefer_initializing_formals 89 | - prefer_is_empty 90 | - prefer_is_not_empty 91 | - prefer_typing_uninitialized_variables 92 | - recursive_getters 93 | - slash_for_doc_comments 94 | - sort_unnamed_constructors_first 95 | - test_types_in_equals 96 | - throw_in_finally 97 | - type_init_formals 98 | - unnecessary_brace_in_string_interps 99 | - unnecessary_getters_setters 100 | - unnecessary_null_aware_assignments 101 | - unnecessary_null_in_if_null_operators 102 | - unnecessary_overrides 103 | - unnecessary_this 104 | - unrelated_type_equality_checks 105 | - use_rethrow_when_possible 106 | - valid_regexps 107 | # - prefer_double_quotes 108 | # - unnecessary_new ➜ Isso aqui eu queria deixar, mas o linter ainda não reconhece. Tentar novamente no futuro. 109 | # - unnecessary_const ➜ Isso aqui eu queria deixar, mas o linter ainda não reconhece. Tentar novamente no futuro. 110 | # - non_constant_identifier_names 111 | # - always_put_control_body_on_new_line 112 | # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 113 | # - always_specify_types 114 | # - avoid_annotating_with_dynamic # conflicts with always_specify_types 115 | # - avoid_as 116 | # - avoid_bool_literals_in_conditional_expressions # not yet tested 117 | # - avoid_catches_without_on_clauses # we do this commonly 118 | # - avoid_catching_errors # we do this commonly 119 | # - avoid_classes_with_only_static_members 120 | # - avoid_double_and_int_checks # only useful when targeting JS runtime 121 | # - avoid_js_rounded_ints # only useful when targeting JS runtime 122 | # - avoid_positional_boolean_parameters # not yet tested 123 | # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) 124 | # - avoid_returning_null # we do this commonly 125 | # - avoid_returning_this # https://github.com/dart-lang/linter/issues/842 126 | # - avoid_setters_without_getters # not yet tested 127 | # - avoid_single_cascade_in_expression_statements # not yet tested 128 | # - avoid_types_as_parameter_names # https://github.com/dart-lang/linter/pull/954/files 129 | # - avoid_types_on_closure_parameters # conflicts with always_specify_types 130 | # - avoid_unused_constructor_parameters # https://github.com/dart-lang/linter/pull/847 131 | # - cascade_invocations # not yet tested 132 | # - close_sinks # https://github.com/flutter/flutter/issues/5789 133 | # - comment_references # blocked on https://github.com/dart-lang/dartdoc/issues/1153 134 | # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 135 | # - invariant_booleans # https://github.com/flutter/flutter/issues/5790 136 | # - join_return_with_assignment # not yet tested 137 | # - literal_only_boolean_expressions # https://github.com/flutter/flutter/issues/5791 138 | # - no_adjacent_strings_in_list 139 | # - omit_local_variable_types # opposite of always_specify_types 140 | # - one_member_abstracts # too many false positives 141 | # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 142 | # - parameter_assignments # we do this commonly 143 | # - prefer_const_literals_to_create_immutables 144 | # - prefer_constructors_over_static_methods # not yet tested 145 | # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods 146 | # - prefer_final_locals 147 | # - prefer_function_declarations_over_variables # not yet tested 148 | # - prefer_interpolation_to_compose_strings # not yet tested 149 | # - prefer_iterable_whereType # https://github.com/dart-lang/sdk/issues/32463 150 | # - prefer_single_quotes 151 | # - sort_constructors_first 152 | # - type_annotate_public_apis # subset of always_specify_types 153 | # - unawaited_futures # https://github.com/flutter/flutter/issues/5793 154 | # - unnecessary_lambdas # https://github.com/dart-lang/linter/issues/498 155 | # - unnecessary_parenthesis 156 | # - unnecessary_statements # not yet tested 157 | # - use_setters_to_change_properties # not yet tested 158 | # - use_string_buffers # https://github.com/dart-lang/linter/pull/664 159 | # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review 160 | # - void_checks # not yet tested 161 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:themed/themed.dart'; 3 | 4 | /// This example demonstrates: 5 | /// 1) The [Themed] package is compatible with [Theme] and [ThemeData]. 6 | /// 2) We can use the `const` keyword. 7 | /// 3) An extension allows us to add a Color to a TextStyle. 8 | 9 | class MyTheme { 10 | static const color1 = ColorRef(Colors.white); 11 | static const color2 = ColorRef(Colors.blue); 12 | static const color3 = ColorRef(Colors.green); 13 | 14 | static const mainStyle = TextStyleRef( 15 | TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: MyTheme.color1), 16 | ); 17 | } 18 | 19 | Map anotherTheme = { 20 | MyTheme.color1: Colors.yellow, 21 | MyTheme.color2: Colors.pink, 22 | MyTheme.color3: Colors.purple, 23 | MyTheme.mainStyle: const TextStyle( 24 | fontSize: 22, 25 | fontWeight: FontWeight.w900, 26 | color: MyTheme.color1, 27 | ), 28 | }; 29 | 30 | Map yellowTheme = { 31 | MyTheme.color1: Colors.yellow[200]!, 32 | MyTheme.color2: Colors.yellow[600]!, 33 | MyTheme.color3: Colors.yellow[900]!, 34 | MyTheme.mainStyle: const TextStyle( 35 | fontSize: 22, 36 | fontWeight: FontWeight.w900, 37 | color: MyTheme.color1, 38 | ), 39 | }; 40 | 41 | const Widget space16 = SizedBox(width: 16, height: 16); 42 | final Widget divider = Container(width: double.infinity, height: 2, color: Colors.grey); 43 | 44 | void main() { 45 | runApp(MyApp()); 46 | } 47 | 48 | class MyApp extends StatelessWidget { 49 | @override 50 | Widget build(BuildContext context) { 51 | return Themed( 52 | child: MaterialApp( 53 | title: 'Themed example', 54 | debugShowCheckedModeBanner: false, 55 | // 56 | // 1) The [Themed] package is compatible with [Theme] and [ThemeData]: 57 | theme: ThemeData( 58 | elevatedButtonTheme: ElevatedButtonThemeData( 59 | style: ElevatedButton.styleFrom( 60 | foregroundColor: MyTheme.color1, 61 | backgroundColor: Colors.blue, 62 | ), 63 | ), 64 | ), 65 | // 66 | home: const HomePage(), 67 | ), 68 | ); 69 | } 70 | } 71 | 72 | class HomePage extends StatelessWidget { 73 | const HomePage({super.key}); 74 | 75 | @override 76 | Widget build(BuildContext context) { 77 | return Scaffold( 78 | appBar: AppBar( 79 | backgroundColor: MyTheme.color2, 80 | // 81 | // 2) We can use the `const` keyword: 82 | title: const Text('Themed example', style: MyTheme.mainStyle), 83 | ), 84 | body: const HomePageContent(), 85 | ); 86 | } 87 | } 88 | 89 | class HomePageContent extends StatelessWidget { 90 | const HomePageContent({super.key}); 91 | 92 | @override 93 | Widget build(BuildContext context) { 94 | return Center( 95 | child: Column( 96 | mainAxisSize: MainAxisSize.min, 97 | children: [ 98 | const Text( 99 | 'Click to navigate:', 100 | style: TextStyle( 101 | fontWeight: FontWeight.bold, 102 | color: MyTheme.color3, 103 | ), 104 | ), 105 | // 106 | ElevatedButton( 107 | onPressed: () { 108 | Navigator.push( 109 | context, 110 | MaterialPageRoute( 111 | builder: (context) => ColorSettingsPage(), 112 | ), 113 | ); 114 | }, 115 | child: const Text('Open Color Settings Page'), 116 | ), 117 | // 118 | ], 119 | ), 120 | ); 121 | } 122 | } 123 | 124 | class ColorSettingsPage extends StatefulWidget { 125 | @override 126 | _ColorSettingsPageState createState() => _ColorSettingsPageState(); 127 | } 128 | 129 | class _ColorSettingsPageState extends State { 130 | bool _usingStaticMethod = false; 131 | 132 | @override 133 | Widget build(BuildContext ctx) { 134 | return Scaffold( 135 | appBar: AppBar( 136 | backgroundColor: MyTheme.color2, 137 | // 138 | // 2) We can use the `const` keyword: 139 | title: const Text('Color Settings Page', style: MyTheme.mainStyle), 140 | ), 141 | body: Column( 142 | children: [ 143 | const ExampleText(), 144 | // 145 | SwitchListTile( 146 | title: _usingStaticMethod 147 | ? const Text('Themed.[static method]') 148 | : const Text('Themed.of(context)'), 149 | value: _usingStaticMethod, 150 | onChanged: (bool value) { 151 | setState(() { 152 | _usingStaticMethod = value; 153 | }); 154 | }, 155 | ), 156 | // 157 | Expanded( 158 | child: SingleChildScrollView( 159 | child: Padding( 160 | padding: const EdgeInsets.symmetric(horizontal: 16), 161 | child: 162 | _usingStaticMethod ? _usingThemedStaticMethod() : _usingThemedOf(ctx), 163 | ), 164 | ), 165 | ), 166 | ], 167 | ), 168 | ); 169 | } 170 | 171 | Column _usingThemedOf(BuildContext ctx) { 172 | return Column( 173 | mainAxisAlignment: MainAxisAlignment.center, 174 | children: [ 175 | space16, 176 | // 177 | ElevatedButton( 178 | onPressed: () => Themed.of(ctx).currentTheme = anotherTheme, 179 | child: const Text('Themed.of(ctx).currentTheme = anotherTheme'), 180 | ), 181 | // 182 | ElevatedButton( 183 | onPressed: () => Themed.of(ctx).currentTheme = null, 184 | child: const Text('Themed.of(ctx).currentTheme = null'), 185 | ), 186 | // 187 | ElevatedButton( 188 | onPressed: () => Themed.of(ctx).clearCurrentTheme(), 189 | child: const Text('Themed.of(ctx).clearCurrentTheme()'), 190 | ), 191 | // 192 | ElevatedButton( 193 | onPressed: () => Themed.of(ctx).defaultTheme = yellowTheme, 194 | child: const Text('Themed.of(ctx).defaultTheme = yellowTheme'), 195 | ), 196 | // 197 | ElevatedButton( 198 | onPressed: () => Themed.of(ctx).defaultTheme = null, 199 | child: const Text('Themed.of(ctx).defaultTheme = null'), 200 | ), 201 | // 202 | ElevatedButton( 203 | onPressed: () => Themed.of(ctx).transformColor = ColorRef.shadesOfGreyTransform, 204 | child: const Text( 205 | 'Themed.of(ctx).transformColor = ColorRef.shadesOfGreyTransform', 206 | textAlign: TextAlign.center), 207 | ), 208 | // 209 | ElevatedButton( 210 | onPressed: () => Themed.of(ctx).transformColor = null, 211 | child: const Text('Themed.of(ctx).transformColor = null'), 212 | ), 213 | // 214 | ElevatedButton( 215 | onPressed: () => Themed.of(ctx).clearTransformColor(), 216 | child: const Text('Themed.of(ctx).clearTransformColor()'), 217 | ), 218 | // 219 | ElevatedButton( 220 | onPressed: () => Themed.of(ctx).transformTextStyle = largerText, 221 | child: const Text('Themed.of(ctx).transformTextStyle = largerText', 222 | textAlign: TextAlign.center), 223 | ), 224 | // 225 | ElevatedButton( 226 | onPressed: () => Themed.of(ctx).transformTextStyle = null, 227 | child: const Text('Themed.of(ctx).transformTextStyle = null'), 228 | ), 229 | // 230 | ElevatedButton( 231 | onPressed: () => Themed.of(ctx).clearTransformTextStyle(), 232 | child: const Text('Themed.of(ctx).clearTransformTextStyle()'), 233 | ), 234 | // 235 | space16, 236 | Text( 237 | 'Themed.of(ctx).ifCurrentThemeIs({}) == ${Themed.of(ctx).ifCurrentThemeIs({})}'), 238 | space16, 239 | Text( 240 | 'Themed.of(ctx).ifCurrentThemeIs(anotherTheme) == ${Themed.of(ctx).ifCurrentThemeIs(anotherTheme)}'), 241 | space16, 242 | Text( 243 | 'Themed.of(ctx).ifCurrentThemeIs(yellowTheme) == ${Themed.of(ctx).ifCurrentThemeIs(yellowTheme)}'), 244 | space16, 245 | Text( 246 | 'Themed.of(ctx).ifCurrentTransformColorIs(null) == ${Themed.of(ctx).ifCurrentTransformColorIs(null)}'), 247 | space16, 248 | Text( 249 | 'Themed.of(ctx).ifCurrentTransformColorIs(ColorRef.shadesOfGreyTransform) == ${Themed.of(ctx).ifCurrentTransformColorIs(ColorRef.shadesOfGreyTransform)}'), 250 | space16, 251 | Text( 252 | 'Themed.of(ctx).ifCurrentTransformTextStyleIs(null) == ${Themed.of(ctx).ifCurrentTransformTextStyleIs(null)}'), 253 | space16, 254 | Text( 255 | 'Themed.of(ctx).ifCurrentTransformTextStyleIs(largerText) == ${Themed.of(ctx).ifCurrentTransformTextStyleIs(largerText)}'), 256 | space16, 257 | ], 258 | ); 259 | } 260 | 261 | Column _usingThemedStaticMethod() { 262 | return Column( 263 | mainAxisAlignment: MainAxisAlignment.center, 264 | children: [ 265 | space16, 266 | // 267 | ElevatedButton( 268 | onPressed: () => Themed.currentTheme = anotherTheme, 269 | child: const Text('Themed.currentTheme = anotherTheme'), 270 | ), 271 | // 272 | ElevatedButton( 273 | onPressed: () => Themed.currentTheme = null, 274 | child: const Text('Themed.currentTheme = null'), 275 | ), 276 | // 277 | ElevatedButton( 278 | onPressed: () => Themed.clearCurrentTheme(), 279 | child: const Text('Themed.clearCurrentTheme()'), 280 | ), 281 | // 282 | ElevatedButton( 283 | onPressed: () => Themed.defaultTheme = yellowTheme, 284 | child: const Text('Themed.defaultTheme = yellowTheme'), 285 | ), 286 | // 287 | ElevatedButton( 288 | onPressed: () => Themed.defaultTheme = null, 289 | child: const Text('Themed.defaultTheme = null'), 290 | ), 291 | // 292 | ElevatedButton( 293 | onPressed: () => Themed.transformColor = ColorRef.shadesOfGreyTransform, 294 | child: const Text('Themed.transformColor = ColorRef.shadesOfGreyTransform', 295 | textAlign: TextAlign.center), 296 | ), 297 | // 298 | ElevatedButton( 299 | onPressed: () => Themed.transformColor = null, 300 | child: const Text('Themed.transformColor = null'), 301 | ), 302 | // 303 | ElevatedButton( 304 | onPressed: () => Themed.clearTransformColor(), 305 | child: const Text('Themed.clearTransformColor()'), 306 | ), 307 | // 308 | ElevatedButton( 309 | onPressed: () => Themed.transformTextStyle = largerText, 310 | child: const Text('Themed.transformTextStyle = largerText', 311 | textAlign: TextAlign.center), 312 | ), 313 | // 314 | ElevatedButton( 315 | onPressed: () => Themed.transformTextStyle = null, 316 | child: const Text('Themed.transformTextStyle = null'), 317 | ), 318 | // 319 | ElevatedButton( 320 | onPressed: () => Themed.clearTransformTextStyle(), 321 | child: const Text('Themed.clearTransformTextStyle()'), 322 | ), 323 | // 324 | space16, 325 | Text('Themed.ifCurrentThemeIs({}) == ${Themed.ifCurrentThemeIs({})}'), 326 | space16, 327 | Text( 328 | 'Themed.ifCurrentThemeIs(anotherTheme) == ${Themed.ifCurrentThemeIs(anotherTheme)}'), 329 | space16, 330 | Text( 331 | 'Themed.ifCurrentThemeIs(yellowTheme) == ${Themed.ifCurrentThemeIs(yellowTheme)}'), 332 | space16, 333 | Text( 334 | 'Themed.ifCurrentTransformColorIs(null) == ${Themed.ifCurrentTransformColorIs(null)}'), 335 | space16, 336 | Text( 337 | 'Themed.ifCurrentTransformColorIs(ColorRef.shadesOfGreyTransform) == ${Themed.ifCurrentTransformColorIs(ColorRef.shadesOfGreyTransform)}'), 338 | space16, 339 | Text( 340 | 'Themed.ifCurrentTransformTextStyleIs(null) == ${Themed.ifCurrentTransformTextStyleIs(null)}'), 341 | space16, 342 | Text( 343 | 'Themed.ifCurrentTransformTextStyleIs(largerText) == ${Themed.ifCurrentTransformTextStyleIs(largerText)}'), 344 | space16, 345 | ], 346 | ); 347 | } 348 | 349 | static TextStyle largerText(TextStyle textStyle) => 350 | textStyle.copyWith(fontSize: textStyle.fontSize! * 1.5); 351 | } 352 | 353 | class ExampleText extends StatelessWidget { 354 | const ExampleText({super.key}); 355 | 356 | @override 357 | Widget build(BuildContext context) { 358 | return Container( 359 | color: Colors.grey[300], 360 | child: ConstrainedBox( 361 | constraints: const BoxConstraints(maxHeight: 135), 362 | child: Column( 363 | mainAxisSize: MainAxisSize.min, 364 | mainAxisAlignment: MainAxisAlignment.center, 365 | children: [ 366 | const Spacer(), 367 | Column( 368 | children: [ 369 | Container( 370 | color: MyTheme.color3, 371 | child: const Text( 372 | 'This is some text!', 373 | style: MyTheme.mainStyle, 374 | ), 375 | ), 376 | space16, 377 | Container( 378 | color: MyTheme.color3, 379 | child: Text( 380 | 'This is another text!', 381 | // 3) An extension allows us to add a Color to a TextStyle: 382 | style: MyTheme.mainStyle + Colors.black, 383 | ), 384 | ), 385 | ], 386 | ), 387 | const Spacer(), 388 | divider, 389 | ], 390 | ), 391 | ), 392 | ); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Pub popularity](https://badgen.net/pub/popularity/themed)](https://pub.dev/packages/themed) 2 | [![Pub Version](https://img.shields.io/pub/v/themed?style=flat-square&logo=dart)](https://pub.dev/packages/themed) 3 | [![GitHub stars](https://img.shields.io/github/stars/marcglasberg/themed?style=social)](https://github.com/marcglasberg/themed) 4 | ![Code Climate issues](https://img.shields.io/github/issues/marcglasberg/themed?style=flat-square) 5 | ![GitHub closed issues](https://img.shields.io/github/issues-closed/marcglasberg/themed?style=flat-square) 6 | ![GitHub contributors](https://img.shields.io/github/contributors/marcglasberg/themed?style=flat-square) 7 | ![GitHub repo size](https://img.shields.io/github/repo-size/marcglasberg/themed?style=flat-square) 8 | ![GitHub forks](https://img.shields.io/github/forks/marcglasberg/themed?style=flat-square) 9 | ![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square) 10 | [![Developed by Marcelo Glasberg](https://img.shields.io/badge/Developed%20by%20Marcelo%20Glasberg-blue.svg)](https://glasberg.dev/) 11 | [![Glasberg.dev on pub.dev](https://img.shields.io/pub/publisher/async_redux.svg)](https://pub.dev/publishers/glasberg.dev/packages) 12 | [![Platforms](https://badgen.net/pub/flutter-platform/themed)](https://pub.dev/packages/themed) 13 | 14 | #### Sponsor 15 | 16 | [![](./example/SponsoredByMyTextAi.png)](https://mytext.ai) 17 | 18 | # themed 19 | 20 | The **Themed** package: 21 | 22 | * Lets you define a theme with **const** values, but change them dynamically anyway. 23 | * `ChangeColors` widget to change the brightness, saturation and hue of widgets or images. 24 | * Color extension methods like `decolorize`, `addOpacity`, `removeOpacity`, 25 | `darker`, `lighter`, `average` and more. 26 | * Static methods `ColorUtil.rgbaToArgb` and `ColorUtil.abgrToArgb`. 27 | * TextStyle extension methods: `var myStyle = TextStyle(fontSize: 15) + Colors.blue` and 28 | more. 29 | 30 | ## Const values that you can change 31 | 32 | As we all know, using const variables is the easiest way to create and use themes: 33 | 34 | ``` 35 | static const myColor = Colors.white; 36 | static const myStyle = TextStyle(fontSize: 16); 37 | 38 | Container( 39 | color: myColor, 40 | child: const Text('Hi', style: myStyle))) 41 | ``` 42 | 43 | However, if you do it like that you can't later change the theme dynamically. 44 | By using the **Themed** package you can: 45 | 46 | ``` 47 | static const myColor = ColorRef(Colors.white); 48 | static const myStyle = TextStyleRef(TextStyle(fontSize: 16)); 49 | 50 | Container( 51 | color: myColor, 52 | child: const Text('Hello', style: myStyle))) 53 | 54 | // Later, change the theme dynamically. 55 | Themed.currentTheme = { 56 | myColor: Colors.blue, 57 | myStyle: TextStyle(fontSize: 20); 58 | } 59 | ``` 60 | 61 | ![](https://raw.githubusercontent.com/marcglasberg/themed/master/example/lib/images/themed.png) 62 | 63 | There is no need to use `Theme.of(context)` anymore: 64 | 65 | ``` 66 | // So old-fashioned. 67 | Container( 68 | color: Theme.of(context).primary, 69 | child: Text('Hello', style: TextStyle(color: Theme.of(context).secondary))) 70 | ``` 71 | 72 | Also, since `Theme.of` needs the `context` and is not constant, you can't use it in 73 | constructors. However, the *Themed* package has no such limitations: 74 | 75 | ``` 76 | // The const color is the default value of an optional parameter. 77 | MyWidget({ 78 | this.color = myColor, 79 | }); 80 | ``` 81 | 82 | --- 83 | 84 | # Setup 85 | 86 | Wrap your widget tree with the `Themed` widget, above the `MaterialApp`: 87 | 88 | ``` 89 | @override 90 | Widget build(BuildContext context) { 91 | return Themed( 92 | child: MaterialApp( 93 | ... 94 | ``` 95 | 96 | # Compatibility 97 | 98 | The *Themed* package is a competitor to writing `Theme.of(context).xxx` in your build 99 | methods, but it’s *NOT* a competitor to Flutter’s native theme system and the `Theme` 100 | widget. It’s there to solve a different problem, and it’s usually used together with 101 | the `Theme` widget. For example, if you want to set a global default color for all 102 | buttons, you’ll use the `Theme` widget. You may use it together with the `Themed` 103 | package however, meaning that `Themed` colors and styles may be used inside 104 | a `ThemeData` widget: 105 | 106 | ``` 107 | static const myColor1 = ColorRef(Colors.red); 108 | static const myColor2 = ColorRef(Colors.blue); 109 | ... 110 | 111 | child: MaterialApp( 112 | theme: ThemeData( 113 | primaryColor: MyTheme.color2, 114 | elevatedButtonTheme: 115 | ElevatedButtonThemeData( 116 | style: ElevatedButton.styleFrom(primary: MyTheme.color2), 117 | ), 118 | ), 119 | ``` 120 | 121 | ## How to define a theme map 122 | 123 | Each theme should be a `Map`, where the **keys** are your `ColorRef` 124 | and `TextStyleRef` const values, and the **values** are the colors and styles you want to 125 | use on that theme. For example: 126 | 127 | ``` 128 | Map theme1 = { 129 | MyTheme.color1: Colors.yellow, 130 | MyTheme.color2: Colors.pink, 131 | MyTheme.color3: Colors.purple, 132 | MyTheme.mainStyle: const TextStyle(fontSize: 22, fontWeight: FontWeight.w900, color: MyTheme.color1), 133 | }; 134 | ``` 135 | 136 | At any point in your app you can just change the current theme by doing: 137 | 138 | ``` 139 | // Setting a theme: 140 | Themed.currentTheme = theme1; 141 | 142 | // Setting another theme: 143 | Themed.currentTheme = theme2; 144 | 145 | // Removing the current theme (and falling back to the default theme): 146 | Themed.clearCurrentTheme(); 147 | 148 | // This would also remove the current theme: 149 | Themed.currentTheme = null; 150 | ``` 151 | 152 | ### Resetting 153 | 154 | Calling the static method `Themed.reset()` will remove the entire widget tree inside 155 | the `Themed` widget for one frame, and then restore it, rebuilding everything. This can 156 | be helpful when some widgets are not responding to theme changes. Usage of this method 157 | is not usually necessary. A side effect is that the all stateful widgets below `Themed` 158 | will be recreated, and you'll need to have mechanisms to recover their state, like for 159 | example having the state come from above `Themed` widget, or using a proper state 160 | management solution. 161 | 162 | # Organization 163 | 164 | You can also organize your theme in a class: 165 | 166 | ``` 167 | class MyTheme { 168 | static const myColor = ColorRef(Colors.white); 169 | static const myStyle = TextStyleRef(TextStyle(fontSize: 16, color: Colors.red)); 170 | } 171 | 172 | Container( 173 | color: MyTheme.myColor, 174 | child: const Text('Hello', style: MyTheme.myStyle))) 175 | ``` 176 | 177 | # Color transform 178 | 179 | Instead of changing the current theme you can create a **color transformation**. 180 | For example, this will turn your theme into shades of grey: 181 | 182 | ``` 183 | static Color shadesOfGreyTransform(Color color) { 184 | int average = (color.red + color.green + color.blue) ~/ 3; 185 | return Color.fromARGB(color.alpha, average, average, average); 186 | } 187 | ``` 188 | 189 | Note you can create your own function to process colors, but `shadesOfGreyTransform` is 190 | already provided: 191 | 192 | ``` 193 | // Turn it on: 194 | Themed.transformColor = ColorRef.shadesOfGreyTransform; 195 | 196 | // Then, later, turn it off: 197 | Themed.clearTransformColor(); 198 | ``` 199 | 200 | # Changing brightness, saturation and hue of widgets or images. 201 | 202 | Use the provided `ChangeColors` widget to change the brightness, saturation and hue of any 203 | widget, including images. Example: 204 | 205 | ``` 206 | ChangeColors( 207 | hue: 0.55, 208 | brightness: 0.2, 209 | saturation: 0.1, 210 | child: Image.asset('myImage.png'), 211 | ); 212 | ``` 213 | 214 | To achieve a greyscale effect, you may also use the `ChangeColors.greyscale` constructor. 215 | 216 | _Note: This widget is based upon 217 | 218 | this code (from 219 | BananaNeil's), which is in turn based 220 | upon 221 | this code (by 222 | Richard Lalancette)._ 223 | 224 | # Color extension 225 | 226 | The `lighter` method makes the color lighter (more white). Example: 227 | 228 | ``` 229 | // 20% more white. 230 | Colors.blue.lighter(0.2); 231 | ``` 232 | 233 | The `darker` method makes the color darker (more black). Example: 234 | 235 | ``` 236 | // 20% more black. 237 | Colors.blue.darker(0.2); 238 | ``` 239 | 240 | The `average` method makes the current color more similar to the given `color`. Example: 241 | 242 | ``` 243 | // 50% blue and 50% red. 244 | Colors.blue.average(Colors.red); 245 | 246 | // 20% blue and 80% red. 247 | Colors.blue.average(Colors.red, 0.8); 248 | ``` 249 | 250 | The `decolorize` method makes the current color more grey. Example: 251 | 252 | ``` 253 | // Grey, with luminance similar to the original blue. 254 | Colors.blue.decolorize(); 255 | 256 | // Blue with 20% less color. 257 | Colors.blue.decolorize(0.2); 258 | ``` 259 | 260 | The `addOpacity` method makes the current color more transparent than it already is, by 261 | the given amount. The `removeOpacity` method makes the current color less transparent than 262 | it already is, by the given amount. This is different from the `withOpacity` method, 263 | as you can see below. 264 | 265 | ``` 266 | // 50% transparent blue. 267 | Colors.blue.addOpacity(0.5); 268 | 269 | // 80% transparent black. 270 | Colors.transparent.removeOpacity(0.2); 271 | 272 | // Also 50% transparent blue. 273 | Colors.withOpacity(0.5); 274 | 275 | // 75% transparent blue, because we add 50% and then more 50%. 276 | Colors.blue.addOpacity(0.5).addOpacity(0.5); 277 | 278 | // This is 50% transparent blue, because the opacity is replaced, not added. 279 | Colors.withOpacity(0.5).withOpacity(0.5); 280 | ``` 281 | 282 | There are also two static methods for advanced color representation conversion: 283 | The `ColorUtil.rgbaToArgb` method converts the RGBA color representation to ARGB. 284 | The `ColorUtil.abgrToArgb` method converts the ABGR color representation to ARGB. 285 | 286 | # TextStyle transform 287 | 288 | You can also create a **style transformation**. For example, this will make your fonts 289 | larger: 290 | 291 | ``` 292 | static TextStyle largerText(TextStyle textStyle) => 293 | textStyle.copyWith(fontSize: textStyle.fontSize! * 1.5); 294 | 295 | // Turn it on: 296 | Themed.transformTextStyle = largerText; 297 | 298 | // Then, later, turn it off: 299 | Themed.clearTransformTextStyle(); 300 | ``` 301 | 302 | # TextStyle extension 303 | 304 | With the provided extension, you can make your code more clean-code by creating new text 305 | styles by adding colors and other values to a `TextStyle`. For example: 306 | 307 | ``` 308 | const myStyle = TextStyle(...); 309 | 310 | // Using some style: 311 | Text('Hello', style: myStyle); 312 | 313 | // Making text black: 314 | Text('Hello', style: myStyle + Colors.black); 315 | 316 | // Changing some other stuff: 317 | Text('Hello', style: myStyle + FontWeight.w900 + FontSize(20.0) + TextHeight(1.2)); 318 | ``` 319 | 320 | # Beware not to define the same constant 321 | 322 | Please remember Dart constants point to the same memory space. 323 | In this example, `colorA`, `colorB` and `colorC` represent the same variable: 324 | 325 | ``` 326 | class MyTheme { 327 | static const colorA = ColorRef(Colors.white); 328 | static const colorB = ColorRef(Colors.white); 329 | static const colorC = colorA; 330 | } 331 | ``` 332 | 333 | If you later change the color of `colorA`, you are also automatically changing the color 334 | of `colorB` and `colorB`. 335 | 336 | If you want to create 3 independent colors, and be able to change them independently, you 337 | have to create different constants. You can provide an `id` string, just to 338 | differentiate them. For example: 339 | 340 | ``` 341 | class MyTheme { 342 | static const colorA = ColorRef(Colors.white, id:'A'); 343 | static const colorB = ColorRef(Colors.white, id:'B'); 344 | static const colorB = ColorRef(colorA, id:'C'); 345 | } 346 | ``` 347 | 348 | # Avoid circular dependencies 349 | 350 | The following will lead to a `StackOverflowError` error: 351 | 352 | ``` 353 | Map anotherTheme = { 354 | MyTheme.color1: MyTheme.color2, 355 | MyTheme.color2: MyTheme.color1, 356 | }; 357 | ``` 358 | 359 | You can have references which depend on other references, no problem. But both direct and 360 | indirect circular references must be avoided. 361 | 362 | # Other ways to use it 363 | 364 | If you want, you may also define a **default** theme, and a **current** theme for your 365 | app: 366 | 367 | ``` 368 | @override 369 | Widget build(BuildContext context) { 370 | return Themed( 371 | defaultTheme: { ... }, 372 | currentTheme: { ... }, 373 | child: MaterialApp( 374 | ... 375 | ``` 376 | 377 | The `defaultTheme` and `currentTheme` are both optional. They are simply theme maps, as 378 | explained below. 379 | 380 | When a color/style is used, it will first search it inside the `currentTheme`. 381 | 382 | If it's not found there, it searches inside of `defaultTheme`. 383 | 384 | If it's still not found there, it uses the default color/style which was defined in the 385 | constructor. For example, here the default color is white: `ColorRef(Colors.white)`. 386 | 387 | Please note: If you define all your colors in the `defaultTheme`, then you don't need to 388 | provide default values in the constructor. You can then use the `fromId` constructor: 389 | 390 | ``` 391 | class MyTheme { 392 | static const color1 = ColorRef.fromId('c1'); 393 | static const color2 = ColorRef.fromId('c2'); 394 | static const color3 = ColorRef.fromId('c3'); 395 | static const mainStyle = TextStyleRef.fromId('mainStyle'); 396 | } 397 | ``` 398 | 399 | ## Saving and setting Themes by key 400 | 401 | You can save themes with keys, and then later use the keys to set the theme. The keys can 402 | be anything (Strings, enums etc.): 403 | 404 | ``` 405 | // Save some themes using keys. 406 | enum Keys {light, dark}; 407 | Themed.save(key: Keys.light, theme: { ... }) 408 | Themed.save(key: Keys.dark, theme: { ... }) 409 | 410 | // Then set the theme. 411 | Themed.setThemeByKey(Keys.light); 412 | ``` 413 | 414 | It also works the other way around: 415 | If you use the key first, and only save a theme with that key later: 416 | 417 | ``` 418 | // Set the theme with a key, even before saving the theme. 419 | Themed.setThemeByKey(Keys.light); 420 | 421 | // The theme will change as soon as you save a theme with that key. 422 | Themed.save(key: Keys.light, theme: { ... }) 423 | ``` 424 | 425 | Note: You can also use the methods `saveAll` to save many themes by key at the same time, 426 | and `clearSavedThemeByKey` to remove saved themes. 427 | 428 | Important: When I say "save" above, I mean it's saved in memory, not in the device disk. 429 | 430 | --- 431 | 432 | # Copyright 433 | 434 | **This package is copyrighted and brought to you 435 | by 436 | Parkside Technologies, a company which is simplifying global access to US stocks.** 437 | 438 | This package is published here with permission. 439 | 440 | Please, see the license page for more information. 441 | 442 | *** 443 | 444 | ## By Marcelo Glasberg 445 | 446 | _glasberg.dev_ 447 |
448 | _github.com/marcglasberg_ 449 |
450 | _linkedin.com/in/marcglasberg/_ 451 |
452 | _twitter.com/glasbergmarcelo_ 453 |
454 | 455 | _stackoverflow.com/users/3411681/marcg_ 456 |
457 | _medium.com/@marcglasberg_ 458 |
459 | 460 | *My article in the official Flutter documentation*: 461 | 462 | * Understanding 463 | constraints 464 | 465 | *The Flutter packages I've authored:* 466 | 467 | * async_redux 468 | * provider_for_redux 469 | * i18n_extension 470 | * align_positioned 471 | * network_to_file_image 472 | * image_pixels 473 | * matrix4_transform 474 | * back_button_interceptor 475 | * indexed_list_view 476 | * animated_size_and_fade 477 | * assorted_layout_widgets 478 | * weak_map 479 | * themed 480 | * bdd_framework 481 | * 482 | tiktoken_tokenizer_gpt4o_o1 483 | 484 | *My Medium Articles:* 485 | 486 | * 487 | Async Redux: Flutter’s non-boilerplate version of Redux 488 | (versions: 489 | Português) 490 | * 491 | i18n_extension 492 | (versions: 493 | Português) 494 | * 495 | Flutter: The Advanced Layout Rule Even Beginners Must Know 496 | (versions: русский) 497 | * 498 | The New Way to create Themes in your Flutter App 499 | 500 | [![](./example/SponsoredByMyTextAi.png)](https://mytext.ai) 501 | -------------------------------------------------------------------------------- /lib/src/themed.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use 2 | 3 | import 'dart:ui' as ui 4 | show 5 | ParagraphStyle, 6 | TextStyle, 7 | Shadow, 8 | FontFeature, 9 | TextHeightBehavior, 10 | TextLeadingDistribution, 11 | FontVariation; 12 | 13 | import 'package:flutter/cupertino.dart'; 14 | import 'package:flutter/foundation.dart'; 15 | import 'package:flutter/material.dart'; 16 | import 'package:themed/src/const_theme_exception.dart'; 17 | 18 | abstract class ThemeRef {} 19 | 20 | /// You must have a single [Themed] widget in your widget tree, above 21 | /// your [MaterialApp] or [CupertinoApp] widgets: 22 | /// 23 | /// ```dart 24 | /// import 'package:themed/themed.dart'; 25 | /// 26 | /// Widget build(BuildContext context) { 27 | /// return Themed( 28 | /// child: MaterialApp(...) 29 | /// ); 30 | /// } 31 | /// ``` 32 | /// 33 | /// You may, or may not, also provide a [defaultTheme] and a [currentTheme]: 34 | /// 35 | /// ```dart 36 | /// Widget build(BuildContext context) { 37 | /// return Themed( 38 | /// defaultTheme: { ... }, 39 | /// currentTheme: { ... }, 40 | /// child: 41 | /// ); 42 | /// } 43 | /// ``` 44 | /// 45 | /// # Usage 46 | /// 47 | /// Instead of: 48 | /// 49 | /// Container( 50 | /// color: Theme.of(context).warningColor, 51 | /// child: Text("hello!", style: Theme.of(context).titleTextStyle, 52 | /// ); 53 | /// 54 | /// You can write: 55 | /// 56 | /// Container( 57 | /// color: const AppColor.warning, 58 | /// child: Text("hello!", style: const AppStyle.title, 59 | /// ); 60 | /// 61 | class Themed extends StatefulWidget { 62 | // 63 | static final _themedKey = GlobalKey<_ThemedState>(); 64 | 65 | final Widget child; 66 | 67 | /// Saved themes. 68 | static final Map> _saved = {}; 69 | static Object? _delayedThemeChangeByKey; 70 | 71 | /// You must have a single [Themed] widget in your widget tree, above 72 | /// your [MaterialApp] or [CupertinoApp] widgets: 73 | /// 74 | /// ```dart 75 | /// import 'package:themed/themed.dart'; 76 | /// 77 | /// Widget build(BuildContext context) { 78 | /// return Themed( 79 | /// child: MaterialApp(...) 80 | /// ); 81 | /// } 82 | /// ``` 83 | /// 84 | /// You may, or may not, also provide a [defaultTheme] and a [currentTheme]: 85 | /// 86 | /// ```dart 87 | /// Widget build(BuildContext context) { 88 | /// return Themed( 89 | /// defaultTheme: { ... }, 90 | /// currentTheme: { ... }, 91 | /// child: ... 92 | /// ); 93 | /// } 94 | /// ``` 95 | /// 96 | /// # Usage 97 | /// 98 | /// Instead of: 99 | /// 100 | /// Container( 101 | /// color: Theme.of(context).warningColor, 102 | /// child: Text("hello!", style: Theme.of(context).titleTextStyle, 103 | /// ); 104 | /// 105 | /// You can write: 106 | /// 107 | /// Container( 108 | /// color: const AppColor.warning, 109 | /// child: Text("hello!", style: const AppStyle.title, 110 | /// ); 111 | /// 112 | Themed({ 113 | Map? defaultTheme, 114 | Map? currentTheme, 115 | required this.child, 116 | }) : super(key: _themedKey) { 117 | if (defaultTheme != null) _defaultTheme = _toIdenticalKeyedMap(defaultTheme); 118 | if (currentTheme != null) _currentTheme = _toIdenticalKeyedMap(currentTheme); 119 | } 120 | 121 | /// Calling [Themed.reset] will remove the entire widget tree inside [Themed] for one 122 | /// frame, and then restore it, rebuilding everything. This can be helpful when some 123 | /// widgets are not responding to theme changes. This is a brute-force method, and it's 124 | /// not necessary in all cases. 125 | /// A side effect is that the all stateful widgets below [Themed] will be recreated, 126 | /// and their state reset by calling their `initState()` method. This means you should 127 | /// only use this method if you don't mind loosing all this state, or if you have 128 | /// mechanisms to recover it, like for example having the state come from above 129 | /// the [Themed] widget. 130 | static void reset() { 131 | var homepageState = _themedKey.currentState; 132 | homepageState?._reset(); 133 | } 134 | 135 | static void _setState() { 136 | _themedKey.currentState?.setState(() {}); // ignore: invalid_use_of_protected_member 137 | 138 | var context = _themedKey.currentContext; 139 | if (context != null) _rebuildAllChildrenOfContext(context); 140 | } 141 | 142 | /// Same as `Themed.of(context).currentTheme = { ... };` 143 | /// 144 | /// The current theme overrides the default theme. If a color is present in the current theme, it 145 | /// will be used. If not, the color from the default theme will be used instead. For this reason, 146 | /// the current theme doesn't need to have all the colors, but only the ones you want to change 147 | /// from the default. 148 | static set currentTheme(Map? currentTheme) { 149 | _delayedThemeChangeByKey = null; 150 | _currentTheme = _toIdenticalKeyedMap(currentTheme); 151 | _setState(); 152 | } 153 | 154 | /// Same as `Themed.of(context).clearTheme();` 155 | /// 156 | /// Removes the current theme, falling back to the default theme 157 | static void clearCurrentTheme() { 158 | _clearCurrentTheme(); 159 | _setState(); 160 | } 161 | 162 | /// Same as `Themed.of(context).defaultTheme = { ... };` 163 | /// 164 | /// The default theme must define all used colors. 165 | static set defaultTheme(Map? defaultTheme) { 166 | _defaultTheme = _toIdenticalKeyedMap(defaultTheme); 167 | _setState(); 168 | } 169 | 170 | /// Saves a [theme] with a [key]. 171 | /// See [setThemeByKey] to understand how to use the saved theme. 172 | /// 173 | static void save({required Object key, required Map theme}) { 174 | _saved[key] = theme; 175 | if (_delayedThemeChangeByKey == key) { 176 | _delayedThemeChangeByKey = null; 177 | currentTheme = theme; 178 | } 179 | } 180 | 181 | /// Saves a map of themes by key. Example: 182 | /// 183 | /// ``` 184 | /// final Map> myThemes = { 185 | /// Keys.light: lightTheme, 186 | /// Keys.dark: darkTheme, 187 | /// }; 188 | /// 189 | /// Themed.saveAll(myThemes); 190 | /// ``` 191 | /// 192 | /// See [setThemeByKey] to understand how to use the saved themes. 193 | /// 194 | static void saveAll(Map> themesByKey) { 195 | themesByKey.forEach((key, value) => Themed.save(key: key, theme: value)); 196 | } 197 | 198 | /// If you call [setThemeByKey] with a [key], and a theme was previously saved with that [key] 199 | /// (by using the [save] method), then the current theme will immediately change into that theme. 200 | /// 201 | /// However, if a theme was NOT previously saved with that [key], then the current theme will 202 | /// not change immediately, but may change later, as soon as you save a theme with that [key]. 203 | /// 204 | /// In other words, you can first save a them then set it, 205 | /// or you can first set it and then save it. 206 | /// 207 | static void setThemeByKey(Object key) { 208 | var theme = _saved[key]; 209 | if (theme == null) 210 | _delayedThemeChangeByKey = key; 211 | else { 212 | _delayedThemeChangeByKey = null; 213 | currentTheme = _saved[key]; 214 | } 215 | } 216 | 217 | /// Removes all saved themes. 218 | /// Or, to remove just a specific saved theme, pass its [key]. 219 | /// Note: This will not change the current theme. 220 | static void clearSavedThemeByKey([Object? key]) { 221 | if (key != null) { 222 | _saved.remove(key); 223 | if (_delayedThemeChangeByKey == key) _delayedThemeChangeByKey = null; 224 | } else { 225 | _delayedThemeChangeByKey = null; 226 | _saved.clear(); 227 | } 228 | } 229 | 230 | /// Sets a transform which will be applied to all colors. 231 | /// 232 | /// Same as `Themed.of(context).transformColor = ...;` 233 | /// 234 | static set transformColor(Color Function(Color)? transform) { 235 | _setTransformColor(transform); 236 | _setState(); 237 | } 238 | 239 | /// Same as `Themed.of(context).clearTransformColor();` 240 | /// 241 | /// Removes the current color transform. 242 | static void clearTransformColor() { 243 | transformColor = null; 244 | } 245 | 246 | /// Same as `Themed.of(context).transformTextStyle = ...;` 247 | /// 248 | /// Sets a transform which will be applied to all text styles. For example: 249 | /// 250 | /// ``` 251 | /// Themed.transformTextStyle = (TextStyle style) => 252 | /// (style.fontSize == null) 253 | /// ? style 254 | /// : style.copyWith(fontSize: style.fontSize! * 1.20); 255 | /// ``` 256 | static set transformTextStyle(TextStyle Function(TextStyle)? transform) { 257 | _setTransformTextStyle(transform); 258 | _setState(); 259 | } 260 | 261 | /// Same as `Themed.of(context).clearTransformTextStyle();` 262 | /// 263 | /// Removes the current text style transform. 264 | static void clearTransformTextStyle() { 265 | transformTextStyle = null; 266 | } 267 | 268 | /// Returns true if the given theme is equal to the current one. 269 | /// Note: To check if the default them is being used, do: `ifThemeIs({})`. 270 | static bool ifCurrentThemeIs(Map theme) => _ifCurrentThemeIs(theme); 271 | 272 | /// Same as `Themed.ifCurrentTransformColorIs(...)`. 273 | /// 274 | /// Returns true if the given color transform is equal to the current one. 275 | static bool ifCurrentTransformColorIs(Color Function(Color)? transform) => 276 | _ifCurrentTransformColorIs(transform); 277 | 278 | /// Same as `Themed.ifCurrentTransformTextStyleIs(...)`. 279 | /// 280 | /// Returns true if the given text style transform is equal to the current one. 281 | static bool ifCurrentTransformTextStyleIs(TextStyle Function(TextStyle)? transform) => 282 | _ifCurrentTransformTextStyleIs(transform); 283 | 284 | /// Getter: 285 | /// print(Themed.of(context).theme); 286 | /// 287 | /// Setter: 288 | /// Themed.of(context).theme = Locale("en", "US"); 289 | /// 290 | static _ThemedState of(BuildContext context) { 291 | _InheritedConstTheme? inherited = 292 | context.dependOnInheritedWidgetOfExactType<_InheritedConstTheme>(); 293 | 294 | if (inherited == null) 295 | throw ConstThemeException("Can't find the `Themed` widget up in the tree. " 296 | "Please make sure to wrap some ancestor widget with `Themed`."); 297 | 298 | return inherited.data; 299 | } 300 | 301 | @override 302 | _ThemedState createState() => _ThemedState(); 303 | } 304 | 305 | class _ThemedState extends State { 306 | // 307 | /// To change the current theme: 308 | /// 309 | /// Themed.of(context).currentTheme = { ... }; 310 | /// 311 | /// Same as `Themed.currentTheme = { ... }`. 312 | /// 313 | /// The current theme overrides the default theme. If a color is present in the current theme, it 314 | /// will be used. If not, the color from the default theme will be used instead. For this reason, 315 | /// the current theme doesn't need to have all the colors, but only the ones you want to change 316 | /// from the default. 317 | /// 318 | set currentTheme(Map? currentTheme) { 319 | Themed._delayedThemeChangeByKey = null; 320 | if (mounted) 321 | setState(() { 322 | _currentTheme = _toIdenticalKeyedMap(currentTheme); 323 | _rebuildAllChildren(); 324 | }); 325 | } 326 | 327 | /// Removes the current theme, falling back to the default theme. 328 | /// 329 | /// Themed.of(context).clearCurrentTheme() 330 | /// 331 | /// Same as `Themed.clearCurrentTheme();`. 332 | /// 333 | void clearCurrentTheme() { 334 | if (mounted) 335 | setState(() { 336 | _clearCurrentTheme(); 337 | _rebuildAllChildren(); 338 | }); 339 | } 340 | 341 | /// To set the default theme: 342 | /// 343 | /// Themed.of(context).defaultTheme = { ... }; 344 | /// 345 | /// Same as `Themed.defaultTheme = { ... }`. 346 | /// 347 | /// Note the default theme MUST define all used colors. 348 | /// 349 | set defaultTheme(Map? defaultTheme) { 350 | if (mounted) 351 | setState(() { 352 | _defaultTheme = _toIdenticalKeyedMap(defaultTheme); 353 | _rebuildAllChildren(); 354 | }); 355 | } 356 | 357 | /// Sets a transform which will be applied to all colors: 358 | /// 359 | /// Themed.of(context).transformColor = ... 360 | /// 361 | /// Same as `Themed.transformColor = ...`. 362 | /// 363 | set transformColor(Color Function(Color)? transform) { 364 | if (mounted) 365 | setState(() { 366 | _setTransformColor(transform); 367 | _rebuildAllChildren(); 368 | }); 369 | } 370 | 371 | void clearTransformColor() { 372 | transformColor = null; 373 | } 374 | 375 | /// Sets a transform which will be applied to all text styles: 376 | /// 377 | /// Themed.of(context).transformTextStyle = ... 378 | /// 379 | /// Same as `Themed.transformTextStyle = ...`. 380 | /// 381 | set transformTextStyle(TextStyle Function(TextStyle)? transform) { 382 | if (mounted) 383 | setState(() { 384 | _setTransformTextStyle(transform); 385 | _rebuildAllChildren(); 386 | }); 387 | } 388 | 389 | void clearTransformTextStyle() { 390 | transformTextStyle = null; 391 | } 392 | 393 | /// Returns true if the given theme is equal to the current one. 394 | /// Note: To check if the default them is being used, do: `ifThemeIs({})`. 395 | /// 396 | /// Themed.of(context).ifCurrentThemeIs({...}) 397 | /// 398 | /// Same as `Themed.ifCurrentThemeIs(...)`. 399 | /// 400 | bool ifCurrentThemeIs(Map theme) => _ifCurrentThemeIs(theme); 401 | 402 | /// Returns true if the given color transform is equal to the current one. 403 | /// 404 | /// Themed.of(context).ifCurrentTransformColorIs(...) 405 | /// 406 | /// Same as `Themed.ifCurrentTransformColorIs(...)`. 407 | /// 408 | bool ifCurrentTransformColorIs(Color Function(Color)? transform) => 409 | _ifCurrentTransformColorIs(transform); 410 | 411 | /// Returns true if the given text style transform is equal to the current one. 412 | /// 413 | /// Themed.of(context).ifCurrentTransformTextStyleIs(...) 414 | /// 415 | /// Same as `Themed.ifCurrentTransformTextStyleIs(...)`. 416 | /// 417 | bool ifCurrentTransformTextStyleIs(TextStyle Function(TextStyle)? transform) => 418 | _ifCurrentTransformTextStyleIs(transform); 419 | 420 | bool _isResetting = false; 421 | 422 | void _reset() { 423 | if (mounted) { 424 | setState(() { 425 | _isResetting = true; 426 | WidgetsBinding.instance.addPostFrameCallback((_) { 427 | if (mounted) setState(() => _isResetting = false); 428 | }); 429 | }); 430 | } 431 | } 432 | 433 | @override 434 | Widget build(BuildContext context) { 435 | if (_isResetting) { 436 | return const SizedBox(); 437 | } else { 438 | return _InheritedConstTheme( 439 | data: this, 440 | child: widget.child, 441 | ); 442 | } 443 | } 444 | 445 | /// See: https://stackoverflow.com/a/58513635/3411681 446 | void _rebuildAllChildren() { 447 | _rebuildAllChildrenOfContext(context); 448 | } 449 | } 450 | 451 | class _InheritedConstTheme extends InheritedWidget { 452 | // 453 | final _ThemedState data; 454 | 455 | _InheritedConstTheme({ 456 | Key? key, 457 | required this.data, 458 | required Widget child, 459 | }) : super(key: key, child: child); 460 | 461 | @override 462 | bool updateShouldNotify(_InheritedConstTheme old) => true; 463 | } 464 | 465 | class ColorRef extends Color implements ThemeRef { 466 | // 467 | final String? id; 468 | 469 | final Color? defaultColor; 470 | 471 | const ColorRef(this.defaultColor, {this.id}) : super(0); 472 | 473 | const ColorRef.fromId(this.id) 474 | : defaultColor = null, 475 | super(0); 476 | 477 | /// Transform that removes the colors, leaving only shades of gray. 478 | /// Use it like this: `Themed.setTransform(ColorRef.shadesOfGrey);` 479 | static Color shadesOfGreyTransform(Color color) { 480 | int average = (color.red + color.green + color.blue) ~/ 3; 481 | return Color.fromARGB(color.alpha, average, average, average); 482 | } 483 | 484 | Color get color => Color(value); 485 | 486 | /// A 32 bit value representing this color. 487 | /// 488 | /// The bits are assigned as follows: 489 | /// 490 | /// * Bits 24-31 are the alpha value. 491 | /// * Bits 16-23 are the red value. 492 | /// * Bits 8-15 are the green value. 493 | /// * Bits 0-7 are the blue value. 494 | @Deprecated('Use component accessors like .r or .g.') 495 | @override 496 | int get value { 497 | Color? result = _currentTheme[this] as Color?; 498 | result ??= _defaultTheme[this] as Color?; 499 | result ??= defaultColor; 500 | if (result == null) throw ConstThemeException('Theme color "$id" is not defined.'); 501 | if (_transformColor != null) result = _transformColor!(result); 502 | return result.value; 503 | } 504 | 505 | /// The alpha channel of this color. 506 | /// 507 | /// A value of 0.0 means this color is fully transparent. A value of 1.0 means 508 | /// this color is fully opaque. 509 | @override 510 | double get a => ((value >> 24) & 0xFF) / 255.0; 511 | 512 | /// The red channel of this color. 513 | @override 514 | double get r { 515 | return ((value >> 16) & 0xFF) / 255.0; 516 | } 517 | 518 | /// The green channel of this color. 519 | @override 520 | double get g { 521 | return ((value >> 8) & 0xFF) / 255.0; 522 | } 523 | 524 | /// The blue channel of this color. 525 | @override 526 | double get b { 527 | return (value & 0xFF) / 255.0; 528 | } 529 | 530 | @override 531 | String toString() => 'ColorRef(' 532 | '0x${value.toRadixString(16).padLeft(8, '0')}${id == null ? "" : ", id: $id"}' 533 | ')'; 534 | 535 | /// The equality operator. 536 | /// 537 | /// Two [ColorRef]s are equal if they have the same [defaultColor] and [id]. 538 | /// Please note, the current color (that depends on the current theme) is irrelevant. 539 | /// 540 | /// To compare by current color (that depends on the current theme), use 541 | /// method [sameColor] instead. 542 | /// 543 | @override 544 | bool operator ==(Object other) { 545 | // 546 | // During theme changes, for one single frame, no color is considered 547 | // the same as itself. This will make sure all colors rebuild. 548 | if (identical(this, other)) 549 | return !_rebuilding; 550 | // 551 | else 552 | return super == other && 553 | other is ColorRef && 554 | runtimeType == other.runtimeType && 555 | id == other.id && 556 | defaultColor == other.defaultColor; 557 | } 558 | 559 | /// Return true if [other] is a [ColorRef] or [Color] with the same color as 560 | /// this one. Note: If [other] is a [ColorRef], the compared color is the current one, 561 | /// i.e., the one that depend on the current theme. 562 | /// 563 | bool sameColor(Object other) { 564 | return identical(this, other) || 565 | (other is ColorRef && value == other.value) || 566 | (other is Color && value == other.value); 567 | } 568 | 569 | @override 570 | int get hashCode => id.hashCode ^ defaultColor.hashCode; 571 | } 572 | 573 | class TextStyleRef extends TextStyle implements ThemeRef { 574 | // 575 | final String? id; 576 | final TextStyle? defaultTextStyle; 577 | 578 | const TextStyleRef(this.defaultTextStyle, {this.id}) : super(); 579 | 580 | const TextStyleRef.fromId(this.id) 581 | : defaultTextStyle = null, 582 | super(); 583 | 584 | TextStyle get textStyle { 585 | TextStyle? result = _currentTheme[this] as TextStyle?; 586 | result ??= _defaultTheme[this] as TextStyle?; 587 | result ??= defaultTextStyle; 588 | if (result == null) 589 | throw ConstThemeException('Theme text-style "$id" is not defined.'); 590 | if (_transformTextStyle != null) result = _transformTextStyle!(result); 591 | return result; 592 | } 593 | 594 | @override 595 | bool operator ==(Object other) => 596 | identical(this, other) || 597 | other is TextStyleRef && 598 | runtimeType == other.runtimeType && 599 | id == other.id && 600 | defaultTextStyle == other.defaultTextStyle; 601 | 602 | @override 603 | int get hashCode => id.hashCode ^ defaultTextStyle.hashCode; 604 | 605 | @override 606 | TextStyle apply({ 607 | Color? color, 608 | Color? backgroundColor, 609 | TextDecoration? decoration, 610 | Color? decorationColor, 611 | TextDecorationStyle? decorationStyle, 612 | double decorationThicknessFactor = 1.0, 613 | double decorationThicknessDelta = 0.0, 614 | String? fontFamily, 615 | List? fontFamilyFallback, 616 | double fontSizeFactor = 1.0, 617 | double fontSizeDelta = 0.0, 618 | int fontWeightDelta = 0, 619 | FontStyle? fontStyle, 620 | double letterSpacingFactor = 1.0, 621 | double letterSpacingDelta = 0.0, 622 | double wordSpacingFactor = 1.0, 623 | double wordSpacingDelta = 0.0, 624 | double heightFactor = 1.0, 625 | double heightDelta = 0.0, 626 | TextBaseline? textBaseline, 627 | ui.TextLeadingDistribution? leadingDistribution, 628 | Locale? locale, 629 | List? shadows, 630 | List? fontFeatures, 631 | List? fontVariations, 632 | String? package, 633 | TextOverflow? overflow, 634 | }) { 635 | return textStyle.apply( 636 | color: color, 637 | backgroundColor: backgroundColor, 638 | decoration: decoration, 639 | decorationColor: decorationColor, 640 | decorationStyle: decorationStyle, 641 | decorationThicknessFactor: decorationThicknessFactor, 642 | decorationThicknessDelta: decorationThicknessDelta, 643 | fontFamily: fontFamily, 644 | fontFamilyFallback: fontFamilyFallback, 645 | fontSizeFactor: fontSizeFactor, 646 | fontSizeDelta: fontSizeDelta, 647 | fontWeightDelta: fontWeightDelta, 648 | fontStyle: fontStyle, 649 | letterSpacingFactor: letterSpacingFactor, 650 | letterSpacingDelta: letterSpacingDelta, 651 | wordSpacingFactor: wordSpacingFactor, 652 | wordSpacingDelta: wordSpacingDelta, 653 | heightFactor: heightFactor, 654 | heightDelta: heightDelta, 655 | textBaseline: textBaseline, 656 | leadingDistribution: leadingDistribution, 657 | locale: locale, 658 | shadows: shadows, 659 | fontFeatures: fontFeatures, 660 | overflow: overflow, 661 | package: package, 662 | ); 663 | } 664 | 665 | @override 666 | Paint? get background => textStyle.background; 667 | 668 | @override 669 | Color? get backgroundColor => textStyle.backgroundColor; 670 | 671 | @override 672 | Color? get color => textStyle.color; 673 | 674 | @override 675 | RenderComparison compareTo(TextStyle other) { 676 | return textStyle.compareTo(other); 677 | } 678 | 679 | @override 680 | TextStyle copyWith({ 681 | bool? inherit, 682 | Color? color, 683 | Color? backgroundColor, 684 | String? fontFamily, 685 | List? fontFamilyFallback, 686 | double? fontSize, 687 | FontWeight? fontWeight, 688 | FontStyle? fontStyle, 689 | double? letterSpacing, 690 | double? wordSpacing, 691 | TextBaseline? textBaseline, 692 | double? height, 693 | ui.TextLeadingDistribution? leadingDistribution, 694 | Locale? locale, 695 | Paint? foreground, 696 | Paint? background, 697 | List? shadows, 698 | List? fontFeatures, 699 | List? fontVariations, 700 | TextDecoration? decoration, 701 | Color? decorationColor, 702 | TextDecorationStyle? decorationStyle, 703 | double? decorationThickness, 704 | String? debugLabel, 705 | TextOverflow? overflow, 706 | String? package, 707 | }) { 708 | return textStyle.copyWith( 709 | inherit: inherit, 710 | color: color, 711 | backgroundColor: backgroundColor, 712 | fontFamily: fontFamily, 713 | fontFamilyFallback: fontFamilyFallback, 714 | fontSize: fontSize, 715 | fontWeight: fontWeight, 716 | fontStyle: fontStyle, 717 | letterSpacing: letterSpacing, 718 | wordSpacing: wordSpacing, 719 | textBaseline: textBaseline, 720 | height: height, 721 | leadingDistribution: leadingDistribution, 722 | locale: locale, 723 | foreground: foreground, 724 | background: background, 725 | shadows: shadows, 726 | fontFeatures: fontFeatures, 727 | fontVariations: fontVariations, 728 | decoration: decoration, 729 | decorationColor: decorationColor, 730 | decorationStyle: decorationStyle, 731 | decorationThickness: decorationThickness, 732 | debugLabel: debugLabel, 733 | overflow: overflow, 734 | package: package, 735 | ); 736 | } 737 | 738 | @override 739 | String? get debugLabel => textStyle.debugLabel; 740 | 741 | @override 742 | TextDecoration? get decoration => textStyle.decoration; 743 | 744 | @override 745 | Color? get decorationColor => textStyle.decorationColor; 746 | 747 | @override 748 | TextDecorationStyle? get decorationStyle => textStyle.decorationStyle; 749 | 750 | @override 751 | double? get decorationThickness => textStyle.decorationThickness; 752 | 753 | @override 754 | String? get fontFamily => textStyle.fontFamily; 755 | 756 | @override 757 | List? get fontFamilyFallback => textStyle.fontFamilyFallback; 758 | 759 | @override 760 | List? get fontFeatures => textStyle.fontFeatures; 761 | 762 | @override 763 | double? get fontSize => textStyle.fontSize; 764 | 765 | @override 766 | FontStyle? get fontStyle => textStyle.fontStyle; 767 | 768 | @override 769 | FontWeight? get fontWeight => textStyle.fontWeight; 770 | 771 | @override 772 | Paint? get foreground => textStyle.foreground; 773 | 774 | @override 775 | ui.ParagraphStyle getParagraphStyle({ 776 | TextAlign? textAlign, 777 | TextDirection? textDirection, 778 | TextScaler textScaler = TextScaler.noScaling, 779 | String? ellipsis, 780 | int? maxLines, 781 | ui.TextHeightBehavior? textHeightBehavior, 782 | Locale? locale, 783 | String? fontFamily, 784 | double? fontSize, 785 | FontWeight? fontWeight, 786 | FontStyle? fontStyle, 787 | double? height, 788 | StrutStyle? strutStyle, 789 | }) { 790 | return textStyle.getParagraphStyle( 791 | textAlign: textAlign, 792 | textDirection: textDirection, 793 | textScaler: textScaler, 794 | ellipsis: ellipsis, 795 | maxLines: maxLines, 796 | textHeightBehavior: textHeightBehavior, 797 | locale: locale, 798 | fontFamily: fontFamily, 799 | fontSize: fontSize, 800 | fontWeight: fontWeight, 801 | fontStyle: fontStyle, 802 | height: height, 803 | strutStyle: strutStyle, 804 | ); 805 | } 806 | 807 | @override 808 | ui.TextStyle getTextStyle({ 809 | @Deprecated( 810 | 'Use textScaler instead. ' 811 | 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' 812 | 'This feature was deprecated after v3.12.0-2.0.pre.', 813 | ) 814 | double textScaleFactor = 1.0, 815 | TextScaler textScaler = TextScaler.noScaling, 816 | }) => 817 | textStyle.getTextStyle( 818 | textScaleFactor: textScaleFactor, 819 | textScaler: textScaler, 820 | ); 821 | 822 | @override 823 | double? get height => textStyle.height; 824 | 825 | @override 826 | bool get inherit => textStyle.inherit; 827 | 828 | @override 829 | ui.TextLeadingDistribution? get leadingDistribution => textStyle.leadingDistribution; 830 | 831 | @override 832 | double? get letterSpacing => textStyle.letterSpacing; 833 | 834 | @override 835 | Locale? get locale => textStyle.locale; 836 | 837 | @override 838 | TextStyle merge(TextStyle? other) => textStyle.merge(other); 839 | 840 | @override 841 | List? get shadows => textStyle.shadows; 842 | 843 | @override 844 | TextBaseline? get textBaseline => textStyle.textBaseline; 845 | 846 | @override 847 | DiagnosticsNode toDiagnosticsNode({String? name, DiagnosticsTreeStyle? style}) => 848 | textStyle.toDiagnosticsNode(name: name, style: style); 849 | 850 | @override 851 | String toStringShort() => textStyle.toStringShort(); 852 | 853 | @override 854 | double? get wordSpacing => textStyle.wordSpacing; 855 | 856 | @override 857 | TextOverflow? get overflow => textStyle.overflow; 858 | 859 | @override 860 | String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { 861 | String? result = defaultTextStyle?.toString(); 862 | if (result != null) { 863 | int pos = result.indexOf('('); 864 | if (pos == -1) return result; 865 | result = 866 | 'TextStyleRef(${result.substring(pos + 1, result.length - 1)}${id == null ? "" : ", id: $id"})'; 867 | return result; 868 | } else 869 | return 'TextStyleRef(id: ${id == null ? "" : id})'; 870 | } 871 | } 872 | 873 | /// The current theme overrides the default theme. If a value is present in the current 874 | /// theme, it will be used. If not, the value from the default theme will be used instead. 875 | /// For this reason, the current theme doesn't need to have all values, but only the ones 876 | /// you want to change from the default. 877 | Map _currentTheme = const {}; 878 | 879 | /// The default theme usually defines all values (colors and text-styles), or is left 880 | /// empty. 881 | /// 882 | /// If a value is not present in the current theme and also not present in the default 883 | /// theme, then the DEFAULT VALUE will be used instead. The default value is optional, 884 | /// and is that one defined when the value is created. For example, in 885 | /// `static const errorColor = ColorRef('errorColor', Color(0xFFCA2323));` 886 | /// the default value is `Color(0xFFCA2323)`. 887 | /// 888 | Map _defaultTheme = const {}; 889 | 890 | Color Function(Color)? _transformColor; 891 | 892 | TextStyle Function(TextStyle)? _transformTextStyle; 893 | 894 | bool _rebuilding = false; 895 | 896 | void _rebuildAllChildrenOfContext(BuildContext context) { 897 | void rebuild(Element el) { 898 | el.markNeedsBuild(); 899 | el.visitChildren(rebuild); 900 | } 901 | 902 | _rebuilding = true; 903 | 904 | (context as Element).visitChildren(rebuild); 905 | 906 | WidgetsBinding.instance.addPostFrameCallback((_) { 907 | _rebuilding = false; 908 | }); 909 | } 910 | 911 | /// Removes the current theme, falling back to the default theme. 912 | void _clearCurrentTheme() { 913 | Themed._delayedThemeChangeByKey = null; 914 | _currentTheme = const {}; 915 | } 916 | 917 | /// Returns true if the given theme is equal to the current one. 918 | /// Note: To check if the default them is being used, do: `ifThemeIs({})`. 919 | bool _ifCurrentThemeIs(Map theme) { 920 | theme = _toIdenticalKeyedMap(theme); 921 | return mapEquals(theme, _currentTheme); 922 | } 923 | 924 | /// Sets a transform which will be applied to all colors. 925 | void _setTransformColor(Color Function(Color)? transform) { 926 | _transformColor = transform; 927 | } 928 | 929 | /// Returns true if the given color transform is equal to the current one. 930 | bool _ifCurrentTransformColorIs(Color Function(Color)? transform) => 931 | identical(transform, _transformColor); 932 | 933 | /// Sets a transform which will be applied to all colors. 934 | void _setTransformTextStyle(TextStyle Function(TextStyle)? transform) { 935 | _transformTextStyle = transform; 936 | } 937 | 938 | /// Returns true if the given text style transform is equal to the current one. 939 | bool _ifCurrentTransformTextStyleIs(TextStyle Function(TextStyle)? transform) => 940 | identical(transform, _transformTextStyle); 941 | 942 | /// Converts a given Map? to a new map with identical keys. 943 | /// 944 | /// This is done to ensure that the keys in the map are compared by identity rather 945 | /// than by equality, which is important because ThemeRef values may have custom equality 946 | /// logic that could cause issues. 947 | /// 948 | /// - If the input theme is null, it returns an empty map. 949 | /// - Otherwise, it creates a new map using Map.identity(), which 950 | /// ensures that keys are compared by identity. It then iterates over the keys and values 951 | /// of the input map, adding them to the new map. 952 | /// 953 | /// Finally, it returns the new map. 954 | /// This approach ensures that the keys in the resulting map are compared by their 955 | /// identity, avoiding potential issues with custom equality logic in ThemeRef values. 956 | /// 957 | Map _toIdenticalKeyedMap(Map? theme) { 958 | if (theme == null) 959 | return const {}; 960 | else { 961 | // Note: We add the maps like this, because the original theme may have ThemeRef 962 | // values which present our weird equality, so it may fail if we do it any other way. 963 | var result = Map.identity(); 964 | List keys = theme.keys.toList(); 965 | List values = theme.values.toList(); 966 | for (int i = 0; i < keys.length; i++) { 967 | result[keys[i]] = values[i]; 968 | } 969 | return result; 970 | } 971 | } 972 | --------------------------------------------------------------------------------