add new dep
This commit is contained in:
parent
f46500ce45
commit
60da6275f8
73 changed files with 50785 additions and 0 deletions
21
emoji_picker_flutter/LICENSE
Normal file
21
emoji_picker_flutter/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Stefan Humm
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
8
emoji_picker_flutter/android/.gitignore
vendored
Normal file
8
emoji_picker_flutter/android/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/libraries
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
78
emoji_picker_flutter/android/build.gradle
Normal file
78
emoji_picker_flutter/android/build.gradle
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
group 'com.fintasys.emoji_picker_flutter'
|
||||||
|
version '1.0-SNAPSHOT'
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
ext.kotlin_version = '1.9.20'
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
// Built-in Kotlin: AGP 9+ provides Kotlin, so only apply KGP on older AGP.
|
||||||
|
// AGP 9 enables Built-in Kotlin by default, but Flutter's AGP 9 migrator writes
|
||||||
|
// android.builtInKotlin=false into gradle.properties while apps/plugins transition,
|
||||||
|
// which leaves nothing to apply the `kotlin {}` extension unless we fall back to KGP.
|
||||||
|
def agpMajorVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')[0].toInteger()
|
||||||
|
def builtInKotlinDisabled = project.hasProperty('android.builtInKotlin') &&
|
||||||
|
project.property('android.builtInKotlin').toString() == 'false'
|
||||||
|
def appliesLegacyKotlin = agpMajorVersion < 9 || builtInKotlinDisabled
|
||||||
|
if (appliesLegacyKotlin) {
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
if (project.android.hasProperty("namespace")) {
|
||||||
|
namespace 'com.fintasys.emoji_picker_flutter'
|
||||||
|
}
|
||||||
|
|
||||||
|
compileSdk = 35
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main.java.srcDirs += 'src/main/kotlin'
|
||||||
|
}
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the Kotlin JVM target to match compileOptions (Java 17). It is not
|
||||||
|
// inherited from compileOptions automatically, so it must be set on both paths.
|
||||||
|
// Legacy KGP (<2.0) exposes kotlinOptions; Built-in Kotlin (AGP 9+, KGP 2.0+)
|
||||||
|
// exposes the compilerOptions DSL instead.
|
||||||
|
if (appliesLegacyKotlin) {
|
||||||
|
android.kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
if (appliesLegacyKotlin) {
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
emoji_picker_flutter/android/gradle.properties
Normal file
3
emoji_picker_flutter/android/gradle.properties
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
5
emoji_picker_flutter/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
emoji_picker_flutter/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
|
||||||
1
emoji_picker_flutter/android/settings.gradle
Normal file
1
emoji_picker_flutter/android/settings.gradle
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
rootProject.name = 'emoji_picker_flutter'
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.fintasys.emoji_picker_flutter">
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package com.fintasys.emoji_picker_flutter
|
||||||
|
|
||||||
|
import android.graphics.Paint
|
||||||
|
import androidx.annotation.NonNull
|
||||||
|
import androidx.core.graphics.PaintCompat
|
||||||
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
|
import io.flutter.plugin.common.MethodCall
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||||
|
import io.flutter.plugin.common.MethodChannel.Result
|
||||||
|
|
||||||
|
/** EmojiPickerFlutterPlugin */
|
||||||
|
class EmojiPickerFlutterPlugin : FlutterPlugin, MethodCallHandler {
|
||||||
|
/// The MethodChannel that will the communication between Flutter and native Android
|
||||||
|
///
|
||||||
|
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
|
||||||
|
/// when the Flutter Engine is detached from the Activity
|
||||||
|
private lateinit var channel: MethodChannel
|
||||||
|
val paint = Paint()
|
||||||
|
|
||||||
|
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "emoji_picker_flutter")
|
||||||
|
channel.setMethodCallHandler(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
|
||||||
|
when (call.method) {
|
||||||
|
/// returns list of boolean values that corresponds to the `source` list lenght
|
||||||
|
/// each value indicates whether the `source` emoji is supported on the platform
|
||||||
|
"getSupportedEmojis" -> {
|
||||||
|
val list = call.argument<List<String>>("source")
|
||||||
|
val supportedList: List<Boolean>? = list?.map { s ->
|
||||||
|
PaintCompat.hasGlyph(paint, s)
|
||||||
|
}
|
||||||
|
result.success(supportedList)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
||||||
|
channel.setMethodCallHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
emoji_picker_flutter/ios/.gitignore
vendored
Normal file
40
emoji_picker_flutter/ios/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
.idea/
|
||||||
|
.vagrant/
|
||||||
|
.sconsign.dblite
|
||||||
|
.svn/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
profile
|
||||||
|
|
||||||
|
DerivedData/
|
||||||
|
build/
|
||||||
|
GeneratedPluginRegistrant.h
|
||||||
|
GeneratedPluginRegistrant.m
|
||||||
|
|
||||||
|
.generated/
|
||||||
|
|
||||||
|
*.pbxuser
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
|
||||||
|
!default.pbxuser
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.perspectivev3
|
||||||
|
|
||||||
|
xcuserdata
|
||||||
|
|
||||||
|
*.moved-aside
|
||||||
|
|
||||||
|
*.pyc
|
||||||
|
*sync/
|
||||||
|
Icon?
|
||||||
|
.tags*
|
||||||
|
|
||||||
|
/Flutter/Generated.xcconfig
|
||||||
|
/Flutter/ephemeral/
|
||||||
|
/Flutter/flutter_export_environment.sh
|
||||||
|
|
||||||
|
.swiftpm/
|
||||||
21
emoji_picker_flutter/ios/emoji_picker_flutter.podspec
Normal file
21
emoji_picker_flutter/ios/emoji_picker_flutter.podspec
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#
|
||||||
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||||
|
# Run `pod lib lint emoji_picker_flutter.podspec` to validate before publishing.
|
||||||
|
#
|
||||||
|
Pod::Spec.new do |s|
|
||||||
|
s.name = 'emoji_picker_flutter'
|
||||||
|
s.version = '0.0.1'
|
||||||
|
s.summary = 'Flutter Emoji Picker'
|
||||||
|
s.description = 'Flutter Emoji Picker Plugin'
|
||||||
|
s.homepage = 'https://fintasys.com'
|
||||||
|
s.license = { :file => '../LICENSE', :type => 'MIT' }
|
||||||
|
s.author = { 'Your Company' => 'email@example.com' }
|
||||||
|
s.source = { :path => '.' }
|
||||||
|
s.source_files = 'emoji_picker_flutter/Sources/emoji_picker_flutter/**/*.swift'
|
||||||
|
s.dependency 'Flutter'
|
||||||
|
s.platform = :ios, '11.0'
|
||||||
|
|
||||||
|
# Flutter.framework does not contain a i386 slice.
|
||||||
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||||
|
s.swift_version = '5.0'
|
||||||
|
end
|
||||||
34
emoji_picker_flutter/ios/emoji_picker_flutter/Package.swift
Normal file
34
emoji_picker_flutter/ios/emoji_picker_flutter/Package.swift
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
// swift-tools-version: 5.9
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "emoji_picker_flutter",
|
||||||
|
platforms: [
|
||||||
|
.iOS("12.0"),
|
||||||
|
.macOS("10.14")
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
// If the plugin name contains "_", replace with "-" for the library name.
|
||||||
|
.library(name: "emoji-picker-flutter", targets: ["emoji_picker_flutter"])
|
||||||
|
],
|
||||||
|
dependencies: [],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "emoji_picker_flutter",
|
||||||
|
dependencies: [],
|
||||||
|
resources: [
|
||||||
|
// TODO: If your plugin requires a privacy manifest
|
||||||
|
// (e.g. if it uses any required reason APIs), update the PrivacyInfo.xcprivacy file
|
||||||
|
// to describe your plugin's privacy impact, and then uncomment this line.
|
||||||
|
// For more information, see:
|
||||||
|
// https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
|
||||||
|
// .process("PrivacyInfo.xcprivacy"),
|
||||||
|
|
||||||
|
// TODO: If you have other resources that need to be bundled with your plugin, refer to
|
||||||
|
// the following instructions to add them:
|
||||||
|
// https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public class EmojiPickerFlutterPlugin: NSObject, FlutterPlugin {
|
||||||
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||||
|
let channel = FlutterMethodChannel(name: "emoji_picker_flutter", binaryMessenger: registrar.messenger())
|
||||||
|
let instance = EmojiPickerFlutterPlugin()
|
||||||
|
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
switch call.method {
|
||||||
|
case "getPlatformVersion":
|
||||||
|
result("iOS " + UIDevice.current.systemVersion)
|
||||||
|
default:
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
emoji_picker_flutter/lib/emoji_picker_flutter.dart
Normal file
40
emoji_picker_flutter/lib/emoji_picker_flutter.dart
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
library;
|
||||||
|
|
||||||
|
export 'package:emoji_picker_flutter/locales/emoji_set.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/bottom_action_bar/bottom_action_bar.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/bottom_action_bar/bottom_action_bar_config.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/bottom_action_bar/default_bottom_action_bar.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/category_view/category_emoji.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/category_view/category_extra_tab.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/category_view/category_icon.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/category_view/category_icons.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/category_view/category_view.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/category_view/category_view_config.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/category_view/default_category_tab_bar.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/category_view/default_category_view.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/category_view/recent_tab_behavior.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/config.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/default_emoji_set.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/emoji.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/emoji_picker.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/emoji_picker_controller.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/emoji_picker_utils.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/emoji_text_editing_controller.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/emoji_text_style.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/emoji_view/default_emoji_picker_view.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/emoji_view/emoji_container.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/emoji_view/emoji_picker_view.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/emoji_view/emoji_view_config.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/emoji_view_state.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/recent_emoji.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/search_view/default_search_view.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/search_view/search_view.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/search_view/search_view_config.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/skin_tones/emoji_skin_tones.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/skin_tones/skin_tone_config.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/skin_tones/skin_tone_overlay.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/skin_tones/triangle_decoration.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/view_order_config.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/widgets/backspace_button.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/widgets/emoji_cell.dart';
|
||||||
|
export 'package:emoji_picker_flutter/src/widgets/search_button.dart';
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'emoji_picker_flutter_platform_interface.dart';
|
||||||
|
|
||||||
|
/// An implementation of [EmojiPickerFlutterPlatform] that uses method channels.
|
||||||
|
class MethodChannelEmojiPickerFlutter extends EmojiPickerFlutterPlatform {
|
||||||
|
/// The method channel used to interact with the native platform.
|
||||||
|
@visibleForTesting
|
||||||
|
final methodChannel = const MethodChannel('emoji_picker_flutter');
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||||
|
|
||||||
|
import 'emoji_picker_flutter_method_channel.dart';
|
||||||
|
|
||||||
|
/// EmojiPickerFlutterPlatform
|
||||||
|
abstract class EmojiPickerFlutterPlatform extends PlatformInterface {
|
||||||
|
/// Constructs a EmojiPickerFlutterPlatform.
|
||||||
|
EmojiPickerFlutterPlatform() : super(token: _token);
|
||||||
|
|
||||||
|
static final Object _token = Object();
|
||||||
|
|
||||||
|
static EmojiPickerFlutterPlatform _instance =
|
||||||
|
MethodChannelEmojiPickerFlutter();
|
||||||
|
|
||||||
|
/// The default instance of [EmojiPickerFlutterPlatform] to use.
|
||||||
|
///
|
||||||
|
/// Defaults to [MethodChannelEmojiPickerFlutter].
|
||||||
|
static EmojiPickerFlutterPlatform get instance => _instance;
|
||||||
|
|
||||||
|
/// Platform-specific implementations should set this with their own
|
||||||
|
/// platform-specific class that extends [EmojiPickerFlutterPlatform] when
|
||||||
|
/// they register themselves.
|
||||||
|
static set instance(EmojiPickerFlutterPlatform instance) {
|
||||||
|
PlatformInterface.verifyToken(instance, _token);
|
||||||
|
_instance = instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
emoji_picker_flutter/lib/emoji_picker_flutter_web.dart
Normal file
26
emoji_picker_flutter/lib/emoji_picker_flutter_web.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// In order to *not* need this ignore, consider extracting the "web" version
|
||||||
|
// of your plugin as a separate package, instead of inlining it in the same
|
||||||
|
// package as the core of your plugin.
|
||||||
|
// ignore: avoid_web_libraries_in_flutter
|
||||||
|
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||||
|
import 'package:web/web.dart' as html show window;
|
||||||
|
|
||||||
|
import 'emoji_picker_platform_interface.dart';
|
||||||
|
|
||||||
|
/// A web implementation of the EmojiPickerPlatform of the EmojiPicker plugin.
|
||||||
|
class EmojiPickerFlutterPluginWeb extends EmojiPickerPlatform {
|
||||||
|
/// Constructs a EmojiPickerFlutterPluginWeb
|
||||||
|
EmojiPickerFlutterPluginWeb();
|
||||||
|
|
||||||
|
/// RegisterWith
|
||||||
|
static void registerWith(Registrar registrar) {
|
||||||
|
EmojiPickerPlatform.instance = EmojiPickerFlutterPluginWeb();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [String] containing the version of the platform.
|
||||||
|
@override
|
||||||
|
Future<String?> getPlatformVersion() async {
|
||||||
|
final version = html.window.navigator.userAgent;
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
emoji_picker_flutter/lib/emoji_picker_method_channel.dart
Normal file
19
emoji_picker_flutter/lib/emoji_picker_method_channel.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'emoji_picker_platform_interface.dart';
|
||||||
|
|
||||||
|
/// An implementation of [EmojiPickerPlatform] that uses method channels.
|
||||||
|
class MethodChannelEmojiPicker extends EmojiPickerPlatform {
|
||||||
|
/// The method channel used to interact with the native platform.
|
||||||
|
@visibleForTesting
|
||||||
|
final methodChannel = const MethodChannel('emojipicker');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getPlatformVersion() async {
|
||||||
|
final version = await methodChannel.invokeMethod<String>(
|
||||||
|
'getPlatformVersion',
|
||||||
|
);
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||||
|
|
||||||
|
import 'emoji_picker_method_channel.dart';
|
||||||
|
|
||||||
|
/// EmojiPickerPlatform
|
||||||
|
abstract class EmojiPickerPlatform extends PlatformInterface {
|
||||||
|
/// Constructs a EmojiPickerPlatform.
|
||||||
|
EmojiPickerPlatform() : super(token: _token);
|
||||||
|
|
||||||
|
static final Object _token = Object();
|
||||||
|
|
||||||
|
static EmojiPickerPlatform _instance = MethodChannelEmojiPicker();
|
||||||
|
|
||||||
|
/// The default instance of [EmojiPickerPlatform] to use.
|
||||||
|
///
|
||||||
|
/// Defaults to [MethodChannelEmojiPicker].
|
||||||
|
static EmojiPickerPlatform get instance => _instance;
|
||||||
|
|
||||||
|
/// Platform-specific implementations should set this with their own
|
||||||
|
/// platform-specific class that extends [EmojiPickerPlatform] when
|
||||||
|
/// they register themselves.
|
||||||
|
static set instance(EmojiPickerPlatform instance) {
|
||||||
|
PlatformInterface.verifyToken(instance, _token);
|
||||||
|
_instance = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// getPlatformVersion
|
||||||
|
Future<String?> getPlatformVersion() {
|
||||||
|
throw UnimplementedError('platformVersion() has not been implemented.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Default method for locale selection
|
||||||
|
List<CategoryEmoji> getDefaultEmojiLocale(Locale locale) {
|
||||||
|
switch (locale.languageCode) {
|
||||||
|
case 'de':
|
||||||
|
return emojiSetGerman;
|
||||||
|
case 'en':
|
||||||
|
return emojiSetEnglish;
|
||||||
|
case 'es':
|
||||||
|
return emojiSetSpanish;
|
||||||
|
case 'fr':
|
||||||
|
return emojiSetFrance;
|
||||||
|
case 'hi':
|
||||||
|
return emojiSetHindi;
|
||||||
|
case 'it':
|
||||||
|
return emojiSetItalian;
|
||||||
|
case 'ja':
|
||||||
|
return emojiSetJapanese;
|
||||||
|
case 'nl':
|
||||||
|
return emojiSetDutch;
|
||||||
|
case 'pt':
|
||||||
|
return emojiSetPortuguese;
|
||||||
|
case 'ru':
|
||||||
|
return emojiSetRussian;
|
||||||
|
case 'zh':
|
||||||
|
return emojiSetChinese;
|
||||||
|
default:
|
||||||
|
return emojiSetEnglish;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
emoji_picker_flutter/lib/locales/emoji_set.dart
Normal file
11
emoji_picker_flutter/lib/locales/emoji_set.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export 'package:emoji_picker_flutter/locales/emoji_set_de.dart';
|
||||||
|
export 'package:emoji_picker_flutter/locales/emoji_set_en.dart';
|
||||||
|
export 'package:emoji_picker_flutter/locales/emoji_set_es.dart';
|
||||||
|
export 'package:emoji_picker_flutter/locales/emoji_set_fr.dart';
|
||||||
|
export 'package:emoji_picker_flutter/locales/emoji_set_hi.dart';
|
||||||
|
export 'package:emoji_picker_flutter/locales/emoji_set_it.dart';
|
||||||
|
export 'package:emoji_picker_flutter/locales/emoji_set_ja.dart';
|
||||||
|
export 'package:emoji_picker_flutter/locales/emoji_set_nl.dart';
|
||||||
|
export 'package:emoji_picker_flutter/locales/emoji_set_pt.dart';
|
||||||
|
export 'package:emoji_picker_flutter/locales/emoji_set_ru.dart';
|
||||||
|
export 'package:emoji_picker_flutter/locales/emoji_set_zh.dart';
|
||||||
4476
emoji_picker_flutter/lib/locales/emoji_set_de.dart
Normal file
4476
emoji_picker_flutter/lib/locales/emoji_set_de.dart
Normal file
File diff suppressed because it is too large
Load diff
3751
emoji_picker_flutter/lib/locales/emoji_set_en.dart
Normal file
3751
emoji_picker_flutter/lib/locales/emoji_set_en.dart
Normal file
File diff suppressed because it is too large
Load diff
3534
emoji_picker_flutter/lib/locales/emoji_set_es.dart
Normal file
3534
emoji_picker_flutter/lib/locales/emoji_set_es.dart
Normal file
File diff suppressed because it is too large
Load diff
4920
emoji_picker_flutter/lib/locales/emoji_set_fr.dart
Normal file
4920
emoji_picker_flutter/lib/locales/emoji_set_fr.dart
Normal file
File diff suppressed because it is too large
Load diff
4089
emoji_picker_flutter/lib/locales/emoji_set_hi.dart
Normal file
4089
emoji_picker_flutter/lib/locales/emoji_set_hi.dart
Normal file
File diff suppressed because it is too large
Load diff
5676
emoji_picker_flutter/lib/locales/emoji_set_it.dart
Normal file
5676
emoji_picker_flutter/lib/locales/emoji_set_it.dart
Normal file
File diff suppressed because it is too large
Load diff
2582
emoji_picker_flutter/lib/locales/emoji_set_ja.dart
Normal file
2582
emoji_picker_flutter/lib/locales/emoji_set_ja.dart
Normal file
File diff suppressed because it is too large
Load diff
4295
emoji_picker_flutter/lib/locales/emoji_set_nl.dart
Normal file
4295
emoji_picker_flutter/lib/locales/emoji_set_nl.dart
Normal file
File diff suppressed because it is too large
Load diff
4337
emoji_picker_flutter/lib/locales/emoji_set_pt.dart
Normal file
4337
emoji_picker_flutter/lib/locales/emoji_set_pt.dart
Normal file
File diff suppressed because it is too large
Load diff
4641
emoji_picker_flutter/lib/locales/emoji_set_ru.dart
Normal file
4641
emoji_picker_flutter/lib/locales/emoji_set_ru.dart
Normal file
File diff suppressed because it is too large
Load diff
2551
emoji_picker_flutter/lib/locales/emoji_set_zh.dart
Normal file
2551
emoji_picker_flutter/lib/locales/emoji_set_zh.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,22 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Template class for custom implementation
|
||||||
|
abstract class BottomActionBar extends StatefulWidget {
|
||||||
|
/// Constructor
|
||||||
|
const BottomActionBar(
|
||||||
|
this.config,
|
||||||
|
this.state,
|
||||||
|
this.showSearchView, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Config for customizations
|
||||||
|
final Config config;
|
||||||
|
|
||||||
|
/// State that holds current emoji data
|
||||||
|
final EmojiViewState state;
|
||||||
|
|
||||||
|
/// Show Search Bar
|
||||||
|
final VoidCallback showSearchView;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Callback function for custom bottom action bar
|
||||||
|
typedef BottomActionBarBuilder =
|
||||||
|
Widget Function(
|
||||||
|
Config config,
|
||||||
|
EmojiViewState state,
|
||||||
|
VoidCallback showSearchView,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Bottom Action Bar Config
|
||||||
|
class BottomActionBarConfig {
|
||||||
|
/// Constructor
|
||||||
|
const BottomActionBarConfig({
|
||||||
|
this.enabled = true,
|
||||||
|
this.showBackspaceButton = true,
|
||||||
|
this.showSearchViewButton = true,
|
||||||
|
this.backgroundColor = Colors.blue,
|
||||||
|
this.buttonColor = Colors.blue,
|
||||||
|
this.buttonIconColor = Colors.white,
|
||||||
|
this.customBottomActionBar,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Enable Bottom Action Bar
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
/// Show Backspace button
|
||||||
|
final bool showBackspaceButton;
|
||||||
|
|
||||||
|
/// Show Search View button
|
||||||
|
final bool showSearchViewButton;
|
||||||
|
|
||||||
|
/// Background color of search bar
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
/// Search Button color
|
||||||
|
final Color buttonColor;
|
||||||
|
|
||||||
|
/// Search Button Icon color
|
||||||
|
final Color buttonIconColor;
|
||||||
|
|
||||||
|
/// Custom search bar
|
||||||
|
/// Hot reload is not supported
|
||||||
|
final BottomActionBarBuilder? customBottomActionBar;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(other) {
|
||||||
|
return (other is BottomActionBarConfig) &&
|
||||||
|
other.enabled == enabled &&
|
||||||
|
other.showBackspaceButton == showBackspaceButton &&
|
||||||
|
other.showSearchViewButton == showSearchViewButton &&
|
||||||
|
other.backgroundColor == backgroundColor &&
|
||||||
|
other.buttonColor == buttonColor &&
|
||||||
|
other.buttonIconColor == buttonIconColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
enabled.hashCode ^
|
||||||
|
showBackspaceButton.hashCode ^
|
||||||
|
showSearchViewButton.hashCode ^
|
||||||
|
backgroundColor.hashCode ^
|
||||||
|
buttonColor.hashCode ^
|
||||||
|
buttonIconColor.hashCode;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Default Bottom Action Bar implementation
|
||||||
|
class DefaultBottomActionBar extends BottomActionBar {
|
||||||
|
/// Constructor
|
||||||
|
const DefaultBottomActionBar(
|
||||||
|
super.config,
|
||||||
|
super.state,
|
||||||
|
super.showSearchView, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _DefaultBottomActionBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DefaultBottomActionBarState extends State<DefaultBottomActionBar> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
color: widget.config.bottomActionBarConfig.backgroundColor,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [_buildSearchViewButton(), _buildBackspaceButton()],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSearchViewButton() {
|
||||||
|
if (widget.config.bottomActionBarConfig.showSearchViewButton) {
|
||||||
|
return CircleAvatar(
|
||||||
|
backgroundColor: widget.config.bottomActionBarConfig.buttonColor,
|
||||||
|
child: SearchButton(
|
||||||
|
widget.config,
|
||||||
|
widget.showSearchView,
|
||||||
|
widget.config.bottomActionBarConfig.buttonIconColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBackspaceButton() {
|
||||||
|
if (widget.config.bottomActionBarConfig.showBackspaceButton) {
|
||||||
|
return BackspaceButton(
|
||||||
|
widget.config,
|
||||||
|
widget.state.onBackspacePressed,
|
||||||
|
widget.state.onBackspaceLongPressed,
|
||||||
|
widget.config.bottomActionBarConfig.buttonIconColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
|
||||||
|
/// Container for Category and their emoji
|
||||||
|
class CategoryEmoji {
|
||||||
|
/// Constructor
|
||||||
|
const CategoryEmoji(this.category, this.emoji);
|
||||||
|
|
||||||
|
/// Category instance
|
||||||
|
final Category category;
|
||||||
|
|
||||||
|
/// List of emoji of this category
|
||||||
|
final List<Emoji> emoji;
|
||||||
|
|
||||||
|
/// Copy method
|
||||||
|
CategoryEmoji copyWith({Category? category, List<Emoji>? emoji}) {
|
||||||
|
return CategoryEmoji(category ?? this.category, emoji ?? this.emoji);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
/// Behavior of extra tab
|
||||||
|
enum CategoryExtraTab {
|
||||||
|
/// Don't show extra tab
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
/// Display backspace button in tab bar
|
||||||
|
BACKSPACE,
|
||||||
|
|
||||||
|
/// Display search button in tab bar
|
||||||
|
SEARCH,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Class that defines the icon representing a [Category]
|
||||||
|
class CategoryIcon {
|
||||||
|
/// Icon of Category
|
||||||
|
const CategoryIcon({
|
||||||
|
required this.icon,
|
||||||
|
this.color = const Color.fromRGBO(211, 211, 211, 1),
|
||||||
|
this.selectedColor = const Color.fromRGBO(178, 178, 178, 1),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The icon to represent the category
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
/// The default color of the icon
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
/// The color of the icon once the category is selected
|
||||||
|
final Color selectedColor;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Class used to define all the [CategoryIcon] shown for each [Category]
|
||||||
|
///
|
||||||
|
/// This allows the keyboard to be personalized by changing icons shown.
|
||||||
|
/// If a [CategoryIcon] is set as null or not defined during initialization,
|
||||||
|
/// the default icons will be used instead
|
||||||
|
class CategoryIcons {
|
||||||
|
/// Constructor
|
||||||
|
const CategoryIcons({
|
||||||
|
this.recentIcon = Icons.access_time,
|
||||||
|
this.smileyIcon = Icons.tag_faces,
|
||||||
|
this.animalIcon = Icons.pets,
|
||||||
|
this.foodIcon = Icons.fastfood,
|
||||||
|
this.activityIcon = Icons.directions_run,
|
||||||
|
this.travelIcon = Icons.location_city,
|
||||||
|
this.objectIcon = Icons.lightbulb_outline,
|
||||||
|
this.symbolIcon = Icons.emoji_symbols,
|
||||||
|
this.flagIcon = Icons.flag,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Icon for [Category.RECENT]
|
||||||
|
final IconData recentIcon;
|
||||||
|
|
||||||
|
/// Icon for [Category.SMILEYS]
|
||||||
|
final IconData smileyIcon;
|
||||||
|
|
||||||
|
/// Icon for [Category.ANIMALS]
|
||||||
|
final IconData animalIcon;
|
||||||
|
|
||||||
|
/// Icon for [Category.FOODS]
|
||||||
|
final IconData foodIcon;
|
||||||
|
|
||||||
|
/// Icon for [Category.ACTIVITIES]
|
||||||
|
final IconData activityIcon;
|
||||||
|
|
||||||
|
/// Icon for [Category.TRAVEL]
|
||||||
|
final IconData travelIcon;
|
||||||
|
|
||||||
|
/// Icon for [Category.OBJECTS]
|
||||||
|
final IconData objectIcon;
|
||||||
|
|
||||||
|
/// Icon for [Category.SYMBOLS]
|
||||||
|
final IconData symbolIcon;
|
||||||
|
|
||||||
|
/// Icon for [Category.FLAGS]
|
||||||
|
final IconData flagIcon;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Template class for custom implementation
|
||||||
|
/// Inhert this class to create your own Category view
|
||||||
|
abstract class CategoryView extends StatefulWidget {
|
||||||
|
/// Constructor
|
||||||
|
const CategoryView(
|
||||||
|
this.config,
|
||||||
|
this.state,
|
||||||
|
this.tabController,
|
||||||
|
this.pageController, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Config for customizations
|
||||||
|
final Config config;
|
||||||
|
|
||||||
|
/// State that holds current emoji data
|
||||||
|
final EmojiViewState state;
|
||||||
|
|
||||||
|
/// TabController for Category view
|
||||||
|
final TabController tabController;
|
||||||
|
|
||||||
|
/// Page Controller of Emoji view
|
||||||
|
final PageController pageController;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the icon for the category
|
||||||
|
IconData getIconForCategory(CategoryIcons categoryIcons, Category category) {
|
||||||
|
switch (category) {
|
||||||
|
case Category.RECENT:
|
||||||
|
return categoryIcons.recentIcon;
|
||||||
|
case Category.SMILEYS:
|
||||||
|
return categoryIcons.smileyIcon;
|
||||||
|
case Category.ANIMALS:
|
||||||
|
return categoryIcons.animalIcon;
|
||||||
|
case Category.FOODS:
|
||||||
|
return categoryIcons.foodIcon;
|
||||||
|
case Category.TRAVEL:
|
||||||
|
return categoryIcons.travelIcon;
|
||||||
|
case Category.ACTIVITIES:
|
||||||
|
return categoryIcons.activityIcon;
|
||||||
|
case Category.OBJECTS:
|
||||||
|
return categoryIcons.objectIcon;
|
||||||
|
case Category.SYMBOLS:
|
||||||
|
return categoryIcons.symbolIcon;
|
||||||
|
case Category.FLAGS:
|
||||||
|
return categoryIcons.flagIcon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Template class for custom implementation
|
||||||
|
/// Inhert this class to create your own category view state
|
||||||
|
class CategoryViewState<T extends CategoryView> extends State<T>
|
||||||
|
with SkinToneOverlayStateMixin {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
throw UnimplementedError('Category View implementation missing');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Callback function for custom category view
|
||||||
|
typedef CategoryViewBuilder =
|
||||||
|
Widget Function(
|
||||||
|
Config config,
|
||||||
|
EmojiViewState state,
|
||||||
|
TabController tabController,
|
||||||
|
PageController pageController,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Category view Config
|
||||||
|
class CategoryViewConfig {
|
||||||
|
/// Constructor
|
||||||
|
const CategoryViewConfig({
|
||||||
|
this.tabBarHeight = 46.0,
|
||||||
|
this.tabIndicatorAnimDuration = kTabScrollDuration,
|
||||||
|
this.initCategory = Category.RECENT,
|
||||||
|
this.recentTabBehavior = RecentTabBehavior.RECENT,
|
||||||
|
this.extraTab = CategoryExtraTab.NONE,
|
||||||
|
this.backgroundColor = const Color(0xFFEBEFF2),
|
||||||
|
this.indicatorColor = Colors.blue,
|
||||||
|
this.iconColor = Colors.grey,
|
||||||
|
this.iconColorSelected = Colors.blue,
|
||||||
|
this.backspaceColor = Colors.blue,
|
||||||
|
this.dividerColor,
|
||||||
|
this.categoryIcons = const CategoryIcons(),
|
||||||
|
this.customCategoryView,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Tab bar height
|
||||||
|
final double tabBarHeight;
|
||||||
|
|
||||||
|
/// Duration of tab indicator to animate to next category
|
||||||
|
final Duration tabIndicatorAnimDuration;
|
||||||
|
|
||||||
|
/// The initial [Category] that will be selected
|
||||||
|
/// This [Category] will have its button in the bottombar darkened
|
||||||
|
final Category initCategory;
|
||||||
|
|
||||||
|
/// Behavior of Recent Tab (Recent, Popular)
|
||||||
|
final RecentTabBehavior recentTabBehavior;
|
||||||
|
|
||||||
|
/// Extra tab button in category tab bar
|
||||||
|
final CategoryExtraTab? extraTab;
|
||||||
|
|
||||||
|
/// Background color of TabBar
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
/// The color of the category indicator
|
||||||
|
final Color indicatorColor;
|
||||||
|
|
||||||
|
/// The color of the category icons
|
||||||
|
final Color iconColor;
|
||||||
|
|
||||||
|
/// The color of the category icon when selected
|
||||||
|
final Color iconColorSelected;
|
||||||
|
|
||||||
|
/// The color of the backspace icon button
|
||||||
|
final Color backspaceColor;
|
||||||
|
|
||||||
|
/// Divider color between TabBar and emoji's, use Colors.transparent to remove
|
||||||
|
final Color? dividerColor;
|
||||||
|
|
||||||
|
/// Determines the icon to display for each [Category]
|
||||||
|
final CategoryIcons categoryIcons;
|
||||||
|
|
||||||
|
/// Custom search bar
|
||||||
|
/// Hot reload is not supported
|
||||||
|
final CategoryViewBuilder? customCategoryView;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(other) {
|
||||||
|
return (other is CategoryViewConfig) &&
|
||||||
|
other.tabBarHeight == tabBarHeight &&
|
||||||
|
other.tabIndicatorAnimDuration == tabIndicatorAnimDuration &&
|
||||||
|
other.initCategory == initCategory &&
|
||||||
|
other.recentTabBehavior == recentTabBehavior &&
|
||||||
|
other.extraTab == extraTab &&
|
||||||
|
other.backgroundColor == backgroundColor &&
|
||||||
|
other.indicatorColor == indicatorColor &&
|
||||||
|
other.iconColor == iconColor &&
|
||||||
|
other.iconColorSelected == iconColorSelected &&
|
||||||
|
other.backspaceColor == backspaceColor &&
|
||||||
|
other.dividerColor == dividerColor &&
|
||||||
|
other.categoryIcons == categoryIcons;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
tabBarHeight.hashCode ^
|
||||||
|
tabIndicatorAnimDuration.hashCode ^
|
||||||
|
initCategory.hashCode ^
|
||||||
|
recentTabBehavior.hashCode ^
|
||||||
|
extraTab.hashCode ^
|
||||||
|
backgroundColor.hashCode ^
|
||||||
|
indicatorColor.hashCode ^
|
||||||
|
iconColor.hashCode ^
|
||||||
|
iconColorSelected.hashCode ^
|
||||||
|
backspaceColor.hashCode ^
|
||||||
|
dividerColor.hashCode ^
|
||||||
|
categoryIcons.hashCode;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Default category tab bar
|
||||||
|
class DefaultCategoryTabBar extends StatelessWidget {
|
||||||
|
/// Constructor
|
||||||
|
const DefaultCategoryTabBar(
|
||||||
|
this.config,
|
||||||
|
this.tabController,
|
||||||
|
this.pageController,
|
||||||
|
this.categoryEmojis,
|
||||||
|
this.closeSkinToneOverlay, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Config
|
||||||
|
final Config config;
|
||||||
|
|
||||||
|
/// Tab controller
|
||||||
|
final TabController tabController;
|
||||||
|
|
||||||
|
/// Page controller
|
||||||
|
final PageController pageController;
|
||||||
|
|
||||||
|
/// Category emojis
|
||||||
|
final List<CategoryEmoji> categoryEmojis;
|
||||||
|
|
||||||
|
/// Close skin tone overlay callback
|
||||||
|
final VoidCallback closeSkinToneOverlay;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: config.categoryViewConfig.tabBarHeight,
|
||||||
|
child: TabBar(
|
||||||
|
labelColor: config.categoryViewConfig.iconColorSelected,
|
||||||
|
indicatorColor: config.categoryViewConfig.indicatorColor,
|
||||||
|
unselectedLabelColor: config.categoryViewConfig.iconColor,
|
||||||
|
dividerColor: config.categoryViewConfig.dividerColor,
|
||||||
|
controller: tabController,
|
||||||
|
labelPadding: EdgeInsets.zero,
|
||||||
|
onTap: (index) {
|
||||||
|
closeSkinToneOverlay();
|
||||||
|
pageController.jumpToPage(index);
|
||||||
|
},
|
||||||
|
tabs: categoryEmojis
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map<Widget>(
|
||||||
|
(item) => _buildCategoryTab(item.key, item.value.category),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCategoryTab(int index, Category category) {
|
||||||
|
return Tab(
|
||||||
|
icon: Icon(
|
||||||
|
getIconForCategory(config.categoryViewConfig.categoryIcons, category),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Default category view
|
||||||
|
class DefaultCategoryView extends CategoryView {
|
||||||
|
/// Constructor
|
||||||
|
const DefaultCategoryView(
|
||||||
|
super.config,
|
||||||
|
super.state,
|
||||||
|
super.tabController,
|
||||||
|
super.pageController, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
DefaultCategoryViewState createState() => DefaultCategoryViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default Category View State
|
||||||
|
class DefaultCategoryViewState extends CategoryViewState {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
color: widget.config.categoryViewConfig.backgroundColor,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: DefaultCategoryTabBar(
|
||||||
|
widget.config,
|
||||||
|
widget.tabController,
|
||||||
|
widget.pageController,
|
||||||
|
widget.state.categoryEmoji,
|
||||||
|
closeSkinToneOverlay,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildExtraTab(widget.config.categoryViewConfig.extraTab),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildExtraTab(CategoryExtraTab? extraTab) {
|
||||||
|
if (extraTab == CategoryExtraTab.BACKSPACE) {
|
||||||
|
return BackspaceButton(
|
||||||
|
widget.config,
|
||||||
|
widget.state.onBackspacePressed,
|
||||||
|
widget.state.onBackspaceLongPressed,
|
||||||
|
widget.config.categoryViewConfig.backspaceColor,
|
||||||
|
);
|
||||||
|
} else if (extraTab == CategoryExtraTab.SEARCH) {
|
||||||
|
return SearchButton(
|
||||||
|
widget.config,
|
||||||
|
widget.state.onShowSearchView,
|
||||||
|
widget.config.categoryViewConfig.iconColor,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
/// Behavior of Recent Tab
|
||||||
|
enum RecentTabBehavior {
|
||||||
|
/// Don't show Recent Tab
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
/// Display the last used emoji at the top of the list
|
||||||
|
RECENT,
|
||||||
|
|
||||||
|
/// Display the most often used emoji at the top of the list
|
||||||
|
POPULAR,
|
||||||
|
}
|
||||||
113
emoji_picker_flutter/lib/src/config.dart
Normal file
113
emoji_picker_flutter/lib/src/config.dart
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:emoji_picker_flutter/locales/default_emoji_set_locale.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Number of skin tone icons
|
||||||
|
const kSkinToneCount = 6;
|
||||||
|
|
||||||
|
/// Config for customizations
|
||||||
|
class Config {
|
||||||
|
/// Constructor
|
||||||
|
const Config({
|
||||||
|
this.height = 256,
|
||||||
|
this.checkPlatformCompatibility = true,
|
||||||
|
this.emojiSet = getDefaultEmojiLocale,
|
||||||
|
this.locale = const Locale('en'),
|
||||||
|
this.emojiTextStyle,
|
||||||
|
this.customBackspaceIcon,
|
||||||
|
this.customSearchIcon,
|
||||||
|
this.viewOrderConfig = const ViewOrderConfig(),
|
||||||
|
this.emojiViewConfig = const EmojiViewConfig(),
|
||||||
|
this.skinToneConfig = const SkinToneConfig(),
|
||||||
|
this.categoryViewConfig = const CategoryViewConfig(),
|
||||||
|
this.bottomActionBarConfig = const BottomActionBarConfig(),
|
||||||
|
this.searchViewConfig = const SearchViewConfig(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Max Height of the Emoji's view
|
||||||
|
/// If explicitly set to null, the emoji view will not be constrained by
|
||||||
|
/// height
|
||||||
|
final double? height;
|
||||||
|
|
||||||
|
/// Verify that emoji glyph is supported by the platform (Android only)
|
||||||
|
final bool checkPlatformCompatibility;
|
||||||
|
|
||||||
|
/// Useful to provide a customized list of Emoji or add/remove the support
|
||||||
|
/// for specific locales (create similar method as in
|
||||||
|
/// default_emoji_set_locale.dart).
|
||||||
|
/// If not provided, the default emoji set will be used based on the
|
||||||
|
/// locales that are available in the package.
|
||||||
|
final List<CategoryEmoji> Function(Locale locale)? emojiSet;
|
||||||
|
|
||||||
|
/// Locale to choose the fitting language for the emoji set
|
||||||
|
/// This will affect the emoji search results
|
||||||
|
final Locale locale;
|
||||||
|
|
||||||
|
/// Custom emoji text style to apply to emoji characters in the grid
|
||||||
|
///
|
||||||
|
/// If you define a custom fontFamily or use GoogleFonts to set this property
|
||||||
|
/// you can consider to set [checkPlatformCompatibility] to false. It will
|
||||||
|
/// improve initalization performance and prevent technically supported glyphs
|
||||||
|
/// from being filtered out.
|
||||||
|
///
|
||||||
|
/// This has priority over [EmojiViewConfig.emojiSizeMax] if font size is set.
|
||||||
|
final TextStyle? emojiTextStyle;
|
||||||
|
|
||||||
|
/// Custom backspace icon
|
||||||
|
final Icon? customBackspaceIcon;
|
||||||
|
|
||||||
|
/// Custom search icon
|
||||||
|
final Icon? customSearchIcon;
|
||||||
|
|
||||||
|
/// Config the order of the views displayed in the UI
|
||||||
|
/// (category bar, emoji view, search bar)
|
||||||
|
final ViewOrderConfig viewOrderConfig;
|
||||||
|
|
||||||
|
/// Emoji view config
|
||||||
|
final EmojiViewConfig emojiViewConfig;
|
||||||
|
|
||||||
|
/// Skin tone config
|
||||||
|
final SkinToneConfig skinToneConfig;
|
||||||
|
|
||||||
|
/// Category view config
|
||||||
|
final CategoryViewConfig categoryViewConfig;
|
||||||
|
|
||||||
|
/// Search bar config
|
||||||
|
final BottomActionBarConfig bottomActionBarConfig;
|
||||||
|
|
||||||
|
/// Search View config
|
||||||
|
final SearchViewConfig searchViewConfig;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(other) {
|
||||||
|
return (other is Config) &&
|
||||||
|
other.height == height &&
|
||||||
|
other.viewOrderConfig == viewOrderConfig &&
|
||||||
|
other.checkPlatformCompatibility == checkPlatformCompatibility &&
|
||||||
|
other.emojiSet == emojiSet &&
|
||||||
|
other.locale == locale &&
|
||||||
|
other.emojiTextStyle == emojiTextStyle &&
|
||||||
|
other.customBackspaceIcon == customBackspaceIcon &&
|
||||||
|
other.customSearchIcon == customSearchIcon &&
|
||||||
|
other.emojiViewConfig == emojiViewConfig &&
|
||||||
|
other.skinToneConfig == skinToneConfig &&
|
||||||
|
other.bottomActionBarConfig == bottomActionBarConfig &&
|
||||||
|
other.searchViewConfig == searchViewConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
(height?.hashCode ?? 0) ^
|
||||||
|
viewOrderConfig.hashCode ^
|
||||||
|
checkPlatformCompatibility.hashCode ^
|
||||||
|
emojiSet.hashCode ^
|
||||||
|
locale.hashCode ^
|
||||||
|
(emojiTextStyle?.hashCode ?? 0) ^
|
||||||
|
customBackspaceIcon.hashCode ^
|
||||||
|
customSearchIcon.hashCode ^
|
||||||
|
categoryViewConfig.hashCode ^
|
||||||
|
emojiViewConfig.hashCode ^
|
||||||
|
skinToneConfig.hashCode ^
|
||||||
|
bottomActionBarConfig.hashCode ^
|
||||||
|
searchViewConfig.hashCode;
|
||||||
|
}
|
||||||
1
emoji_picker_flutter/lib/src/core/io/io_helper.dart
Normal file
1
emoji_picker_flutter/lib/src/core/io/io_helper.dart
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export 'io_web.dart' if (dart.library.io) 'dart:io';
|
||||||
110
emoji_picker_flutter/lib/src/core/io/io_web.dart
Normal file
110
emoji_picker_flutter/lib/src/core/io/io_web.dart
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// THIS FILE WAS DERIVED FROM SOURCE CODE UNDER THE FOLLOWING LICENSE
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Copyright 2012, the Dart project authors. All rights reserved.
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following
|
||||||
|
// disclaimer in the documentation and/or other materials provided
|
||||||
|
// with the distribution.
|
||||||
|
// * Neither the name of Google Inc. nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived
|
||||||
|
// from this software without specific prior written permission.
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
//
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// THIS, DERIVED FILE IS LICENSE UNDER THE FOLLOWING LICENSE
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// Copyright 2020 terrier989@gmail.com.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import 'package:web/web.dart' as web;
|
||||||
|
|
||||||
|
/// Information about the environment in which the current program is running.
|
||||||
|
///
|
||||||
|
/// Platform provides information such as the operating system,
|
||||||
|
/// the hostname of the computer, the value of environment variables,
|
||||||
|
/// the path to the running program,
|
||||||
|
/// and other global properties of the program being run.
|
||||||
|
class Platform {
|
||||||
|
/// Whether the operating system is a version of
|
||||||
|
/// [Linux](https://en.wikipedia.org/wiki/Linux).
|
||||||
|
///
|
||||||
|
/// This value is `false` if the operating system is a specialized
|
||||||
|
/// version of Linux that identifies itself by a different name,
|
||||||
|
/// for example Android (see [isAndroid]).
|
||||||
|
static final bool isLinux = (operatingSystem == 'linux');
|
||||||
|
|
||||||
|
/// Whether the operating system is a version of
|
||||||
|
/// [macOS](https://en.wikipedia.org/wiki/MacOS).
|
||||||
|
static final bool isMacOS = (operatingSystem == 'macos');
|
||||||
|
|
||||||
|
/// Whether the operating system is a version of
|
||||||
|
/// [Microsoft Windows](https://en.wikipedia.org/wiki/Microsoft_Windows).
|
||||||
|
static final bool isWindows = (operatingSystem == 'windows');
|
||||||
|
|
||||||
|
/// Whether the operating system is a version of
|
||||||
|
/// [Android](https://en.wikipedia.org/wiki/Android_%28operating_system%29).
|
||||||
|
static final bool isAndroid = (operatingSystem == 'android');
|
||||||
|
|
||||||
|
/// Whether the operating system is a version of
|
||||||
|
/// [iOS](https://en.wikipedia.org/wiki/IOS).
|
||||||
|
static final bool isIOS = (operatingSystem == 'ios');
|
||||||
|
|
||||||
|
/// Whether the operating system is a version of
|
||||||
|
/// [Fuchsia](https://en.wikipedia.org/wiki/Google_Fuchsia).
|
||||||
|
static final bool isFuchsia = (operatingSystem == 'fuchsia');
|
||||||
|
|
||||||
|
/// A string representing the operating system or platform.
|
||||||
|
static String get operatingSystem {
|
||||||
|
final s = web.window.navigator.userAgent.toLowerCase();
|
||||||
|
if (s.contains('iphone') ||
|
||||||
|
s.contains('ipad') ||
|
||||||
|
s.contains('ipod') ||
|
||||||
|
s.contains('watch os')) {
|
||||||
|
return 'ios';
|
||||||
|
}
|
||||||
|
if (s.contains('mac os')) {
|
||||||
|
return 'macos';
|
||||||
|
}
|
||||||
|
if (s.contains('fuchsia')) {
|
||||||
|
return 'fuchsia';
|
||||||
|
}
|
||||||
|
if (s.contains('android')) {
|
||||||
|
return 'android';
|
||||||
|
}
|
||||||
|
if (s.contains('linux') || s.contains('cros') || s.contains('chromebook')) {
|
||||||
|
return 'linux';
|
||||||
|
}
|
||||||
|
if (s.contains('windows')) {
|
||||||
|
return 'windows';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
1893
emoji_picker_flutter/lib/src/default_emoji_set.dart
Normal file
1893
emoji_picker_flutter/lib/src/default_emoji_set.dart
Normal file
File diff suppressed because it is too large
Load diff
55
emoji_picker_flutter/lib/src/emoji.dart
Normal file
55
emoji_picker_flutter/lib/src/emoji.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Delimiter for keywords
|
||||||
|
const _keywordDelimiter = ' | ';
|
||||||
|
|
||||||
|
/// A class to store data for each individual emoji
|
||||||
|
@immutable
|
||||||
|
class Emoji {
|
||||||
|
/// Emoji constructor
|
||||||
|
const Emoji(this.emoji, this.name, {this.hasSkinTone = false});
|
||||||
|
|
||||||
|
/// The unicode string for this emoji
|
||||||
|
///
|
||||||
|
/// This is the string that should be displayed to view the emoji
|
||||||
|
final String emoji;
|
||||||
|
|
||||||
|
/// The name or description for this emoji
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// Flag if emoji supports multiple skin tones
|
||||||
|
final bool hasSkinTone;
|
||||||
|
|
||||||
|
/// List of keywords that describe the emoji
|
||||||
|
List<String> get keywords => name.split(_keywordDelimiter);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Emoji: $emoji, Name: $name, HasSkinTone: $hasSkinTone';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse Emoji from json
|
||||||
|
static Emoji fromJson(Map<String, dynamic> json) {
|
||||||
|
return Emoji(
|
||||||
|
json['emoji'] as String,
|
||||||
|
json['name'] as String,
|
||||||
|
hasSkinTone: json['hasSkinTone'] != null
|
||||||
|
? json['hasSkinTone'] as bool
|
||||||
|
: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode Emoji to json
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {'emoji': emoji, 'name': name, 'hasSkinTone': hasSkinTone};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy method
|
||||||
|
Emoji copyWith({String? name, String? emoji, bool? hasSkinTone}) {
|
||||||
|
return Emoji(
|
||||||
|
emoji ?? this.emoji,
|
||||||
|
name ?? this.name,
|
||||||
|
hasSkinTone: hasSkinTone ?? this.hasSkinTone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
566
emoji_picker_flutter/lib/src/emoji_picker.dart
Normal file
566
emoji_picker_flutter/lib/src/emoji_picker.dart
Normal file
|
|
@ -0,0 +1,566 @@
|
||||||
|
import 'package:emoji_picker_flutter/locales/default_emoji_set_locale.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/category_view/category_emoji.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/category_view/recent_tab_behavior.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/config.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_picker_controller.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_picker_internal_utils.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_view/default_emoji_picker_view.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_view/emoji_view_config.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_view_state.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/recent_emoji.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/search_view/default_search_view.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'core/io/io_helper.dart';
|
||||||
|
|
||||||
|
/// All the possible categories that [Emoji] can be put into
|
||||||
|
///
|
||||||
|
/// All [Category] are shown in the category bar
|
||||||
|
enum Category {
|
||||||
|
/// Recent / Popular emojis
|
||||||
|
RECENT,
|
||||||
|
|
||||||
|
/// Smiley emojis
|
||||||
|
SMILEYS,
|
||||||
|
|
||||||
|
/// Animal emojis
|
||||||
|
ANIMALS,
|
||||||
|
|
||||||
|
/// Food emojis
|
||||||
|
FOODS,
|
||||||
|
|
||||||
|
/// Activity emojis
|
||||||
|
ACTIVITIES,
|
||||||
|
|
||||||
|
/// Travel emojis
|
||||||
|
TRAVEL,
|
||||||
|
|
||||||
|
/// Ojects emojis
|
||||||
|
OBJECTS,
|
||||||
|
|
||||||
|
/// Sumbol emojis
|
||||||
|
SYMBOLS,
|
||||||
|
|
||||||
|
/// Flag emojis
|
||||||
|
FLAGS,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extension on Category enum to get its name
|
||||||
|
extension CategoryExtension on Category {
|
||||||
|
/// Returns name of Category
|
||||||
|
String get name {
|
||||||
|
switch (this) {
|
||||||
|
case Category.RECENT:
|
||||||
|
return 'recent';
|
||||||
|
case Category.SMILEYS:
|
||||||
|
return 'smileys';
|
||||||
|
case Category.ANIMALS:
|
||||||
|
return 'animals';
|
||||||
|
case Category.FOODS:
|
||||||
|
return 'foods';
|
||||||
|
case Category.ACTIVITIES:
|
||||||
|
return 'activities';
|
||||||
|
case Category.TRAVEL:
|
||||||
|
return 'travel';
|
||||||
|
case Category.OBJECTS:
|
||||||
|
return 'objects';
|
||||||
|
case Category.SYMBOLS:
|
||||||
|
return 'symbols';
|
||||||
|
case Category.FLAGS:
|
||||||
|
return 'flags';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enum to alter the keyboard button style
|
||||||
|
enum ButtonMode {
|
||||||
|
/// No cell touch effects, uses GestureDetector only. Provides best grid
|
||||||
|
/// scrolling performance
|
||||||
|
NONE,
|
||||||
|
|
||||||
|
/// Android button style - gives the button a splash color with ripple effect
|
||||||
|
MATERIAL,
|
||||||
|
|
||||||
|
/// iOS button style - gives the button a fade out effect when pressed
|
||||||
|
CUPERTINO,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Callback function for when emoji is selected
|
||||||
|
///
|
||||||
|
/// The function returns the selected [Emoji] as well
|
||||||
|
/// as the [Category] from which it originated
|
||||||
|
/// Category can be null in some cases, for example in search results
|
||||||
|
typedef OnEmojiSelected = void Function(Category? category, Emoji emoji);
|
||||||
|
|
||||||
|
/// Callback from emoji cell to show a skin tone selection overlay
|
||||||
|
typedef OnSkinToneDialogRequested =
|
||||||
|
void Function(
|
||||||
|
Offset emojiBoxPosition,
|
||||||
|
Emoji emoji,
|
||||||
|
double emojiSize,
|
||||||
|
CategoryEmoji? categoryEmoji,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Callback function for backspace button
|
||||||
|
typedef OnBackspacePressed = void Function();
|
||||||
|
|
||||||
|
/// Callback function for backspace button when long pressed
|
||||||
|
typedef OnBackspaceLongPressed = void Function();
|
||||||
|
|
||||||
|
/// Callback function for category tab changed
|
||||||
|
typedef OnCategoryChanged = void Function(Category category);
|
||||||
|
|
||||||
|
/// The Emoji Keyboard widget
|
||||||
|
///
|
||||||
|
/// This widget displays a grid of [Emoji] sorted by [Category]
|
||||||
|
/// which the user can horizontally scroll through.
|
||||||
|
///
|
||||||
|
/// There is also a bottombar which displays all the possible [Category]
|
||||||
|
/// and allow the user to quickly switch to that [Category]
|
||||||
|
class EmojiPicker extends StatefulWidget {
|
||||||
|
/// EmojiPicker for flutter
|
||||||
|
const EmojiPicker({
|
||||||
|
super.key,
|
||||||
|
this.textEditingController,
|
||||||
|
this.scrollController,
|
||||||
|
this.controller,
|
||||||
|
this.onEmojiSelected,
|
||||||
|
this.onBackspacePressed,
|
||||||
|
this.onCategoryChanged,
|
||||||
|
this.config = const Config(),
|
||||||
|
this.customWidget,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Custom widget
|
||||||
|
final EmojiViewBuilder? customWidget;
|
||||||
|
|
||||||
|
/// If you provide the [TextEditingController] that is linked to a
|
||||||
|
/// [TextField] this widget handles inserting and deleting for you
|
||||||
|
/// automatically.
|
||||||
|
final TextEditingController? textEditingController;
|
||||||
|
|
||||||
|
/// If you provide the [ScrollController] that is linked to a
|
||||||
|
/// [TextField] this widget handles auto scrolling for you.
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
|
||||||
|
/// Controller for managing the emoji picker state, including
|
||||||
|
/// the currently selected category tab.
|
||||||
|
///
|
||||||
|
/// If provided, this controller allows you to:
|
||||||
|
/// - Read the current selected category programmatically
|
||||||
|
/// - Change the selected category programmatically
|
||||||
|
/// - Listen to category changes
|
||||||
|
///
|
||||||
|
/// If both [controller] and [onCategoryChanged] are provided,
|
||||||
|
/// the controller takes precedence.
|
||||||
|
final EmojiPickerController? controller;
|
||||||
|
|
||||||
|
/// The function called when the emoji is selected
|
||||||
|
final OnEmojiSelected? onEmojiSelected;
|
||||||
|
|
||||||
|
/// The function called when backspace button is pressed
|
||||||
|
final OnBackspacePressed? onBackspacePressed;
|
||||||
|
|
||||||
|
/// The function called when category tab changes
|
||||||
|
final OnCategoryChanged? onCategoryChanged;
|
||||||
|
|
||||||
|
/// Config for customizations
|
||||||
|
final Config config;
|
||||||
|
|
||||||
|
@override
|
||||||
|
EmojiPickerState createState() => EmojiPickerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EmojiPickerState
|
||||||
|
class EmojiPickerState extends State<EmojiPicker> {
|
||||||
|
final List<CategoryEmoji> _categoryEmoji = List.empty(growable: true);
|
||||||
|
List<RecentEmoji> _recentEmoji = List.empty(growable: true);
|
||||||
|
late EmojiViewState _state;
|
||||||
|
|
||||||
|
// Prevent emojis to be reloaded with every build
|
||||||
|
bool _loaded = false;
|
||||||
|
|
||||||
|
// Display Search bar
|
||||||
|
bool _isSearchBarVisible = false;
|
||||||
|
|
||||||
|
// Internal helper
|
||||||
|
final _emojiPickerInternalUtils = EmojiPickerInternalUtils();
|
||||||
|
|
||||||
|
/// Update recentEmoji list from outside using EmojiPickerUtils
|
||||||
|
void updateRecentEmoji(
|
||||||
|
List<RecentEmoji> recentEmoji, {
|
||||||
|
bool refresh = false,
|
||||||
|
}) {
|
||||||
|
_recentEmoji = recentEmoji;
|
||||||
|
// Only mutate the displayed category data when we actually intend to
|
||||||
|
// refresh the UI. `_categoryEmoji` is shared by reference with `_state`,
|
||||||
|
// so mutating it here without a `setState()` would still surface the new
|
||||||
|
// recent list on the next unrelated rebuild triggered by a parent widget
|
||||||
|
// (e.g. the RECENT tab re-ordering under the user). The persisted store is
|
||||||
|
// already updated by the caller, so the change is not lost - it simply
|
||||||
|
// becomes visible on the next explicit refresh instead of leaking early.
|
||||||
|
if (!refresh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final recentTabIndex = _categoryEmoji.indexWhere(
|
||||||
|
(element) => element.category == Category.RECENT,
|
||||||
|
);
|
||||||
|
if (recentTabIndex != -1) {
|
||||||
|
_categoryEmoji[recentTabIndex] = _categoryEmoji[recentTabIndex].copyWith(
|
||||||
|
emoji: _recentEmoji.map((e) => e.emoji).toList(),
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_updateEmojis();
|
||||||
|
widget.textEditingController?.addListener(_scrollToCursorAfterTextChange);
|
||||||
|
widget.controller?.addListener(_onControllerChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant EmojiPicker oldWidget) {
|
||||||
|
if (oldWidget.config != widget.config) {
|
||||||
|
// Config changed - rebuild EmojiPickerView completely
|
||||||
|
_loaded = false;
|
||||||
|
_updateEmojis();
|
||||||
|
}
|
||||||
|
// Handle controller changes
|
||||||
|
if (oldWidget.controller != widget.controller) {
|
||||||
|
oldWidget.controller?.removeListener(_onControllerChanged);
|
||||||
|
widget.controller?.addListener(_onControllerChanged);
|
||||||
|
}
|
||||||
|
// Handle text editing controller changes
|
||||||
|
if (oldWidget.textEditingController != widget.textEditingController) {
|
||||||
|
oldWidget.textEditingController?.removeListener(
|
||||||
|
_scrollToCursorAfterTextChange,
|
||||||
|
);
|
||||||
|
widget.textEditingController?.addListener(_scrollToCursorAfterTextChange);
|
||||||
|
}
|
||||||
|
_resetStateWhenOffstage();
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!_loaded) {
|
||||||
|
return widget.config.emojiViewConfig.loadingIndicator;
|
||||||
|
}
|
||||||
|
if (_isSearchBarVisible) {
|
||||||
|
return _buildSearchBar();
|
||||||
|
}
|
||||||
|
return _buildEmojiView();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetStateWhenOffstage() {
|
||||||
|
final offstageParent = context.findAncestorWidgetOfExactType<Offstage>();
|
||||||
|
if (offstageParent != null &&
|
||||||
|
offstageParent.offstage == true &&
|
||||||
|
_isSearchBarVisible) {
|
||||||
|
setState(() {
|
||||||
|
_isSearchBarVisible = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onBackspacePressed() {
|
||||||
|
if (widget.textEditingController != null) {
|
||||||
|
final controller = widget.textEditingController!;
|
||||||
|
|
||||||
|
final text = controller.value.text;
|
||||||
|
var cursorPosition = controller.selection.base.offset;
|
||||||
|
|
||||||
|
// If cursor is not set, then place it at the end of the textfield
|
||||||
|
if (cursorPosition < 0) {
|
||||||
|
controller.selection = TextSelection(
|
||||||
|
baseOffset: controller.text.length,
|
||||||
|
extentOffset: controller.text.length,
|
||||||
|
);
|
||||||
|
cursorPosition = controller.selection.base.offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorPosition >= 0) {
|
||||||
|
final selection = controller.value.selection;
|
||||||
|
final newTextBeforeCursor = selection
|
||||||
|
.textBefore(text)
|
||||||
|
.characters
|
||||||
|
.skipLast(1)
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
controller.value = controller.value.copyWith(
|
||||||
|
text: newTextBeforeCursor + selection.textAfter(text),
|
||||||
|
selection: TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: newTextBeforeCursor.length),
|
||||||
|
),
|
||||||
|
composing: TextRange.collapsed(newTextBeforeCursor.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.onBackspacePressed?.call();
|
||||||
|
|
||||||
|
if (widget.textEditingController == null) {
|
||||||
|
_scrollToCursorAfterTextChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onBackspaceLongPressed() {
|
||||||
|
if (widget.textEditingController != null) {
|
||||||
|
final controller = widget.textEditingController!;
|
||||||
|
|
||||||
|
final text = controller.value.text;
|
||||||
|
var cursorPosition = controller.selection.base.offset;
|
||||||
|
|
||||||
|
// If cursor is not set, then place it at the end of the textfield
|
||||||
|
if (cursorPosition < 0) {
|
||||||
|
controller.selection = TextSelection(
|
||||||
|
baseOffset: controller.text.length,
|
||||||
|
extentOffset: controller.text.length,
|
||||||
|
);
|
||||||
|
cursorPosition = controller.selection.base.offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursorPosition >= 0) {
|
||||||
|
final selection = controller.value.selection;
|
||||||
|
final newTextBeforeCursor = _deleteWordByWord(
|
||||||
|
selection.textBefore(text).toString(),
|
||||||
|
);
|
||||||
|
controller.value = controller.value.copyWith(
|
||||||
|
text: newTextBeforeCursor + selection.textAfter(text),
|
||||||
|
selection: TextSelection.fromPosition(
|
||||||
|
TextPosition(offset: newTextBeforeCursor.length),
|
||||||
|
),
|
||||||
|
composing: TextRange.collapsed(newTextBeforeCursor.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _deleteWordByWord(String text) {
|
||||||
|
// Trim trailing spaces
|
||||||
|
text = text.trimRight();
|
||||||
|
|
||||||
|
// Find the last space to determine the start of the last word
|
||||||
|
final lastSpaceIndex = text.lastIndexOf(' ');
|
||||||
|
|
||||||
|
// If there is a space, remove the last word and spaces before it
|
||||||
|
if (lastSpaceIndex != -1) {
|
||||||
|
return text.substring(0, lastSpaceIndex).trimRight();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no space, remove the entire text
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add recent emoji handling to tap listener
|
||||||
|
void _onEmojiSelected(Category? category, Emoji emoji) {
|
||||||
|
if (widget.config.categoryViewConfig.recentTabBehavior ==
|
||||||
|
RecentTabBehavior.POPULAR) {
|
||||||
|
_emojiPickerInternalUtils
|
||||||
|
.addEmojiToPopularUsed(emoji: emoji, config: widget.config)
|
||||||
|
.then(
|
||||||
|
(newRecentEmoji) => {
|
||||||
|
// we don't want to rebuild the widget if user is currently on
|
||||||
|
// the RECENT tab, it will make emojis jump since sorting
|
||||||
|
// is based on the use frequency
|
||||||
|
updateRecentEmoji(
|
||||||
|
newRecentEmoji,
|
||||||
|
refresh: category != Category.RECENT,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (widget.config.categoryViewConfig.recentTabBehavior ==
|
||||||
|
RecentTabBehavior.RECENT) {
|
||||||
|
_emojiPickerInternalUtils
|
||||||
|
.addEmojiToRecentlyUsed(emoji: emoji, config: widget.config)
|
||||||
|
.then(
|
||||||
|
(newRecentEmoji) => {
|
||||||
|
// we don't want to rebuild the widget if user is currently on
|
||||||
|
// the RECENT tab, it will make emojis jump since sorting
|
||||||
|
// is based on the use frequency
|
||||||
|
updateRecentEmoji(
|
||||||
|
newRecentEmoji,
|
||||||
|
refresh: category != Category.RECENT,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.textEditingController != null) {
|
||||||
|
// based on https://stackoverflow.com/a/60058972/10975692
|
||||||
|
final controller = widget.textEditingController!;
|
||||||
|
final text = controller.text;
|
||||||
|
final selection = controller.selection;
|
||||||
|
final cursorPosition = controller.selection.base.offset;
|
||||||
|
|
||||||
|
if (cursorPosition < 0) {
|
||||||
|
controller.text += emoji.emoji;
|
||||||
|
widget.onEmojiSelected?.call(category, emoji);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final newText = text.replaceRange(
|
||||||
|
selection.start,
|
||||||
|
selection.end,
|
||||||
|
emoji.emoji,
|
||||||
|
);
|
||||||
|
final emojiLength = emoji.emoji.length;
|
||||||
|
controller.value = controller.value.copyWith(
|
||||||
|
text: newText,
|
||||||
|
selection: selection.copyWith(
|
||||||
|
baseOffset: selection.start + emojiLength,
|
||||||
|
extentOffset: selection.start + emojiLength,
|
||||||
|
),
|
||||||
|
composing: TextRange.collapsed(newText.length),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.onEmojiSelected?.call(category, emoji);
|
||||||
|
|
||||||
|
if (widget.textEditingController == null) {
|
||||||
|
_scrollToCursorAfterTextChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize emoji data
|
||||||
|
Future<void> _updateEmojis() async {
|
||||||
|
_categoryEmoji.clear();
|
||||||
|
if ([
|
||||||
|
RecentTabBehavior.RECENT,
|
||||||
|
RecentTabBehavior.POPULAR,
|
||||||
|
].contains(widget.config.categoryViewConfig.recentTabBehavior)) {
|
||||||
|
final futureOrRecent = _emojiPickerInternalUtils.getRecentEmojis();
|
||||||
|
_recentEmoji = futureOrRecent is List<RecentEmoji>
|
||||||
|
? futureOrRecent
|
||||||
|
: await futureOrRecent;
|
||||||
|
final recentEmojiMap = _recentEmoji.map((e) => e.emoji).toList();
|
||||||
|
_categoryEmoji.add(CategoryEmoji(Category.RECENT, recentEmojiMap));
|
||||||
|
}
|
||||||
|
final data =
|
||||||
|
widget.config.emojiSet?.call(widget.config.locale) ??
|
||||||
|
getDefaultEmojiLocale(widget.config.locale);
|
||||||
|
if (widget.config.checkPlatformCompatibility) {
|
||||||
|
final futureOrCategories = _emojiPickerInternalUtils.filterUnsupported(
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
_categoryEmoji.addAll(
|
||||||
|
futureOrCategories is List<CategoryEmoji>
|
||||||
|
? futureOrCategories
|
||||||
|
: await futureOrCategories,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_categoryEmoji.addAll(data);
|
||||||
|
}
|
||||||
|
_state = EmojiViewState(
|
||||||
|
_categoryEmoji,
|
||||||
|
_onEmojiSelected,
|
||||||
|
_onBackspacePressed,
|
||||||
|
_onBackspaceLongPressed,
|
||||||
|
_showSearchView,
|
||||||
|
_handleCategoryChanged,
|
||||||
|
currentCategory: widget.controller?.currentCategory,
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_loaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSearchBar() {
|
||||||
|
return widget.config.searchViewConfig.customSearchView == null
|
||||||
|
? DefaultSearchView(widget.config, _state, _hideSearchView)
|
||||||
|
: widget.config.searchViewConfig.customSearchView!(
|
||||||
|
widget.config,
|
||||||
|
_state,
|
||||||
|
_hideSearchView,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _wrapScrollBehaviorForPlatforms(Widget child) {
|
||||||
|
return !kIsWeb && Platform.isLinux
|
||||||
|
? ScrollConfiguration(
|
||||||
|
behavior: ScrollConfiguration.of(
|
||||||
|
context,
|
||||||
|
).copyWith(scrollbars: false),
|
||||||
|
child: child,
|
||||||
|
)
|
||||||
|
: child;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmojiView() {
|
||||||
|
final content = widget.customWidget == null
|
||||||
|
? DefaultEmojiPickerView(widget.config, _state, _showSearchView)
|
||||||
|
: widget.customWidget!(widget.config, _state, _showSearchView);
|
||||||
|
|
||||||
|
return _wrapScrollBehaviorForPlatforms(
|
||||||
|
widget.config.height != null
|
||||||
|
? SizedBox(height: widget.config.height, child: content)
|
||||||
|
: content,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showSearchView() {
|
||||||
|
setState(() {
|
||||||
|
_isSearchBarVisible = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _hideSearchView() {
|
||||||
|
setState(() {
|
||||||
|
_isSearchBarVisible = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollToCursorAfterTextChange() {
|
||||||
|
if (widget.scrollController != null) {
|
||||||
|
final scrollController = widget.scrollController!;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (scrollController.hasClients) {
|
||||||
|
scrollController.animateTo(
|
||||||
|
scrollController.position.maxScrollExtent,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.ease,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handler for controller category changes
|
||||||
|
void _onControllerChanged() {
|
||||||
|
// When the controller's category changes programmatically,
|
||||||
|
// navigate the tabbar to the new category without rebuilding
|
||||||
|
if (mounted && _loaded && widget.controller != null) {
|
||||||
|
_state.categoryNavigationNotifier.value =
|
||||||
|
widget.controller!.currentCategory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle category changes (both from user interaction and programmatic)
|
||||||
|
void _handleCategoryChanged(Category category) {
|
||||||
|
// Update controller if present
|
||||||
|
widget.controller?.updateCategory(category);
|
||||||
|
|
||||||
|
// Call callback if present and no controller (for backward compatibility)
|
||||||
|
if (widget.controller == null) {
|
||||||
|
widget.onCategoryChanged?.call(category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.controller?.removeListener(_onControllerChanged);
|
||||||
|
widget.textEditingController?.removeListener(
|
||||||
|
_scrollToCursorAfterTextChange,
|
||||||
|
);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
71
emoji_picker_flutter/lib/src/emoji_picker_controller.dart
Normal file
71
emoji_picker_flutter/lib/src/emoji_picker_controller.dart
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_picker.dart' show Category;
|
||||||
|
import 'package:flutter/foundation.dart' hide Category;
|
||||||
|
|
||||||
|
/// Controller for EmojiPicker widget that allows reading and changing
|
||||||
|
/// the selected category programmatically.
|
||||||
|
///
|
||||||
|
/// Similar to [TextEditingController], this controller provides:
|
||||||
|
/// - Reading the current selected category
|
||||||
|
/// - Setting the category programmatically
|
||||||
|
/// - Listening to category changes
|
||||||
|
///
|
||||||
|
/// Example usage:
|
||||||
|
/// ```dart
|
||||||
|
/// final controller = EmojiPickerController();
|
||||||
|
///
|
||||||
|
/// // Listen to changes
|
||||||
|
/// controller.addListener(() {
|
||||||
|
/// print('Category changed to: ${controller.currentCategory}');
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// // Set category programmatically
|
||||||
|
/// controller.setCategory(Category.SMILEYS);
|
||||||
|
///
|
||||||
|
/// // Use with EmojiPicker
|
||||||
|
/// EmojiPicker(
|
||||||
|
/// controller: controller,
|
||||||
|
/// // ... other config
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
class EmojiPickerController extends ChangeNotifier {
|
||||||
|
/// Creates an EmojiPickerController with an optional initial category.
|
||||||
|
/// Defaults to [Category.RECENT] if not specified.
|
||||||
|
EmojiPickerController({Category initialCategory = Category.RECENT})
|
||||||
|
: _currentCategory = initialCategory;
|
||||||
|
|
||||||
|
Category _currentCategory;
|
||||||
|
|
||||||
|
/// The currently selected category.
|
||||||
|
Category get currentCategory => _currentCategory;
|
||||||
|
|
||||||
|
/// Sets the selected category and notifies listeners.
|
||||||
|
///
|
||||||
|
/// This will cause the emoji picker to navigate to the specified category
|
||||||
|
/// if it's currently being used by an EmojiPicker widget.
|
||||||
|
void setCategory(Category category) {
|
||||||
|
if (_currentCategory != category) {
|
||||||
|
_currentCategory = category;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal method used by EmojiPicker to update the controller
|
||||||
|
/// when the user manually changes categories.
|
||||||
|
///
|
||||||
|
/// This should not be called directly by user code.
|
||||||
|
void updateCategory(Category category) {
|
||||||
|
if (_currentCategory != category) {
|
||||||
|
_currentCategory = category;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a copy of this controller with the same current category.
|
||||||
|
EmojiPickerController copyWith({Category? category}) {
|
||||||
|
return EmojiPickerController(initialCategory: category ?? _currentCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'EmojiPickerController(currentCategory: $_currentCategory)';
|
||||||
|
}
|
||||||
227
emoji_picker_flutter/lib/src/emoji_picker_internal_utils.dart
Normal file
227
emoji_picker_flutter/lib/src/emoji_picker_internal_utils.dart
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/foundation.dart' hide Category;
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import 'core/io/io_helper.dart';
|
||||||
|
|
||||||
|
/// Initial value for RecentEmoji
|
||||||
|
const initVal = 1;
|
||||||
|
|
||||||
|
/// Helper class that provides internal usage
|
||||||
|
class EmojiPickerInternalUtils {
|
||||||
|
// Establish communication with native
|
||||||
|
static const _platform = MethodChannel('emoji_picker_flutter');
|
||||||
|
|
||||||
|
static final RegExp _skinToneRegExp = RegExp(SkinTone.values.join('|'));
|
||||||
|
|
||||||
|
/// Caches, per [Category], whether each emoji glyph is supported on the
|
||||||
|
/// platform. Keyed by the emoji string (not the [CategoryEmoji]) so the
|
||||||
|
/// cache stays correct across locales and custom emoji sets, which change
|
||||||
|
/// an emoji's name/keywords but never the glyph or its platform support.
|
||||||
|
static final Map<Category, Map<String, bool>> _emojiSupport = {};
|
||||||
|
|
||||||
|
/// Caches the recently used emojis so that [getRecentEmojis] avoids
|
||||||
|
/// re-reading [SharedPreferences] on subsequent calls. Kept in sync by the
|
||||||
|
/// add/clear methods below.
|
||||||
|
static List<RecentEmoji>? _recentEmojis;
|
||||||
|
|
||||||
|
// Query the native side for which of the given glyphs are supported and
|
||||||
|
// merge the result into the per-category support cache.
|
||||||
|
Future<void> _cacheSupport(CategoryEmoji category, List<Emoji> query) async {
|
||||||
|
final available = (await _platform.invokeListMethod<bool>(
|
||||||
|
'getSupportedEmojis',
|
||||||
|
{'source': query.map((e) => e.emoji).toList(growable: false)},
|
||||||
|
))!;
|
||||||
|
|
||||||
|
final support = _emojiSupport.putIfAbsent(category.category, () => {});
|
||||||
|
for (var i = 0; i < query.length; i++) {
|
||||||
|
support[query[i].emoji] = available[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter a category down to the glyphs known to be supported. Every glyph is
|
||||||
|
// expected to be present in the cache by the time this is called.
|
||||||
|
CategoryEmoji _applySupport(CategoryEmoji category) {
|
||||||
|
final support = _emojiSupport[category.category];
|
||||||
|
return category.copyWith(
|
||||||
|
emoji: category.emoji.where((e) => support?[e.emoji] ?? false).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filters out emojis not supported on the platform
|
||||||
|
///
|
||||||
|
/// Returns synchronously when the support of every requested glyph is
|
||||||
|
/// already cached, so a rebuilt [EmojiPicker] does not have to hit the
|
||||||
|
/// native side again. Glyphs whose support is not yet known (e.g. after a
|
||||||
|
/// locale switch that introduces new glyphs, or a custom emoji set) are
|
||||||
|
/// queried and merged into the cache.
|
||||||
|
FutureOr<List<CategoryEmoji>> filterUnsupported(List<CategoryEmoji> data) {
|
||||||
|
if (kIsWeb || !Platform.isAndroid) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect, per category, the glyphs whose support is not yet cached.
|
||||||
|
final pending = <CategoryEmoji, List<Emoji>>{};
|
||||||
|
for (final cat in data) {
|
||||||
|
final support = _emojiSupport[cat.category];
|
||||||
|
final unknown = support == null
|
||||||
|
? cat.emoji
|
||||||
|
: cat.emoji.where((e) => !support.containsKey(e.emoji)).toList();
|
||||||
|
if (unknown.isNotEmpty) {
|
||||||
|
pending[cat] = unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.isEmpty) {
|
||||||
|
return [for (final cat in data) _applySupport(cat)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Future(() async {
|
||||||
|
await Future.wait([
|
||||||
|
for (final entry in pending.entries)
|
||||||
|
_cacheSupport(entry.key, entry.value),
|
||||||
|
]);
|
||||||
|
return [for (final cat in data) _applySupport(cat)];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns list of recently used emoji from cache
|
||||||
|
///
|
||||||
|
/// Reads from [SharedPreferences] on the first call and reuses the cached
|
||||||
|
/// result afterwards. The cache is kept in sync by [addEmojiToRecentlyUsed],
|
||||||
|
/// [addEmojiToPopularUsed] and [clearRecentEmojisInLocalStorage].
|
||||||
|
FutureOr<List<RecentEmoji>> getRecentEmojis() {
|
||||||
|
if (_recentEmojis != null) {
|
||||||
|
return _recentEmojis!.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Future(() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
var emojiJson = prefs.getString('recent');
|
||||||
|
if (emojiJson == null) {
|
||||||
|
return _recentEmojis = [];
|
||||||
|
}
|
||||||
|
var json = jsonDecode(emojiJson) as List<dynamic>;
|
||||||
|
return _recentEmojis = json
|
||||||
|
.map<RecentEmoji>(RecentEmoji.fromJson)
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an emoji to recently used list
|
||||||
|
Future<List<RecentEmoji>> addEmojiToRecentlyUsed({
|
||||||
|
required Emoji emoji,
|
||||||
|
Config config = const Config(),
|
||||||
|
}) async {
|
||||||
|
// Remove emoji's skin tone in Recent-Category
|
||||||
|
if (emoji.hasSkinTone) {
|
||||||
|
emoji = removeSkinTone(emoji);
|
||||||
|
}
|
||||||
|
var recentEmoji = await getRecentEmojis();
|
||||||
|
var recentEmojiIndex = recentEmoji.indexWhere(
|
||||||
|
(element) => element.emoji.emoji == emoji.emoji,
|
||||||
|
);
|
||||||
|
if (recentEmojiIndex != -1) {
|
||||||
|
// Already exist in recent list
|
||||||
|
// Remove it
|
||||||
|
recentEmoji.removeAt(recentEmojiIndex);
|
||||||
|
}
|
||||||
|
// Add it first position
|
||||||
|
recentEmoji.insert(0, RecentEmoji(emoji, initVal));
|
||||||
|
|
||||||
|
// Limit entries to recentsLimit
|
||||||
|
recentEmoji = recentEmoji.sublist(
|
||||||
|
0,
|
||||||
|
min(config.emojiViewConfig.recentsLimit, recentEmoji.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
// save locally
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.setString('recent', jsonEncode(recentEmoji));
|
||||||
|
|
||||||
|
return _recentEmojis = recentEmoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an emoji to popular used list or increase its counter
|
||||||
|
Future<List<RecentEmoji>> addEmojiToPopularUsed({
|
||||||
|
required Emoji emoji,
|
||||||
|
Config config = const Config(),
|
||||||
|
}) async {
|
||||||
|
// Remove emoji's skin tone in Recent-Category
|
||||||
|
if (emoji.hasSkinTone) {
|
||||||
|
emoji = removeSkinTone(emoji);
|
||||||
|
}
|
||||||
|
var recentEmoji = await getRecentEmojis();
|
||||||
|
var recentEmojiIndex = recentEmoji.indexWhere(
|
||||||
|
(element) => element.emoji.emoji == emoji.emoji,
|
||||||
|
);
|
||||||
|
if (recentEmojiIndex != -1) {
|
||||||
|
// Already exist in recent list
|
||||||
|
// Just update counter
|
||||||
|
recentEmoji[recentEmojiIndex].counter++;
|
||||||
|
} else if (recentEmoji.length == config.emojiViewConfig.recentsLimit &&
|
||||||
|
config.emojiViewConfig.replaceEmojiOnLimitExceed) {
|
||||||
|
// Replace latest emoji with the fresh one
|
||||||
|
recentEmoji[recentEmoji.length - 1] = RecentEmoji(emoji, initVal);
|
||||||
|
} else {
|
||||||
|
recentEmoji.add(RecentEmoji(emoji, initVal));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by counter desc
|
||||||
|
recentEmoji.sort((a, b) => b.counter - a.counter);
|
||||||
|
|
||||||
|
// Limit entries to recentsLimit
|
||||||
|
recentEmoji = recentEmoji.sublist(
|
||||||
|
0,
|
||||||
|
min(config.emojiViewConfig.recentsLimit, recentEmoji.length),
|
||||||
|
);
|
||||||
|
|
||||||
|
// save locally
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.setString('recent', jsonEncode(recentEmoji));
|
||||||
|
|
||||||
|
return _recentEmojis = recentEmoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the list of recent emojis in local storage
|
||||||
|
Future<void> clearRecentEmojisInLocalStorage() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
prefs.setString('recent', jsonEncode([]));
|
||||||
|
_recentEmojis = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the last remembered skin tone modifier, or `null` if none stored
|
||||||
|
Future<String?> getRememberedSkinTone() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
return prefs.getString('skin_tone');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persists the remembered skin tone modifier. Passing `null` clears it.
|
||||||
|
Future<void> setRememberedSkinTone(String? skinTone) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
if (skinTone == null) {
|
||||||
|
await prefs.remove('skin_tone');
|
||||||
|
} else {
|
||||||
|
await prefs.setString('skin_tone', skinTone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove skin tone from given emoji
|
||||||
|
Emoji removeSkinTone(Emoji emoji) {
|
||||||
|
return emoji.copyWith(emoji: emoji.emoji.replaceFirst(_skinToneRegExp, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the in-memory caches. The caches are `static`, so they otherwise
|
||||||
|
/// persist for the lifetime of the isolate and can leak state between test
|
||||||
|
/// cases. Call this in `setUp`/`tearDown` to keep tests isolated.
|
||||||
|
@visibleForTesting
|
||||||
|
static void resetCaches() {
|
||||||
|
_emojiSupport.clear();
|
||||||
|
_recentEmojis = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
253
emoji_picker_flutter/lib/src/emoji_picker_utils.dart
Normal file
253
emoji_picker_flutter/lib/src/emoji_picker_utils.dart
Normal file
|
|
@ -0,0 +1,253 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_picker_internal_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Emoji Regex
|
||||||
|
/// Keycap Sequence '((\u0023|\u002a|[\u0030-\u0039])\ufe0f\u20e3){1}'
|
||||||
|
/// Issue: https://github.com/flutter/flutter/issues/36062
|
||||||
|
const EmojiRegex =
|
||||||
|
r'((\u0023|\u002a|[\u0030-\u0039])\ufe0f\u20e3){1}|\p{Emoji}|\u200D|\uFE0F';
|
||||||
|
|
||||||
|
/// Helper class that provides extended usage
|
||||||
|
class EmojiPickerUtils {
|
||||||
|
/// Singleton Constructor
|
||||||
|
factory EmojiPickerUtils() {
|
||||||
|
return _singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
EmojiPickerUtils._internal();
|
||||||
|
|
||||||
|
static final EmojiPickerUtils _singleton = EmojiPickerUtils._internal();
|
||||||
|
static final RegExp _whitespaceRegExp = RegExp(r'\s+');
|
||||||
|
final List<Emoji> _allAvailableEmojiEntities = [];
|
||||||
|
RegExp? _emojiRegExp;
|
||||||
|
|
||||||
|
/// Returns list of recently used emoji from cache
|
||||||
|
Future<List<RecentEmoji>> getRecentEmojis() =>
|
||||||
|
Future.value(EmojiPickerInternalUtils().getRecentEmojis());
|
||||||
|
|
||||||
|
/// Filters out emojis not supported on the platform
|
||||||
|
Future<List<CategoryEmoji>> filterUnsupported(List<CategoryEmoji> data) =>
|
||||||
|
Future.value(EmojiPickerInternalUtils().filterUnsupported(data));
|
||||||
|
|
||||||
|
/// Search for related emoticons based on keywords
|
||||||
|
Future<List<Emoji>> searchEmoji(
|
||||||
|
String search,
|
||||||
|
List<CategoryEmoji> emojiSet, {
|
||||||
|
bool checkPlatformCompatibility = true,
|
||||||
|
}) async {
|
||||||
|
if (search.isEmpty) return [];
|
||||||
|
|
||||||
|
if (_allAvailableEmojiEntities.isEmpty) {
|
||||||
|
final emojiPickerInternalUtils = EmojiPickerInternalUtils();
|
||||||
|
|
||||||
|
final data = [...emojiSet]
|
||||||
|
..removeWhere((e) => e.category == Category.RECENT);
|
||||||
|
final availableCategoryEmoji = checkPlatformCompatibility
|
||||||
|
? await emojiPickerInternalUtils.filterUnsupported(data)
|
||||||
|
: data;
|
||||||
|
|
||||||
|
// Set all the emoji entities
|
||||||
|
for (var emojis in availableCategoryEmoji) {
|
||||||
|
_allAvailableEmojiEntities.addAll(emojis.emoji);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the input string into a list of lowercase keywords
|
||||||
|
final keywordSet = search
|
||||||
|
.split(_whitespaceRegExp)
|
||||||
|
.where((e) => e.isNotEmpty)
|
||||||
|
.map((e) => e.toLowerCase())
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
if (keywordSet.isEmpty) return [];
|
||||||
|
|
||||||
|
return _allAvailableEmojiEntities.where((emoji) {
|
||||||
|
// Perform lowercasing of emoji keywords once
|
||||||
|
final emojiKeywordSet = emoji.keywords
|
||||||
|
.map((e) => e.toLowerCase())
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
// Check if first keyword is a prefix of any emoji keyword
|
||||||
|
final matchFirstKeyword = emojiKeywordSet.any(
|
||||||
|
(emojiKeyword) => emojiKeyword.startsWith(keywordSet.first),
|
||||||
|
);
|
||||||
|
|
||||||
|
var matchKeywords = false;
|
||||||
|
if (matchFirstKeyword) {
|
||||||
|
// Check if each search keyword is a prefix of any emoji keyword
|
||||||
|
// start from second keyword, returns true if empty (only 1 keyword)
|
||||||
|
matchKeywords = keywordSet.skip(1).every((keyword) {
|
||||||
|
return emojiKeywordSet.any(
|
||||||
|
(emojiKeyword) => emojiKeyword.startsWith(keyword),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
matchKeywords = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for an exact match with emoji character
|
||||||
|
final matchEmoji = emoji.emoji == search.trim();
|
||||||
|
|
||||||
|
return matchKeywords || matchEmoji;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an emoji to recently used list or increase its counter
|
||||||
|
Future<void> addEmojiToRecentlyUsed({
|
||||||
|
required GlobalKey<EmojiPickerState> key,
|
||||||
|
required Emoji emoji,
|
||||||
|
Config config = const Config(),
|
||||||
|
}) async {
|
||||||
|
return EmojiPickerInternalUtils()
|
||||||
|
.addEmojiToRecentlyUsed(emoji: emoji, config: config)
|
||||||
|
.then(
|
||||||
|
(recentEmojiList) =>
|
||||||
|
key.currentState?.updateRecentEmoji(recentEmojiList),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produce a list of spans to adjust style for emoji characters.
|
||||||
|
/// Spans enclosing emojis will have [parentStyle] combined with [emojiStyle].
|
||||||
|
/// Other spans will not have an explicit style (this method does not set
|
||||||
|
/// [parentStyle] to the whole text.
|
||||||
|
List<InlineSpan> setEmojiTextStyle(
|
||||||
|
String text, {
|
||||||
|
required TextStyle emojiStyle,
|
||||||
|
TextStyle? parentStyle,
|
||||||
|
}) {
|
||||||
|
final composedEmojiStyle = (parentStyle ?? const TextStyle())
|
||||||
|
.merge(DefaultEmojiTextStyle)
|
||||||
|
.merge(emojiStyle);
|
||||||
|
|
||||||
|
final spans = <TextSpan>[];
|
||||||
|
final matches = getEmojiRegex().allMatches(text).toList();
|
||||||
|
var cursor = 0;
|
||||||
|
for (final match in matches) {
|
||||||
|
if (cursor != match.start) {
|
||||||
|
// Non emoji text + following emoji
|
||||||
|
spans
|
||||||
|
..add(
|
||||||
|
TextSpan(
|
||||||
|
text: text.substring(cursor, match.start),
|
||||||
|
style: parentStyle,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..add(
|
||||||
|
TextSpan(
|
||||||
|
text: text.substring(match.start, match.end),
|
||||||
|
style: composedEmojiStyle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (spans.isEmpty) {
|
||||||
|
// Create new span if no previous emoji TextSpan exists
|
||||||
|
spans.add(
|
||||||
|
TextSpan(
|
||||||
|
text: text.substring(match.start, match.end),
|
||||||
|
style: composedEmojiStyle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Update last span if current text is still emoji
|
||||||
|
final lastIndex = spans.length - 1;
|
||||||
|
final lastText = spans[lastIndex].text ?? '';
|
||||||
|
final currentText = text.substring(match.start, match.end);
|
||||||
|
spans[lastIndex] = TextSpan(
|
||||||
|
text: '$lastText$currentText',
|
||||||
|
style: composedEmojiStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update cursor
|
||||||
|
cursor = match.end;
|
||||||
|
}
|
||||||
|
// Add remaining text
|
||||||
|
if (cursor != text.length) {
|
||||||
|
spans.add(
|
||||||
|
TextSpan(text: text.substring(cursor, text.length), style: parentStyle),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return spans;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies skin tone to given emoji
|
||||||
|
///
|
||||||
|
/// Any existing skin tone modifier is stripped first, so re-applying a tone
|
||||||
|
/// to an already toned glyph produces a valid single-modifier sequence
|
||||||
|
/// instead of an invalid double-modifier one (e.g. 👋🏻🏽).
|
||||||
|
Emoji applySkinTone(Emoji emoji, String color) {
|
||||||
|
final codeUnits = removeSkinTone(emoji).emoji.codeUnits;
|
||||||
|
var result = List<int>.empty(growable: true)
|
||||||
|
// Basic emoji without gender (until char 2)
|
||||||
|
..addAll(codeUnits.sublist(0, min(codeUnits.length, 2)))
|
||||||
|
// Skin tone
|
||||||
|
..addAll(color.codeUnits);
|
||||||
|
// add the rest of the emoji (gender, etc.) again
|
||||||
|
if (codeUnits.length >= 2) {
|
||||||
|
result.addAll(codeUnits.sublist(2));
|
||||||
|
}
|
||||||
|
return emoji.copyWith(emoji: String.fromCharCodes(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes any skin tone modifier from the given emoji
|
||||||
|
Emoji removeSkinTone(Emoji emoji) =>
|
||||||
|
EmojiPickerInternalUtils().removeSkinTone(emoji);
|
||||||
|
|
||||||
|
/// Returns the emoji that should be displayed (and selected) in the grid,
|
||||||
|
/// recents and search results.
|
||||||
|
///
|
||||||
|
/// When [skinToneConfig] remembers a tone and [rememberedSkinTone] is set,
|
||||||
|
/// the toned glyph is returned; otherwise the original emoji is returned
|
||||||
|
/// unchanged. The result keeps [Emoji.hasSkinTone] intact so the indicator
|
||||||
|
/// and long-press picker keep working on the cell.
|
||||||
|
Emoji applyDisplaySkinTone(
|
||||||
|
Emoji emoji,
|
||||||
|
SkinToneConfig skinToneConfig,
|
||||||
|
String? rememberedSkinTone,
|
||||||
|
) {
|
||||||
|
if (!skinToneConfig.enabled ||
|
||||||
|
!skinToneConfig.rememberSkinTone ||
|
||||||
|
rememberedSkinTone == null ||
|
||||||
|
!emoji.hasSkinTone) {
|
||||||
|
return emoji;
|
||||||
|
}
|
||||||
|
return applySkinTone(emoji, rememberedSkinTone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the skin tone modifier contained in [emoji], or `null` when the
|
||||||
|
/// emoji carries no skin tone.
|
||||||
|
String? extractSkinTone(Emoji emoji) {
|
||||||
|
for (final tone in SkinTone.values) {
|
||||||
|
if (emoji.emoji.contains(tone)) {
|
||||||
|
return tone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the last remembered skin tone modifier, or `null` if none.
|
||||||
|
Future<String?> getRememberedSkinTone() =>
|
||||||
|
EmojiPickerInternalUtils().getRememberedSkinTone();
|
||||||
|
|
||||||
|
/// Persists the remembered skin tone modifier. Passing `null` clears it.
|
||||||
|
Future<void> setRememberedSkinTone(String? skinTone) =>
|
||||||
|
EmojiPickerInternalUtils().setRememberedSkinTone(skinTone);
|
||||||
|
|
||||||
|
/// Clears the list of recent emojis
|
||||||
|
Future<void> clearRecentEmojis({
|
||||||
|
required GlobalKey<EmojiPickerState> key,
|
||||||
|
}) async {
|
||||||
|
return await EmojiPickerInternalUtils()
|
||||||
|
.clearRecentEmojisInLocalStorage()
|
||||||
|
.then((_) => key.currentState?.updateRecentEmoji([], refresh: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the emoji regex
|
||||||
|
/// Based on https://unicode.org/reports/tr51/
|
||||||
|
RegExp getEmojiRegex() {
|
||||||
|
return _emojiRegExp ??= RegExp(EmojiRegex, unicode: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// Default delimiter for regex
|
||||||
|
const delimiter = '|';
|
||||||
|
|
||||||
|
/// Text editing controller that produces text spans on the fly for setting
|
||||||
|
/// a particular style to emoji characters.
|
||||||
|
class EmojiTextEditingController extends TextEditingController {
|
||||||
|
/// Constructor, requres emojiStyle, since otherwise this class has no effect
|
||||||
|
EmojiTextEditingController({super.text, required this.emojiTextStyle});
|
||||||
|
|
||||||
|
/// The style used for the emoji characters
|
||||||
|
final TextStyle emojiTextStyle;
|
||||||
|
|
||||||
|
/// Emoji Picker Utils
|
||||||
|
final EmojiPickerUtils utils = EmojiPickerUtils();
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextSpan buildTextSpan({
|
||||||
|
required BuildContext context,
|
||||||
|
TextStyle? style,
|
||||||
|
required bool withComposing,
|
||||||
|
}) {
|
||||||
|
assert(
|
||||||
|
!value.composing.isValid || !withComposing || value.isComposingRangeValid,
|
||||||
|
);
|
||||||
|
// If the composing range is out of range for the current text, ignore it to
|
||||||
|
// preserve the tree integrity, otherwise in release mode a RangeError will
|
||||||
|
// be thrown and this EditableText will be built with a broken subtree.
|
||||||
|
final composingRegionOutOfRange =
|
||||||
|
!value.isComposingRangeValid || !withComposing;
|
||||||
|
|
||||||
|
// Style when no cursor or selection is set
|
||||||
|
if (composingRegionOutOfRange) {
|
||||||
|
final textSpanChildren = utils.setEmojiTextStyle(
|
||||||
|
text,
|
||||||
|
emojiStyle: emojiTextStyle,
|
||||||
|
parentStyle: style,
|
||||||
|
);
|
||||||
|
return TextSpan(style: style, children: textSpanChildren);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor will automatically highlight current word underlined
|
||||||
|
final underlineStyle =
|
||||||
|
style?.merge(const TextStyle(decoration: TextDecoration.underline)) ??
|
||||||
|
const TextStyle(decoration: TextDecoration.underline);
|
||||||
|
|
||||||
|
return TextSpan(
|
||||||
|
style: style,
|
||||||
|
children: <TextSpan>[
|
||||||
|
TextSpan(
|
||||||
|
children: utils.setEmojiTextStyle(
|
||||||
|
value.composing.textBefore(value.text),
|
||||||
|
emojiStyle: emojiTextStyle,
|
||||||
|
parentStyle: style,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
children: utils.setEmojiTextStyle(
|
||||||
|
value.composing.textInside(value.text),
|
||||||
|
emojiStyle: emojiTextStyle,
|
||||||
|
parentStyle: underlineStyle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
children: utils.setEmojiTextStyle(
|
||||||
|
value.composing.textAfter(value.text),
|
||||||
|
emojiStyle: emojiTextStyle,
|
||||||
|
parentStyle: style,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
emoji_picker_flutter/lib/src/emoji_text_style.dart
Normal file
15
emoji_picker_flutter/lib/src/emoji_text_style.dart
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Emoji text style providing commonly available fallback fonts
|
||||||
|
const DefaultEmojiTextStyle = TextStyle(
|
||||||
|
inherit: true,
|
||||||
|
// Commonly available fallback fonts.
|
||||||
|
fontFamilyFallback: [
|
||||||
|
// iOS and MacOs.
|
||||||
|
'Apple Color Emoji',
|
||||||
|
// Android, ChromeOS, Ubuntu and some other Linux distros.
|
||||||
|
'Noto Color Emoji',
|
||||||
|
// Windows.
|
||||||
|
'Segoe UI Emoji',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Default EmojiPicker Implementation
|
||||||
|
class DefaultEmojiPickerView extends EmojiPickerView {
|
||||||
|
/// Constructor
|
||||||
|
const DefaultEmojiPickerView(
|
||||||
|
super.config,
|
||||||
|
super.state,
|
||||||
|
super.showSearchBar, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DefaultEmojiPickerView> createState() => _DefaultEmojiPickerViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DefaultEmojiPickerViewState extends State<DefaultEmojiPickerView>
|
||||||
|
with SingleTickerProviderStateMixin, SkinToneOverlayStateMixin {
|
||||||
|
late TabController _tabController;
|
||||||
|
late PageController _pageController;
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
final _utils = EmojiPickerUtils();
|
||||||
|
|
||||||
|
/// Last remembered skin tone, applied to skin-tone-capable emoji for
|
||||||
|
/// display and selection when [SkinToneConfig.rememberSkinTone] is enabled.
|
||||||
|
String? _rememberedSkinTone;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
// Use controller's current category if available,
|
||||||
|
// otherwise use config's initCategory
|
||||||
|
final targetCategory =
|
||||||
|
widget.state.currentCategory ??
|
||||||
|
widget.config.categoryViewConfig.initCategory;
|
||||||
|
|
||||||
|
var initCategory = widget.state.categoryEmoji.indexWhere(
|
||||||
|
(element) => element.category == targetCategory,
|
||||||
|
);
|
||||||
|
if (initCategory == -1) {
|
||||||
|
initCategory = 0;
|
||||||
|
}
|
||||||
|
_tabController = TabController(
|
||||||
|
initialIndex: initCategory,
|
||||||
|
length: widget.state.categoryEmoji.length,
|
||||||
|
vsync: this,
|
||||||
|
);
|
||||||
|
_pageController = PageController(initialPage: initCategory)
|
||||||
|
..addListener(closeSkinToneOverlay);
|
||||||
|
_scrollController.addListener(closeSkinToneOverlay);
|
||||||
|
|
||||||
|
// Listen to programmatic category changes from controller
|
||||||
|
widget.state.categoryNavigationNotifier.addListener(
|
||||||
|
_onCategoryNavigationChanged,
|
||||||
|
);
|
||||||
|
|
||||||
|
_loadRememberedSkinTone();
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadRememberedSkinTone() {
|
||||||
|
if (!widget.config.skinToneConfig.rememberSkinTone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_utils.getRememberedSkinTone().then((tone) {
|
||||||
|
if (!mounted || tone == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _rememberedSkinTone = tone);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCategoryNavigationChanged() {
|
||||||
|
final targetCategory = widget.state.categoryNavigationNotifier.value;
|
||||||
|
if (targetCategory != null) {
|
||||||
|
final index = widget.state.categoryEmoji.indexWhere(
|
||||||
|
(element) => element.category == targetCategory,
|
||||||
|
);
|
||||||
|
if (index != -1) {
|
||||||
|
final currentPage = _pageController.page?.round();
|
||||||
|
if (index != currentPage) {
|
||||||
|
// Use jumpToPage for instant navigation without building
|
||||||
|
// intermediate pages. This prevents performance issues when
|
||||||
|
// jumping to tabs far away. The onPageChanged callback will
|
||||||
|
// handle animating the tab indicator
|
||||||
|
_pageController.jumpToPage(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.state.categoryNavigationNotifier.removeListener(
|
||||||
|
_onCategoryNavigationChanged,
|
||||||
|
);
|
||||||
|
closeSkinToneOverlay();
|
||||||
|
_pageController.dispose();
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final emojiSize = widget.config.emojiViewConfig.getEmojiSize(
|
||||||
|
constraints.maxWidth,
|
||||||
|
);
|
||||||
|
final emojiBoxSize = widget.config.emojiViewConfig.getEmojiBoxSize(
|
||||||
|
constraints.maxWidth,
|
||||||
|
);
|
||||||
|
return EmojiContainer(
|
||||||
|
color: widget.config.emojiViewConfig.backgroundColor,
|
||||||
|
buttonMode: widget.config.emojiViewConfig.buttonMode,
|
||||||
|
child: ClipRect(
|
||||||
|
child: Column(
|
||||||
|
children:
|
||||||
|
[
|
||||||
|
widget.config.viewOrderConfig.top,
|
||||||
|
widget.config.viewOrderConfig.middle,
|
||||||
|
widget.config.viewOrderConfig.bottom,
|
||||||
|
].map((item) {
|
||||||
|
switch (item) {
|
||||||
|
case EmojiPickerItem.categoryBar:
|
||||||
|
// Category view
|
||||||
|
return _buildCategoryView();
|
||||||
|
case EmojiPickerItem.emojiView:
|
||||||
|
// Emoji view
|
||||||
|
return _buildEmojiView(emojiSize, emojiBoxSize);
|
||||||
|
case EmojiPickerItem.searchBar:
|
||||||
|
// Search Bar
|
||||||
|
return _buildBottomSearchBar();
|
||||||
|
}
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCategoryView() {
|
||||||
|
return widget.config.categoryViewConfig.customCategoryView != null
|
||||||
|
? widget.config.categoryViewConfig.customCategoryView!(
|
||||||
|
widget.config,
|
||||||
|
widget.state,
|
||||||
|
_tabController,
|
||||||
|
_pageController,
|
||||||
|
)
|
||||||
|
: DefaultCategoryView(
|
||||||
|
widget.config,
|
||||||
|
widget.state,
|
||||||
|
_tabController,
|
||||||
|
_pageController,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmojiView(double emojiSize, double emojiBoxSize) {
|
||||||
|
return Flexible(
|
||||||
|
child: PageView.builder(
|
||||||
|
itemCount: widget.state.categoryEmoji.length,
|
||||||
|
controller: _pageController,
|
||||||
|
onPageChanged: (index) {
|
||||||
|
_tabController.animateTo(
|
||||||
|
index,
|
||||||
|
duration: widget.config.categoryViewConfig.tabIndicatorAnimDuration,
|
||||||
|
);
|
||||||
|
// Notify about category change
|
||||||
|
if (index < widget.state.categoryEmoji.length) {
|
||||||
|
widget.state.onCategoryChanged?.call(
|
||||||
|
widget.state.categoryEmoji[index].category,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (context, index) => _buildPage(
|
||||||
|
emojiSize,
|
||||||
|
emojiBoxSize,
|
||||||
|
widget.state.categoryEmoji[index],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBottomSearchBar() {
|
||||||
|
if (!widget.config.bottomActionBarConfig.enabled) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return widget.config.bottomActionBarConfig.customBottomActionBar != null
|
||||||
|
? widget.config.bottomActionBarConfig.customBottomActionBar!(
|
||||||
|
widget.config,
|
||||||
|
widget.state,
|
||||||
|
widget.showSearchBar,
|
||||||
|
)
|
||||||
|
: DefaultBottomActionBar(
|
||||||
|
widget.config,
|
||||||
|
widget.state,
|
||||||
|
widget.showSearchBar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPage(
|
||||||
|
double emojiSize,
|
||||||
|
double emojiBoxSize,
|
||||||
|
CategoryEmoji categoryEmoji,
|
||||||
|
) {
|
||||||
|
// Display notice if recent has no entries yet
|
||||||
|
if (categoryEmoji.category == Category.RECENT &&
|
||||||
|
categoryEmoji.emoji.isEmpty) {
|
||||||
|
return _buildNoRecent();
|
||||||
|
}
|
||||||
|
// Build page normally
|
||||||
|
return GridView.builder(
|
||||||
|
key: const Key('emojiScrollView'),
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
controller: _scrollController,
|
||||||
|
primary: false,
|
||||||
|
padding: widget.config.emojiViewConfig.gridPadding,
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
childAspectRatio: 1,
|
||||||
|
crossAxisCount: widget.config.emojiViewConfig.columns,
|
||||||
|
mainAxisSpacing: widget.config.emojiViewConfig.verticalSpacing,
|
||||||
|
crossAxisSpacing: widget.config.emojiViewConfig.horizontalSpacing,
|
||||||
|
),
|
||||||
|
itemCount: categoryEmoji.emoji.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
// Apply a remembered/default skin tone for display and selection.
|
||||||
|
// Falls back to the base glyph when no tone is configured.
|
||||||
|
final displayEmoji = _utils.applyDisplaySkinTone(
|
||||||
|
categoryEmoji.emoji[index],
|
||||||
|
widget.config.skinToneConfig,
|
||||||
|
_rememberedSkinTone,
|
||||||
|
);
|
||||||
|
return addSkinToneTargetIfAvailable(
|
||||||
|
hasSkinTone: displayEmoji.hasSkinTone,
|
||||||
|
linkKey: categoryEmoji.category.name + displayEmoji.emoji,
|
||||||
|
child: EmojiCell.fromConfig(
|
||||||
|
emoji: displayEmoji,
|
||||||
|
emojiSize: emojiSize,
|
||||||
|
emojiBoxSize: emojiBoxSize,
|
||||||
|
categoryEmoji: categoryEmoji,
|
||||||
|
onEmojiSelected: _onSkinTonedEmojiSelected,
|
||||||
|
onSkinToneDialogRequested: _openSkinToneDialog,
|
||||||
|
config: widget.config,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build Widget for when no recent emoji are available
|
||||||
|
Widget _buildNoRecent() {
|
||||||
|
return Center(child: widget.config.emojiViewConfig.noRecents);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openSkinToneDialog(
|
||||||
|
Offset emojiBoxPosition,
|
||||||
|
Emoji emoji,
|
||||||
|
double emojiSize,
|
||||||
|
CategoryEmoji? categoryEmoji,
|
||||||
|
) {
|
||||||
|
closeSkinToneOverlay();
|
||||||
|
if (!emoji.hasSkinTone || !widget.config.skinToneConfig.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showSkinToneOverlay(
|
||||||
|
emojiBoxPosition,
|
||||||
|
emoji,
|
||||||
|
emojiSize,
|
||||||
|
categoryEmoji,
|
||||||
|
widget.config,
|
||||||
|
_onSkinTonedEmojiSelected,
|
||||||
|
links[categoryEmoji!.category.name + emoji.emoji]!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSkinTonedEmojiSelected(Category? category, Emoji emoji) {
|
||||||
|
_rememberSkinToneIfEnabled(emoji);
|
||||||
|
widget.state.onEmojiSelected(category, emoji);
|
||||||
|
closeSkinToneOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persists and re-applies the skin tone of the selected [emoji] when
|
||||||
|
/// [SkinToneConfig.rememberSkinTone] is enabled. Selecting a
|
||||||
|
/// skin-tone-capable base glyph (no modifier) clears the remembered tone.
|
||||||
|
void _rememberSkinToneIfEnabled(Emoji emoji) {
|
||||||
|
if (!widget.config.skinToneConfig.rememberSkinTone || !emoji.hasSkinTone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final tone = _utils.extractSkinTone(emoji);
|
||||||
|
if (tone == _rememberedSkinTone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_utils.setRememberedSkinTone(tone);
|
||||||
|
setState(() => _rememberedSkinTone = tone);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
emoji_picker_flutter/lib/src/emoji_view/emoji_container.dart
Normal file
44
emoji_picker_flutter/lib/src/emoji_view/emoji_container.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_picker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A wrapper around a grid or list of emojis.
|
||||||
|
/// If the button style is Cupertino or None, this is just wrapping the
|
||||||
|
/// `child` with a container of a provided color.
|
||||||
|
/// For Material style it is a `Material` widget that allows to render
|
||||||
|
/// touch response for individual InkWell cells.
|
||||||
|
class EmojiContainer extends StatelessWidget {
|
||||||
|
/// Constructor
|
||||||
|
const EmojiContainer({
|
||||||
|
super.key,
|
||||||
|
required this.color,
|
||||||
|
required this.buttonMode,
|
||||||
|
this.padding,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Background color for container
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
/// Button mode that affects the type of container
|
||||||
|
final ButtonMode buttonMode;
|
||||||
|
|
||||||
|
/// Optional padding
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
|
/// Child widget
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (buttonMode == ButtonMode.MATERIAL) {
|
||||||
|
return Material(
|
||||||
|
color: color,
|
||||||
|
child: padding == null
|
||||||
|
? child
|
||||||
|
: Padding(padding: padding!, child: child),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Container(color: color, padding: padding, child: child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import 'package:emoji_picker_flutter/src/config.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_view_state.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Template class for custom implementation
|
||||||
|
/// Inhert this class to create your own EmojiPicker
|
||||||
|
abstract class EmojiPickerView extends StatefulWidget {
|
||||||
|
/// Constructor
|
||||||
|
const EmojiPickerView(
|
||||||
|
this.config,
|
||||||
|
this.state,
|
||||||
|
this.showSearchBar, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Config for customizations
|
||||||
|
final Config config;
|
||||||
|
|
||||||
|
/// State that holds current emoji data
|
||||||
|
final EmojiViewState state;
|
||||||
|
|
||||||
|
/// Show Search Bar
|
||||||
|
final VoidCallback showSearchBar;
|
||||||
|
}
|
||||||
112
emoji_picker_flutter/lib/src/emoji_view/emoji_view_config.dart
Normal file
112
emoji_picker_flutter/lib/src/emoji_view/emoji_view_config.dart
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Callback function for custom view
|
||||||
|
typedef EmojiViewBuilder =
|
||||||
|
Widget Function(
|
||||||
|
Config config,
|
||||||
|
EmojiViewState state,
|
||||||
|
VoidCallback showSearchBar,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Default Widget if no recent is available
|
||||||
|
const DefaultNoRecentsWidget = Text(
|
||||||
|
'No Recents',
|
||||||
|
style: TextStyle(fontSize: 20, color: Colors.black26),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Emoji View Config
|
||||||
|
class EmojiViewConfig {
|
||||||
|
/// Constructor
|
||||||
|
const EmojiViewConfig({
|
||||||
|
this.columns = 10,
|
||||||
|
this.emojiSizeMax = 28.0,
|
||||||
|
this.backgroundColor = const Color(0xFFEBEFF2),
|
||||||
|
this.verticalSpacing = 0,
|
||||||
|
this.horizontalSpacing = 0,
|
||||||
|
this.gridPadding = EdgeInsets.zero,
|
||||||
|
this.recentsLimit = 28,
|
||||||
|
this.replaceEmojiOnLimitExceed = false,
|
||||||
|
this.noRecents = DefaultNoRecentsWidget,
|
||||||
|
this.loadingIndicator = const SizedBox.shrink(),
|
||||||
|
this.buttonMode = ButtonMode.MATERIAL,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Number of emojis per row
|
||||||
|
final int columns;
|
||||||
|
|
||||||
|
/// Width and height the emoji will be maximal displayed
|
||||||
|
/// Can be smaller due to screen size and amount of columns
|
||||||
|
final double emojiSizeMax;
|
||||||
|
|
||||||
|
/// The background color of the emoji view
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
/// Verical spacing between emojis
|
||||||
|
final double verticalSpacing;
|
||||||
|
|
||||||
|
/// Horizontal spacing between emojis
|
||||||
|
final double horizontalSpacing;
|
||||||
|
|
||||||
|
/// Limit of recently used emoji that will be saved
|
||||||
|
final int recentsLimit;
|
||||||
|
|
||||||
|
/// A widget (usually [Text]) to be displayed if no recent emojis to display
|
||||||
|
/// Hot reload is not supported
|
||||||
|
final Widget noRecents;
|
||||||
|
|
||||||
|
/// A widget to display while emoji picker is initializing
|
||||||
|
/// Hot reload is not supported
|
||||||
|
final Widget loadingIndicator;
|
||||||
|
|
||||||
|
/// Choose visual response for tapping on an emoji cell
|
||||||
|
final ButtonMode buttonMode;
|
||||||
|
|
||||||
|
/// The padding of GridView, default is [EdgeInsets.zero]
|
||||||
|
final EdgeInsets gridPadding;
|
||||||
|
|
||||||
|
/// Replace latest emoji on recents list on limit exceed
|
||||||
|
final bool replaceEmojiOnLimitExceed;
|
||||||
|
|
||||||
|
/// Get Emoji size based on properties and screen width
|
||||||
|
double getEmojiSize(double width) {
|
||||||
|
final maxSize = getEmojiBoxSize(width);
|
||||||
|
return min(maxSize, emojiSizeMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get Emoji hitbox size based on properties and screen width
|
||||||
|
double getEmojiBoxSize(double width) {
|
||||||
|
final totalHorizontalSpacing = (columns - 1) * horizontalSpacing;
|
||||||
|
final availableWidth = width - totalHorizontalSpacing;
|
||||||
|
return availableWidth / columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(other) {
|
||||||
|
return (other is EmojiViewConfig) &&
|
||||||
|
other.columns == columns &&
|
||||||
|
other.emojiSizeMax == emojiSizeMax &&
|
||||||
|
other.backgroundColor == backgroundColor &&
|
||||||
|
other.verticalSpacing == verticalSpacing &&
|
||||||
|
other.horizontalSpacing == horizontalSpacing &&
|
||||||
|
other.recentsLimit == recentsLimit &&
|
||||||
|
other.buttonMode == buttonMode &&
|
||||||
|
other.gridPadding == gridPadding &&
|
||||||
|
other.replaceEmojiOnLimitExceed == replaceEmojiOnLimitExceed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
columns.hashCode ^
|
||||||
|
emojiSizeMax.hashCode ^
|
||||||
|
backgroundColor.hashCode ^
|
||||||
|
verticalSpacing.hashCode ^
|
||||||
|
horizontalSpacing.hashCode ^
|
||||||
|
recentsLimit.hashCode ^
|
||||||
|
buttonMode.hashCode ^
|
||||||
|
gridPadding.hashCode ^
|
||||||
|
replaceEmojiOnLimitExceed.hashCode;
|
||||||
|
}
|
||||||
44
emoji_picker_flutter/lib/src/emoji_view_state.dart
Normal file
44
emoji_picker_flutter/lib/src/emoji_view_state.dart
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// State that holds current emoji data
|
||||||
|
class EmojiViewState {
|
||||||
|
/// Constructor
|
||||||
|
EmojiViewState(
|
||||||
|
this.categoryEmoji,
|
||||||
|
this.onEmojiSelected,
|
||||||
|
this.onBackspacePressed,
|
||||||
|
this.onBackspaceLongPressed,
|
||||||
|
this.onShowSearchView,
|
||||||
|
this.onCategoryChanged, {
|
||||||
|
this.currentCategory,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// List of all category including their emoji
|
||||||
|
final List<CategoryEmoji> categoryEmoji;
|
||||||
|
|
||||||
|
/// Callback when pressed on emoji
|
||||||
|
final OnEmojiSelected onEmojiSelected;
|
||||||
|
|
||||||
|
/// Callback when pressed on backspace
|
||||||
|
final OnBackspacePressed? onBackspacePressed;
|
||||||
|
|
||||||
|
/// Callback when long pressed on backspace
|
||||||
|
final OnBackspaceLongPressed onBackspaceLongPressed;
|
||||||
|
|
||||||
|
/// Callback when pressed on search
|
||||||
|
final VoidCallback onShowSearchView;
|
||||||
|
|
||||||
|
/// Callback when category tab changes
|
||||||
|
final OnCategoryChanged? onCategoryChanged;
|
||||||
|
|
||||||
|
/// Current category from controller (if available)
|
||||||
|
/// Used to override the config's initCategory
|
||||||
|
final Category? currentCategory;
|
||||||
|
|
||||||
|
/// Notifier for programmatic category changes from controller
|
||||||
|
/// When this changes, the tabbar should navigate to the new category
|
||||||
|
/// without rebuilding the entire widget
|
||||||
|
final ValueNotifier<Category?> categoryNavigationNotifier =
|
||||||
|
ValueNotifier<Category?>(null);
|
||||||
|
}
|
||||||
27
emoji_picker_flutter/lib/src/recent_emoji.dart
Normal file
27
emoji_picker_flutter/lib/src/recent_emoji.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji.dart';
|
||||||
|
|
||||||
|
/// Class that holds an recent emoji
|
||||||
|
/// Recent Emoji has an instance of the emoji
|
||||||
|
/// And a counter, which counts how often this emoji
|
||||||
|
/// has been used before
|
||||||
|
class RecentEmoji {
|
||||||
|
/// Constructor
|
||||||
|
RecentEmoji(this.emoji, this.counter);
|
||||||
|
|
||||||
|
/// Emoji instance
|
||||||
|
final Emoji emoji;
|
||||||
|
|
||||||
|
/// Counter how often emoji has been used before
|
||||||
|
int counter = 0;
|
||||||
|
|
||||||
|
/// Parse RecentEmoji from json
|
||||||
|
static RecentEmoji fromJson(dynamic json) {
|
||||||
|
return RecentEmoji(
|
||||||
|
Emoji.fromJson(json['emoji'] as Map<String, dynamic>),
|
||||||
|
json['counter'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode RecentEmoji to json
|
||||||
|
Map<String, dynamic> toJson() => {'emoji': emoji, 'counter': counter};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Default Search implementation
|
||||||
|
class DefaultSearchView extends SearchView {
|
||||||
|
/// Constructor
|
||||||
|
const DefaultSearchView(
|
||||||
|
super.config,
|
||||||
|
super.state,
|
||||||
|
super.showEmojiView, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
DefaultSearchViewState createState() => DefaultSearchViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default Search View State
|
||||||
|
class DefaultSearchViewState extends SearchViewState {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final emojiSize = widget.config.emojiViewConfig.getEmojiSize(
|
||||||
|
constraints.maxWidth,
|
||||||
|
);
|
||||||
|
final emojiBoxSize = widget.config.emojiViewConfig.getEmojiBoxSize(
|
||||||
|
constraints.maxWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
color: widget.config.searchViewConfig.backgroundColor,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: SizedBox(
|
||||||
|
height: emojiBoxSize + 8.0,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: results.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return buildEmoji(
|
||||||
|
results[index],
|
||||||
|
emojiSize,
|
||||||
|
emojiBoxSize,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
widget.showEmojiView();
|
||||||
|
},
|
||||||
|
color: widget.config.searchViewConfig.buttonIconColor,
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
onChanged: onTextInputChanged,
|
||||||
|
focusNode: focusNode,
|
||||||
|
style: widget.config.searchViewConfig.inputTextStyle,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: widget.config.searchViewConfig.hintText,
|
||||||
|
hintStyle: widget.config.searchViewConfig.hintTextStyle,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
170
emoji_picker_flutter/lib/src/search_view/search_view.dart
Normal file
170
emoji_picker_flutter/lib/src/search_view/search_view.dart
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:emoji_picker_flutter/src/emoji_picker_internal_utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Template class for custom implementation
|
||||||
|
/// Inhert this class to create your own search view
|
||||||
|
abstract class SearchView extends StatefulWidget {
|
||||||
|
/// Constructor
|
||||||
|
const SearchView(this.config, this.state, this.showEmojiView, {super.key});
|
||||||
|
|
||||||
|
/// Config for customizations
|
||||||
|
final Config config;
|
||||||
|
|
||||||
|
/// State that holds current emoji data
|
||||||
|
final EmojiViewState state;
|
||||||
|
|
||||||
|
/// Return to emoji view
|
||||||
|
final VoidCallback showEmojiView;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Template class for custom implementation
|
||||||
|
/// Inhert this class to create your own search view state
|
||||||
|
class SearchViewState<T extends SearchView> extends State<T>
|
||||||
|
with SkinToneOverlayStateMixin {
|
||||||
|
/// Emoji picker utils
|
||||||
|
final utils = EmojiPickerUtils();
|
||||||
|
|
||||||
|
/// Internal utils, used for the cache-backed synchronous fast-path when
|
||||||
|
/// loading recent emojis (the public [utils] returns a `Future`).
|
||||||
|
final _internalUtils = EmojiPickerInternalUtils();
|
||||||
|
|
||||||
|
/// Focus node for textfield
|
||||||
|
final focusNode = FocusNode();
|
||||||
|
|
||||||
|
/// Search results
|
||||||
|
final results = List<Emoji>.empty(growable: true);
|
||||||
|
|
||||||
|
/// Last remembered skin tone, applied to skin-tone-capable emoji for display
|
||||||
|
/// and selection when [SkinToneConfig.rememberSkinTone] is enabled.
|
||||||
|
String? _rememberedSkinTone;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadRememberedSkinTone();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
// Auto focus textfield
|
||||||
|
FocusScope.of(context).requestFocus(focusNode);
|
||||||
|
// Load recent emojis initially
|
||||||
|
final futureOrRecent = _internalUtils.getRecentEmojis();
|
||||||
|
if (futureOrRecent is List<RecentEmoji>) {
|
||||||
|
setState(
|
||||||
|
() => _updateResults(futureOrRecent.map((e) => e.emoji).toList()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
futureOrRecent.then((value) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _updateResults(value.map((e) => e.emoji).toList()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
focusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadRememberedSkinTone() {
|
||||||
|
if (!widget.config.skinToneConfig.rememberSkinTone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
utils.getRememberedSkinTone().then((tone) {
|
||||||
|
if (!mounted || tone == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() => _rememberedSkinTone = tone);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On text input changed callback
|
||||||
|
void onTextInputChanged(String text) {
|
||||||
|
links.clear();
|
||||||
|
results.clear();
|
||||||
|
utils.searchEmoji(text, widget.state.categoryEmoji).then((value) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _updateResults(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateResults(List<Emoji> emojis) {
|
||||||
|
results
|
||||||
|
..clear()
|
||||||
|
..addAll(emojis);
|
||||||
|
results.asMap().entries.forEach((e) {
|
||||||
|
final displayEmoji = utils.applyDisplaySkinTone(
|
||||||
|
e.value,
|
||||||
|
widget.config.skinToneConfig,
|
||||||
|
_rememberedSkinTone,
|
||||||
|
);
|
||||||
|
links[displayEmoji.emoji] = LayerLink();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build emoji cell
|
||||||
|
Widget buildEmoji(Emoji emoji, double emojiSize, double emojiBoxSize) {
|
||||||
|
// Apply a remembered skin tone for display and selection.
|
||||||
|
// Falls back to the base glyph when no tone is remembered.
|
||||||
|
final displayEmoji = utils.applyDisplaySkinTone(
|
||||||
|
emoji,
|
||||||
|
widget.config.skinToneConfig,
|
||||||
|
_rememberedSkinTone,
|
||||||
|
);
|
||||||
|
return addSkinToneTargetIfAvailable(
|
||||||
|
hasSkinTone: displayEmoji.hasSkinTone,
|
||||||
|
linkKey: displayEmoji.emoji,
|
||||||
|
child: EmojiCell.fromConfig(
|
||||||
|
emoji: displayEmoji,
|
||||||
|
emojiSize: emojiSize,
|
||||||
|
emojiBoxSize: emojiBoxSize,
|
||||||
|
onEmojiSelected: widget.state.onEmojiSelected,
|
||||||
|
config: widget.config,
|
||||||
|
onSkinToneDialogRequested:
|
||||||
|
(emojiBoxPosition, emoji, emojiSize, category) {
|
||||||
|
closeSkinToneOverlay();
|
||||||
|
if (!emoji.hasSkinTone || !widget.config.skinToneConfig.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showSkinToneOverlay(
|
||||||
|
emojiBoxPosition,
|
||||||
|
emoji,
|
||||||
|
emojiSize,
|
||||||
|
null, // Todo: check if we can provide the category
|
||||||
|
widget.config,
|
||||||
|
_onSkinTonedEmojiSelected,
|
||||||
|
links[emoji.emoji]!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSkinTonedEmojiSelected(Category? category, Emoji emoji) {
|
||||||
|
_rememberSkinToneIfEnabled(emoji);
|
||||||
|
widget.state.onEmojiSelected(category, emoji);
|
||||||
|
closeSkinToneOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persists and re-applies the skin tone of the selected [emoji] when
|
||||||
|
/// [SkinToneConfig.rememberSkinTone] is enabled. Selecting a
|
||||||
|
/// skin-tone-capable base glyph (no modifier) clears the remembered tone.
|
||||||
|
void _rememberSkinToneIfEnabled(Emoji emoji) {
|
||||||
|
if (!widget.config.skinToneConfig.rememberSkinTone || !emoji.hasSkinTone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final tone = utils.extractSkinTone(emoji);
|
||||||
|
if (tone == _rememberedSkinTone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
utils.setRememberedSkinTone(tone);
|
||||||
|
setState(() => _rememberedSkinTone = tone);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
throw UnimplementedError('Search View implementation missing');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Callback function for custom search view
|
||||||
|
typedef SearchViewBuilder =
|
||||||
|
Widget Function(
|
||||||
|
Config config,
|
||||||
|
EmojiViewState state,
|
||||||
|
VoidCallback showEmojiView,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Search view Config
|
||||||
|
class SearchViewConfig {
|
||||||
|
/// Constructor
|
||||||
|
const SearchViewConfig({
|
||||||
|
this.backgroundColor = const Color(0xFFEBEFF2),
|
||||||
|
this.buttonIconColor = Colors.black26,
|
||||||
|
this.inputTextStyle,
|
||||||
|
this.hintText = 'Search',
|
||||||
|
this.hintTextStyle,
|
||||||
|
this.customSearchView,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Background color of search bar
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
/// Icon color of hide search view button
|
||||||
|
final Color buttonIconColor;
|
||||||
|
|
||||||
|
/// Inpit text style
|
||||||
|
final TextStyle? inputTextStyle;
|
||||||
|
|
||||||
|
/// Custom hint text
|
||||||
|
final String? hintText;
|
||||||
|
|
||||||
|
/// Custom hint text style
|
||||||
|
final TextStyle? hintTextStyle;
|
||||||
|
|
||||||
|
/// Custom search bar
|
||||||
|
/// Hot reload is not supported
|
||||||
|
final SearchViewBuilder? customSearchView;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(other) {
|
||||||
|
return (other is SearchViewConfig) &&
|
||||||
|
other.backgroundColor == backgroundColor &&
|
||||||
|
other.buttonIconColor == buttonIconColor &&
|
||||||
|
other.hintText == hintText &&
|
||||||
|
other.hintTextStyle == hintTextStyle &&
|
||||||
|
other.inputTextStyle == inputTextStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
backgroundColor.hashCode ^
|
||||||
|
buttonIconColor.hashCode ^
|
||||||
|
hintText.hashCode ^
|
||||||
|
hintTextStyle.hashCode ^
|
||||||
|
inputTextStyle.hashCode;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/// Alternative skin tones of Emoji
|
||||||
|
class SkinTone {
|
||||||
|
SkinTone._();
|
||||||
|
|
||||||
|
/// Light Skin Tone
|
||||||
|
static const String light = '🏻';
|
||||||
|
|
||||||
|
/// Medium-Light Skin Tone
|
||||||
|
static const String mediumLight = '🏼';
|
||||||
|
|
||||||
|
/// Medium Skin Tone
|
||||||
|
static const String medium = '🏽';
|
||||||
|
|
||||||
|
/// Medium-Dark Skin Tone
|
||||||
|
static const String mediumDark = '🏾';
|
||||||
|
|
||||||
|
/// Dark Skin Tone
|
||||||
|
static const String dark = '🏿';
|
||||||
|
|
||||||
|
/// Return all values as Array
|
||||||
|
static const values = [light, mediumLight, medium, mediumDark, dark];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Skin tone config Config
|
||||||
|
class SkinToneConfig {
|
||||||
|
/// Constructor
|
||||||
|
const SkinToneConfig({
|
||||||
|
this.enabled = true,
|
||||||
|
this.dialogBackgroundColor = Colors.white,
|
||||||
|
this.indicatorColor = Colors.grey,
|
||||||
|
this.rememberSkinTone = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Enable feature to select a skin tone of certain emoji's
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
/// The background color of the skin tone dialog
|
||||||
|
final Color dialogBackgroundColor;
|
||||||
|
|
||||||
|
/// Color of the small triangle next to multiple skin tone emoji
|
||||||
|
final Color indicatorColor;
|
||||||
|
|
||||||
|
/// Remember the last skin tone the user selected.
|
||||||
|
///
|
||||||
|
/// When `true`, the tone chosen via the long-press picker is persisted (in
|
||||||
|
/// `SharedPreferences`) and re-applied as the default for every
|
||||||
|
/// skin-tone-capable emoji in the grid, recents and search on the next
|
||||||
|
/// launch. Selecting the base (no-tone) glyph clears the remembered tone.
|
||||||
|
///
|
||||||
|
/// When `false` (default) the base glyph is always shown, which is the
|
||||||
|
/// previous behavior.
|
||||||
|
final bool rememberSkinTone;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(other) {
|
||||||
|
return (other is SkinToneConfig) &&
|
||||||
|
other.enabled == enabled &&
|
||||||
|
other.dialogBackgroundColor == dialogBackgroundColor &&
|
||||||
|
other.indicatorColor == indicatorColor &&
|
||||||
|
other.rememberSkinTone == rememberSkinTone;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
enabled.hashCode ^
|
||||||
|
dialogBackgroundColor.hashCode ^
|
||||||
|
indicatorColor.hashCode ^
|
||||||
|
rememberSkinTone.hashCode;
|
||||||
|
}
|
||||||
150
emoji_picker_flutter/lib/src/skin_tones/skin_tone_overlay.dart
Normal file
150
emoji_picker_flutter/lib/src/skin_tones/skin_tone_overlay.dart
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Skin tone overlay mixin
|
||||||
|
mixin SkinToneOverlayStateMixin<T extends StatefulWidget> on State<T> {
|
||||||
|
final _utils = EmojiPickerUtils();
|
||||||
|
OverlayEntry? _overlay;
|
||||||
|
|
||||||
|
/// Layer links for skin tone overlay
|
||||||
|
final links = HashMap<String, LayerLink>();
|
||||||
|
|
||||||
|
/// Add target for skin tone overlay if skin tone is available
|
||||||
|
Widget addSkinToneTargetIfAvailable({
|
||||||
|
required bool hasSkinTone,
|
||||||
|
required String linkKey,
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
if (hasSkinTone) {
|
||||||
|
final link = links.putIfAbsent(linkKey, LayerLink.new);
|
||||||
|
return CompositedTransformTarget(link: link, child: child);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overlay close & resources disposal
|
||||||
|
void closeSkinToneOverlay() {
|
||||||
|
_overlay?.remove();
|
||||||
|
_overlay = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overlay for SkinTone
|
||||||
|
void showSkinToneOverlay(
|
||||||
|
Offset emojiBoxPosition,
|
||||||
|
Emoji emoji,
|
||||||
|
double emojiSize,
|
||||||
|
CategoryEmoji? categoryEmoji,
|
||||||
|
Config config,
|
||||||
|
OnEmojiSelected onEmojiSelected,
|
||||||
|
LayerLink link,
|
||||||
|
) {
|
||||||
|
// Generate other skintone options
|
||||||
|
final skinTonesEmoji = SkinTone.values
|
||||||
|
.map((skinTone) => _utils.applySkinTone(emoji, skinTone))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
final emojiPickerRenderbox = context.findRenderObject() as RenderBox;
|
||||||
|
final emojiBoxSize = config.emojiViewConfig.getEmojiBoxSize(
|
||||||
|
emojiPickerRenderbox.size.width,
|
||||||
|
);
|
||||||
|
final left = _calculateLeftOffset(
|
||||||
|
emojiBoxSize,
|
||||||
|
emojiBoxPosition,
|
||||||
|
screenWidth,
|
||||||
|
);
|
||||||
|
final top = _calculateTopOffset(emojiBoxSize);
|
||||||
|
|
||||||
|
_overlay = OverlayEntry(
|
||||||
|
builder: (context) => Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
child: CompositedTransformFollower(
|
||||||
|
offset: Offset(left, top),
|
||||||
|
link: link,
|
||||||
|
showWhenUnlinked: false,
|
||||||
|
child: TapRegion(
|
||||||
|
onTapOutside: (_) => closeSkinToneOverlay(),
|
||||||
|
child: Material(
|
||||||
|
elevation: 4.0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
color: config.skinToneConfig.dialogBackgroundColor,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// The first cell is always the neutral (no-tone) glyph so
|
||||||
|
// it doubles as the "reset" option: selecting it clears any
|
||||||
|
// remembered skin tone. When [emoji] carries a remembered
|
||||||
|
// tone this strips it; otherwise it is already neutral.
|
||||||
|
EmojiCell.fromConfig(
|
||||||
|
emoji: _utils.removeSkinTone(emoji),
|
||||||
|
emojiSize: emojiSize,
|
||||||
|
emojiBoxSize: emojiBoxSize,
|
||||||
|
categoryEmoji: categoryEmoji,
|
||||||
|
onEmojiSelected: onEmojiSelected,
|
||||||
|
config: config,
|
||||||
|
),
|
||||||
|
...List.generate(
|
||||||
|
SkinTone.values.length,
|
||||||
|
(index) => EmojiCell.fromConfig(
|
||||||
|
emoji: skinTonesEmoji[index],
|
||||||
|
emojiSize: emojiSize,
|
||||||
|
emojiBoxSize: emojiBoxSize,
|
||||||
|
categoryEmoji: categoryEmoji,
|
||||||
|
onEmojiSelected: onEmojiSelected,
|
||||||
|
config: config,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_overlay != null) {
|
||||||
|
Overlay.of(context).insert(_overlay!);
|
||||||
|
} else {
|
||||||
|
throw Exception('Nullable skin tone overlay insert attempt');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double _calculateTopOffset(double emojiBoxSize) {
|
||||||
|
final verticalPaddingOverlay = 8.0;
|
||||||
|
final top = -emojiBoxSize - verticalPaddingOverlay;
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _calculateLeftOffset(
|
||||||
|
double emojiBoxSize,
|
||||||
|
Offset emojiBoxPosition,
|
||||||
|
double screenWidth,
|
||||||
|
) {
|
||||||
|
var left = -2.5 * emojiBoxSize;
|
||||||
|
|
||||||
|
if (emojiBoxPosition.dx - 1 * emojiBoxSize < 0) {
|
||||||
|
left += 2.5 * emojiBoxSize;
|
||||||
|
} else if (emojiBoxPosition.dx - 2 * emojiBoxSize < 0) {
|
||||||
|
left += 1.5 * emojiBoxSize;
|
||||||
|
} else if (emojiBoxPosition.dx - 3 * emojiBoxSize < 0) {
|
||||||
|
left += 0.5 * emojiBoxSize;
|
||||||
|
} else if (emojiBoxPosition.dx + 2 * emojiBoxSize > screenWidth) {
|
||||||
|
left -= 2.5 * emojiBoxSize;
|
||||||
|
} else if (emojiBoxPosition.dx + 3 * emojiBoxSize > screenWidth) {
|
||||||
|
left -= 1.5 * emojiBoxSize;
|
||||||
|
} else if (emojiBoxPosition.dx + 4 * emojiBoxSize > screenWidth) {
|
||||||
|
left -= 0.5 * emojiBoxSize;
|
||||||
|
}
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
closeSkinToneOverlay();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Decoration that can be used to render a triangle in the bottom-right
|
||||||
|
/// corner of a container
|
||||||
|
class TriangleDecoration extends Decoration {
|
||||||
|
/// Constructor
|
||||||
|
const TriangleDecoration({required this.color, required this.size}) : super();
|
||||||
|
|
||||||
|
/// Color of the triangle
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
/// Width and height of the triangle
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
@override
|
||||||
|
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
|
||||||
|
return _TriangleShapePainter(color, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TriangleShapePainter extends BoxPainter {
|
||||||
|
/// Constructor
|
||||||
|
/// Expects color that the triangle will be filled with and
|
||||||
|
/// size of the triangle
|
||||||
|
_TriangleShapePainter(Color color, double size) {
|
||||||
|
_painter = Paint()
|
||||||
|
..color = color
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
_size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
late final Paint _painter;
|
||||||
|
late final double _size;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
|
||||||
|
// per documentation, the size should be always not null here, no need
|
||||||
|
// for null checks
|
||||||
|
final s = configuration.size!;
|
||||||
|
var path = Path()
|
||||||
|
..moveTo(s.width + offset.dx, s.height - _size + offset.dy)
|
||||||
|
..lineTo(s.width - _size + offset.dx, s.height + offset.dy)
|
||||||
|
..lineTo(s.width + offset.dx, s.height + offset.dy)
|
||||||
|
..lineTo(s.width + offset.dx, s.height - _size + offset.dy)
|
||||||
|
..close();
|
||||||
|
|
||||||
|
canvas.drawPath(path, _painter);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
emoji_picker_flutter/lib/src/view_order_config.dart
Normal file
45
emoji_picker_flutter/lib/src/view_order_config.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/// View order config
|
||||||
|
class ViewOrderConfig {
|
||||||
|
/// Constructor
|
||||||
|
const ViewOrderConfig({
|
||||||
|
this.top = EmojiPickerItem.categoryBar,
|
||||||
|
this.middle = EmojiPickerItem.emojiView,
|
||||||
|
this.bottom = EmojiPickerItem.searchBar,
|
||||||
|
}) : assert(
|
||||||
|
!identical(top, middle) &&
|
||||||
|
!identical(top, bottom) &&
|
||||||
|
!identical(middle, bottom),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// First item
|
||||||
|
final EmojiPickerItem top;
|
||||||
|
|
||||||
|
/// Middle item
|
||||||
|
final EmojiPickerItem middle;
|
||||||
|
|
||||||
|
/// Last item
|
||||||
|
final EmojiPickerItem bottom;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(other) {
|
||||||
|
return (other is ViewOrderConfig) &&
|
||||||
|
other.top == top &&
|
||||||
|
other.middle == middle &&
|
||||||
|
other.bottom == bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => top.hashCode ^ middle.hashCode ^ bottom.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widgets shown in `EmojiPicker` view
|
||||||
|
enum EmojiPickerItem {
|
||||||
|
/// The tab bar to choose between different emoji categories
|
||||||
|
categoryBar,
|
||||||
|
|
||||||
|
/// The area that shows emojis
|
||||||
|
emojiView,
|
||||||
|
|
||||||
|
/// The search bar to search emojis
|
||||||
|
searchBar,
|
||||||
|
}
|
||||||
111
emoji_picker_flutter/lib/src/widgets/backspace_button.dart
Normal file
111
emoji_picker_flutter/lib/src/widgets/backspace_button.dart
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Backspace Button Widget
|
||||||
|
class BackspaceButton extends StatefulWidget {
|
||||||
|
/// Constructor
|
||||||
|
const BackspaceButton(
|
||||||
|
this.config,
|
||||||
|
this.onBackspacePressed,
|
||||||
|
this.onBackspaceLongPressed,
|
||||||
|
this.iconColor, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Config
|
||||||
|
final Config config;
|
||||||
|
|
||||||
|
/// Backspace callback
|
||||||
|
final VoidCallback? onBackspacePressed;
|
||||||
|
|
||||||
|
/// Backspace long press callback
|
||||||
|
final VoidCallback? onBackspaceLongPressed;
|
||||||
|
|
||||||
|
/// Backspace Icon color
|
||||||
|
final Color iconColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BackspaceButton> createState() => _BackspaceButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BackspaceButtonState extends State<BackspaceButton> {
|
||||||
|
Timer? _onBackspacePressedCallbackTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: GestureDetector(
|
||||||
|
onLongPressStart: (_) => _startOnBackspacePressedCallback(),
|
||||||
|
onLongPressEnd: (_) => _stopOnBackspacePressedCallback(),
|
||||||
|
child: IconButton(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
icon:
|
||||||
|
widget.config.customBackspaceIcon ??
|
||||||
|
Icon(Icons.backspace, color: widget.iconColor),
|
||||||
|
onPressed: () {
|
||||||
|
widget.onBackspacePressed?.call();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_onBackspacePressedCallbackTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the callback for long-pressing the backspace button.
|
||||||
|
void _startOnBackspacePressedCallback() {
|
||||||
|
// Initial callback interval for short presses
|
||||||
|
var callbackInterval = const Duration(milliseconds: 75);
|
||||||
|
var millisecondsSincePressed = 0;
|
||||||
|
|
||||||
|
// Callback function executed on each timer tick
|
||||||
|
void _callback(Timer timer) {
|
||||||
|
// Accumulate elapsed time since the last tick
|
||||||
|
millisecondsSincePressed += callbackInterval.inMilliseconds;
|
||||||
|
|
||||||
|
// If the long-press duration exceeds 3 seconds
|
||||||
|
if (millisecondsSincePressed > 3000 &&
|
||||||
|
callbackInterval == const Duration(milliseconds: 75)) {
|
||||||
|
// Switch to a longer callback interval for word-by-word deletion
|
||||||
|
callbackInterval = const Duration(milliseconds: 300);
|
||||||
|
|
||||||
|
// Cancel the existing timer and start a new one with the updated
|
||||||
|
// interval
|
||||||
|
_onBackspacePressedCallbackTimer?.cancel();
|
||||||
|
_onBackspacePressedCallbackTimer = Timer.periodic(
|
||||||
|
callbackInterval,
|
||||||
|
_callback,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset the elapsed time for the new interval
|
||||||
|
millisecondsSincePressed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the appropriate callback based on the interval
|
||||||
|
if (callbackInterval == const Duration(milliseconds: 75)) {
|
||||||
|
widget.onBackspacePressed?.call(); // Short-press callback
|
||||||
|
} else {
|
||||||
|
widget.onBackspaceLongPressed?.call(); // Long-press callback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the initial timer with the short-press interval
|
||||||
|
_onBackspacePressedCallbackTimer = Timer.periodic(
|
||||||
|
callbackInterval,
|
||||||
|
_callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the callback for long-pressing the backspace button.
|
||||||
|
void _stopOnBackspacePressedCallback() {
|
||||||
|
// Cancel the active timer
|
||||||
|
_onBackspacePressedCallbackTimer?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
164
emoji_picker_flutter/lib/src/widgets/emoji_cell.dart
Normal file
164
emoji_picker_flutter/lib/src/widgets/emoji_cell.dart
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A widget that represents an individual clickable emoji cell.
|
||||||
|
/// Can have a long pressed listener [onSkinToneDialogRequested] that
|
||||||
|
/// provides necessary data to show a skin tone popup.
|
||||||
|
class EmojiCell extends StatelessWidget {
|
||||||
|
/// Constructor for manually setting all properties
|
||||||
|
const EmojiCell({
|
||||||
|
super.key,
|
||||||
|
required this.emoji,
|
||||||
|
required this.emojiSize,
|
||||||
|
required this.emojiBoxSize,
|
||||||
|
this.categoryEmoji,
|
||||||
|
required this.buttonMode,
|
||||||
|
required this.enableSkinTones,
|
||||||
|
required this.textStyle,
|
||||||
|
required this.skinToneIndicatorColor,
|
||||||
|
this.onSkinToneDialogRequested,
|
||||||
|
required this.onEmojiSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Constructor that can retrieve as much information as possible from
|
||||||
|
/// [Config]
|
||||||
|
EmojiCell.fromConfig({
|
||||||
|
super.key,
|
||||||
|
required this.emoji,
|
||||||
|
required this.emojiSize,
|
||||||
|
required this.emojiBoxSize,
|
||||||
|
this.categoryEmoji,
|
||||||
|
required this.onEmojiSelected,
|
||||||
|
this.onSkinToneDialogRequested,
|
||||||
|
required Config config,
|
||||||
|
}) : buttonMode = config.emojiViewConfig.buttonMode,
|
||||||
|
enableSkinTones = config.skinToneConfig.enabled,
|
||||||
|
textStyle = config.emojiTextStyle,
|
||||||
|
skinToneIndicatorColor = config.skinToneConfig.indicatorColor;
|
||||||
|
|
||||||
|
/// Emoji to display as the cell content
|
||||||
|
final Emoji emoji;
|
||||||
|
|
||||||
|
/// Font size for the emoji
|
||||||
|
final double emojiSize;
|
||||||
|
|
||||||
|
/// Hitbox of emoji cell
|
||||||
|
final double emojiBoxSize;
|
||||||
|
|
||||||
|
/// Optinonal category that will be passed through to callbacks
|
||||||
|
final CategoryEmoji? categoryEmoji;
|
||||||
|
|
||||||
|
/// Visual tap feedback, see [ButtonMode] for options
|
||||||
|
final ButtonMode buttonMode;
|
||||||
|
|
||||||
|
/// Whether to show skin popup indicator if emoji supports skin colors
|
||||||
|
final bool enableSkinTones;
|
||||||
|
|
||||||
|
/// Custom text style to use on emoji
|
||||||
|
final TextStyle? textStyle;
|
||||||
|
|
||||||
|
/// Color for skin color indicator triangle
|
||||||
|
final Color skinToneIndicatorColor;
|
||||||
|
|
||||||
|
/// Callback triggered on long press. Will be called regardless
|
||||||
|
/// whether [enableSkinTones] is set or not and for any emoji to
|
||||||
|
/// give a way for the caller to dismiss any existing overlays.
|
||||||
|
final OnSkinToneDialogRequested? onSkinToneDialogRequested;
|
||||||
|
|
||||||
|
/// Callback for a single tap on the cell.
|
||||||
|
final OnEmojiSelected onEmojiSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
onPressed() {
|
||||||
|
onEmojiSelected(categoryEmoji?.category, emoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
onLongPressed() {
|
||||||
|
final renderBox = context.findRenderObject() as RenderBox;
|
||||||
|
final emojiBoxPosition = renderBox.localToGlobal(Offset.zero);
|
||||||
|
onSkinToneDialogRequested?.call(
|
||||||
|
emojiBoxPosition,
|
||||||
|
emoji,
|
||||||
|
emojiSize,
|
||||||
|
categoryEmoji,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: emojiBoxSize,
|
||||||
|
height: emojiBoxSize,
|
||||||
|
child: _buildButtonWidget(
|
||||||
|
onPressed: onPressed,
|
||||||
|
onLongPressed: onLongPressed,
|
||||||
|
child: FittedBox(fit: BoxFit.scaleDown, child: _buildEmoji()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build different Button based on ButtonMode
|
||||||
|
Widget _buildButtonWidget({
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
VoidCallback? onLongPressed,
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
if (buttonMode == ButtonMode.MATERIAL) {
|
||||||
|
return MaterialButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
onLongPress: onLongPressed,
|
||||||
|
elevation: 0,
|
||||||
|
highlightElevation: 0,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (buttonMode == ButtonMode.CUPERTINO) {
|
||||||
|
return GestureDetector(
|
||||||
|
onLongPress: onLongPressed,
|
||||||
|
child: CupertinoButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return GestureDetector(
|
||||||
|
onLongPress: onLongPressed,
|
||||||
|
onTap: onPressed,
|
||||||
|
child: Center(child: child),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build and display Emoji centered of its parent
|
||||||
|
Widget _buildEmoji() {
|
||||||
|
final emojiText = Text(
|
||||||
|
emoji.emoji,
|
||||||
|
textScaler: const TextScaler.linear(1.0),
|
||||||
|
style: _getEmojiTextStyle(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return emoji.hasSkinTone &&
|
||||||
|
enableSkinTones &&
|
||||||
|
onSkinToneDialogRequested != null
|
||||||
|
? Container(
|
||||||
|
decoration: TriangleDecoration(
|
||||||
|
color: skinToneIndicatorColor,
|
||||||
|
size: 8.0,
|
||||||
|
),
|
||||||
|
child: emojiText,
|
||||||
|
)
|
||||||
|
: emojiText;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextStyle _getEmojiTextStyle() {
|
||||||
|
final defaultStyle = DefaultEmojiTextStyle.copyWith(
|
||||||
|
fontSize: emojiSize,
|
||||||
|
inherit: true,
|
||||||
|
);
|
||||||
|
// textStyle properties have priority over defaultStyle
|
||||||
|
return textStyle == null ? defaultStyle : defaultStyle.merge(textStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
emoji_picker_flutter/lib/src/widgets/search_button.dart
Normal file
31
emoji_picker_flutter/lib/src/widgets/search_button.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Search Button Widget
|
||||||
|
class SearchButton extends StatelessWidget {
|
||||||
|
/// Constructor
|
||||||
|
const SearchButton(
|
||||||
|
this.config,
|
||||||
|
this.showSearchView,
|
||||||
|
this.buttonIconColor, {
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Config
|
||||||
|
final Config config;
|
||||||
|
|
||||||
|
/// Show search view callback
|
||||||
|
final VoidCallback showSearchView;
|
||||||
|
|
||||||
|
/// Button icon color
|
||||||
|
final Color buttonIconColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: showSearchView,
|
||||||
|
icon:
|
||||||
|
config.customSearchIcon ?? Icon(Icons.search, color: buttonIconColor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
emoji_picker_flutter/pubspec.yaml
Normal file
49
emoji_picker_flutter/pubspec.yaml
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
name: emoji_picker_flutter
|
||||||
|
description: A Flutter package that provides an Emoji picker widget with 1500+ emojis in 8 categories.
|
||||||
|
version: 4.5.3
|
||||||
|
homepage: https://github.com/Fintasys/emoji_picker_flutter
|
||||||
|
|
||||||
|
screenshots:
|
||||||
|
- description: 'Default emoji picker on Android'
|
||||||
|
path: screenshot/example_custom_font_android.png
|
||||||
|
- description: 'WhatsApp-style emoji view'
|
||||||
|
path: screenshot/example_whatsapp_emoji_view.png
|
||||||
|
- description: 'WhatsApp-style search view'
|
||||||
|
path: screenshot/example_whatsapp_search_view.png
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=3.11.5 <4.0.0"
|
||||||
|
flutter: ">=3.41.8"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_web_plugins:
|
||||||
|
sdk: flutter
|
||||||
|
plugin_platform_interface: ^2.1.8
|
||||||
|
shared_preferences: ^2.3.3
|
||||||
|
web: ^1.1.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^6.0.0
|
||||||
|
test: ^1.25.7
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
plugin:
|
||||||
|
platforms:
|
||||||
|
android:
|
||||||
|
package: com.fintasys.emoji_picker_flutter
|
||||||
|
pluginClass: EmojiPickerFlutterPlugin
|
||||||
|
ios:
|
||||||
|
pluginClass: EmojiPickerFlutterPlugin
|
||||||
|
macos:
|
||||||
|
pluginClass: EmojiPickerFlutterPlugin
|
||||||
|
windows:
|
||||||
|
pluginClass: EmojiPickerFlutterPluginCApi
|
||||||
|
linux:
|
||||||
|
pluginClass: EmojiPickerFlutterPlugin
|
||||||
|
web:
|
||||||
|
pluginClass: EmojiPickerFlutterPluginWeb
|
||||||
|
fileName: emoji_picker_flutter_web.dart
|
||||||
|
|
@ -3,6 +3,9 @@ description: Render After Effects animations natively on Flutter. This package i
|
||||||
version: 3.5.1
|
version: 3.5.1
|
||||||
repository: https://github.com/xvrh/lottie-flutter
|
repository: https://github.com/xvrh/lottie-flutter
|
||||||
|
|
||||||
|
workspace:
|
||||||
|
- example
|
||||||
|
|
||||||
funding:
|
funding:
|
||||||
- https://www.buymeacoffee.com/xvrh
|
- https://www.buymeacoffee.com/xvrh
|
||||||
- https://github.com/sponsors/xvrh
|
- https://github.com/sponsors/xvrh
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue