├── .gitignore ├── .metadata ├── README.md ├── android ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── flutter_app │ │ │ │ └── MainActivity.java │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── gif ├── challenge │ └── reader.gif ├── chat_detail.gif ├── message_page.gif ├── moments.gif ├── music_player.gif └── tiktok_detail.gif ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── flutter_export_environment.sh ├── Podfile ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ └── contents.xcworkspacedata └── Runner │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── main.m ├── lib ├── amazing_page_view.dart ├── app_model.dart ├── bottom_sheet.dart ├── chat_model.dart ├── data_source.dart ├── entities.dart ├── home_model.dart ├── home_page.dart ├── image_banner.dart ├── main.dart ├── memonts_model.dart ├── page │ ├── douban │ │ ├── detail_page.dart │ │ └── douban_page.dart │ ├── qqmusic │ │ ├── audio_player.dart │ │ └── music_player_page.dart │ ├── reader │ │ └── read_page.dart │ ├── tiktok │ │ ├── main.dart │ │ ├── user_profile.dart │ │ └── video_feed.dart │ └── wechat │ │ ├── chat_detail_page.dart │ │ ├── discovery_page.dart │ │ ├── friend_list_page.dart │ │ ├── message_page.dart │ │ ├── moments_page.dart │ │ └── subscription_message_page.dart ├── photo_preview.dart ├── rapid_positioning.dart ├── server │ └── ChatAI.dart ├── subscription_box_model.dart ├── utils.dart └── widgets.dart ├── observable_ui ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib │ ├── core.dart │ ├── core2.dart │ ├── provider.dart │ ├── widgets.dart │ └── widgets2.dart ├── pubspec.lock ├── pubspec.yaml └── test │ └── observable_ui_test.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/ServiceDefinitions.json 65 | **/ios/Runner/GeneratedPluginRegistrant.* 66 | 67 | # Exceptions to above rules. 68 | !**/ios/**/default.mode1v3 69 | !**/ios/**/default.mode2v3 70 | !**/ios/**/default.pbxuser 71 | !**/ios/**/default.perspectivev3 72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 73 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 61ede615c0d28939a25fc54a6533c30e438ac156 8 | channel: master 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fake WeChat 2 | 3 | 模仿微信,抖音等一些主流App的经典UI设计,交互设计。[项目简书地址](https://www.jianshu.com/p/8f35d973b95f)。 4 | 当前目标完成,微信阅读界面,如图。感兴趣的同学可以和我一起尝试开发,这个页面的难度系数还比较大,很具有挑战性。 5 | 6 | ![微信阅读界面](https://github.com/liaobushi520/fake_wechat/blob/master/gif/challenge/reader.gif) 7 | 8 | ## 微信消息列表界面 9 | 10 | ![微信消息列表界面](https://github.com/liaobushi520/fake_wechat/blob/master/gif/message_page.gif) 11 | 12 | ## 微信音乐播放界面 13 | 14 | ![微信音乐播放界面](https://github.com/liaobushi520/fake_wechat/blob/master/gif/music_player.gif) 15 | 16 | ## 微信聊天界面 17 | 18 | ![微信聊天界面](https://github.com/liaobushi520/fake_wechat/blob/master/gif/chat_detail.gif) 19 | 20 | ## 微信朋友圈界面 21 | 22 | ![微信聊天界面](https://github.com/liaobushi520/fake_wechat/blob/master/gif/moments.gif) 23 | 24 | ## 抖音详情页面 25 | 26 | ![抖音详情界面](https://github.com/liaobushi520/fake_wechat/blob/master/gif/tiktok_detail.gif) 27 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 28 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.liaobusi.superapp" 37 | minSdkVersion 18 38 | targetSdkVersion 28 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.12' 59 | androidTestImplementation 'androidx.test:runner:1.1.1' 60 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 61 | } 62 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 13 | 17 | 25 | 29 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/example/flutter_app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.flutter_app; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.app.FlutterActivity; 5 | import io.flutter.plugins.GeneratedPluginRegistrant; 6 | 7 | public class MainActivity extends io.flutter.embedding.android.FlutterActivity { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:3.5.0' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | android.enableR8=true 6 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /gif/challenge/reader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/gif/challenge/reader.gif -------------------------------------------------------------------------------- /gif/chat_detail.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/gif/chat_detail.gif -------------------------------------------------------------------------------- /gif/message_page.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/gif/message_page.gif -------------------------------------------------------------------------------- /gif/moments.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/gif/moments.gif -------------------------------------------------------------------------------- /gif/music_player.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/gif/music_player.gif -------------------------------------------------------------------------------- /gif/tiktok_detail.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/gif/tiktok_detail.gif -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/flutter_export_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This is a generated file; do not edit or check into version control. 3 | export "FLUTTER_ROOT=/Users/liaozhongjun/flutter" 4 | export "FLUTTER_APPLICATION_PATH=/Users/liaozhongjun/AndroidStudioProjects/fake_wechat3" 5 | export "FLUTTER_TARGET=lib/main.dart" 6 | export "FLUTTER_BUILD_DIR=build" 7 | export "SYMROOT=${SOURCE_ROOT}/../build/ios" 8 | export "OTHER_LDFLAGS=$(inherited) -framework Flutter" 9 | export "FLUTTER_FRAMEWORK_DIR=/Users/liaozhongjun/flutter/bin/cache/artifacts/engine/ios" 10 | export "FLUTTER_BUILD_NAME=1.0.0" 11 | export "FLUTTER_BUILD_NUMBER=1" 12 | export "DART_OBFUSCATION=false" 13 | export "TRACK_WIDGET_CREATION=false" 14 | export "TREE_SHAKE_ICONS=false" 15 | export "PACKAGE_CONFIG=.packages" 16 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '8.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | generated_key_values = {} 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) do |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | generated_key_values[podname] = podpath 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | end 32 | generated_key_values 33 | end 34 | 35 | target 'Runner' do 36 | use_frameworks! 37 | use_modular_headers! 38 | source 'https://github.com/CocoaPods/Specs.git' 39 | # Flutter Pod 40 | 41 | copied_flutter_dir = File.join(__dir__, 'Flutter') 42 | copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') 43 | copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') 44 | unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) 45 | # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. 46 | # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. 47 | # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. 48 | 49 | generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') 50 | unless File.exist?(generated_xcode_build_settings_path) 51 | raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" 52 | end 53 | generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) 54 | cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; 55 | 56 | unless File.exist?(copied_framework_path) 57 | FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) 58 | end 59 | unless File.exist?(copied_podspec_path) 60 | FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) 61 | end 62 | end 63 | 64 | # Keep pod path relative so it can be checked into Podfile.lock. 65 | pod 'Flutter', :path => 'Flutter' 66 | 67 | # Plugin Pods 68 | 69 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 70 | # referring to absolute paths on developers' machines. 71 | system('rm -rf .symlinks') 72 | system('mkdir -p .symlinks/plugins') 73 | plugin_pods = parse_KV_file('../.flutter-plugins') 74 | plugin_pods.each do |name, path| 75 | symlink = File.join('.symlinks', 'plugins', name) 76 | File.symlink(path, symlink) 77 | pod name, :path => File.join(symlink, 'ios') 78 | end 79 | end 80 | 81 | post_install do |installer| 82 | installer.pods_project.targets.each do |target| 83 | target.build_configurations.each do |config| 84 | config.build_settings['ENABLE_BITCODE'] = 'NO' 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liaobushi520/fake_wechat/f93238693c23f7326539b83084366de8ea2b1144/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | flutter_app 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/amazing_page_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/foundation.dart'; 3 | import 'package:flutter/gestures.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/rendering.dart'; 6 | 7 | ///stole from PageView ,support add header footer 8 | 9 | final PageController _defaultPageController = PageController(); 10 | const PageScrollPhysics _kPagePhysics = PageScrollPhysics(); 11 | 12 | class AmazingPageView extends StatefulWidget { 13 | /// Creates a scrollable list that works page by page from an explicit [List] 14 | /// of widgets. 15 | /// 16 | /// This constructor is appropriate for page views with a small number of 17 | /// children because constructing the [List] requires doing work for every 18 | /// child that could possibly be displayed in the page view, instead of just 19 | /// those children that are actually visible. 20 | /// 21 | /// {@template flutter.widgets.pageView.allowImplicitScrolling} 22 | /// The [allowImplicitScrolling] parameter must not be null. If true, the 23 | /// [PageView] will participate in accessibility scrolling more like a 24 | /// [ListView], where implicit scroll actions will move to the next page 25 | /// rather than into the contents of the [PageView]. 26 | /// {@endtemplate} 27 | AmazingPageView({ 28 | Key key, 29 | this.scrollDirection = Axis.horizontal, 30 | this.reverse = false, 31 | PageController controller, 32 | this.physics, 33 | this.pageSnapping = true, 34 | this.onPageChanged, 35 | List children = const [], 36 | this.dragStartBehavior = DragStartBehavior.start, 37 | this.allowImplicitScrolling = false, 38 | this.onLoadMore, 39 | this.loadMoreFooter, 40 | }) : assert(allowImplicitScrolling != null), 41 | controller = controller ?? _defaultPageController, 42 | childrenDelegate = SliverChildListDelegate(children), 43 | super(key: key); 44 | 45 | /// Creates a scrollable list that works page by page using widgets that are 46 | /// created on demand. 47 | /// 48 | /// This constructor is appropriate for page views with a large (or infinite) 49 | /// number of children because the builder is called only for those children 50 | /// that are actually visible. 51 | /// 52 | /// Providing a non-null [itemCount] lets the [PageView] compute the maximum 53 | /// scroll extent. 54 | /// 55 | /// [itemBuilder] will be called only with indices greater than or equal to 56 | /// zero and less than [itemCount]. 57 | /// 58 | /// [PageView.builder] by default does not support child reordering. If 59 | /// you are planning to change child order at a later time, consider using 60 | /// [PageView] or [PageView.custom]. 61 | /// 62 | /// {@macro flutter.widgets.pageView.allowImplicitScrolling} 63 | AmazingPageView.builder({ 64 | Key key, 65 | this.scrollDirection = Axis.horizontal, 66 | this.reverse = false, 67 | PageController controller, 68 | this.physics, 69 | this.pageSnapping = true, 70 | this.onPageChanged, 71 | @required IndexedWidgetBuilder itemBuilder, 72 | int itemCount, 73 | this.dragStartBehavior = DragStartBehavior.start, 74 | this.allowImplicitScrolling = false, 75 | this.onLoadMore, 76 | this.loadMoreFooter, 77 | }) : assert(allowImplicitScrolling != null), 78 | controller = controller ?? _defaultPageController, 79 | childrenDelegate = 80 | SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), 81 | super(key: key); 82 | 83 | final Future Function() onLoadMore; 84 | 85 | final Widget loadMoreFooter; 86 | 87 | /// Controls whether the widget's pages will respond to 88 | /// [RenderObject.showOnScreen], which will allow for implicit accessibility 89 | /// scrolling. 90 | /// 91 | /// With this flag set to false, when accessibility focus reaches the end of 92 | /// the current page and the user attempts to move it to the next element, the 93 | /// focus will traverse to the next widget outside of the page view. 94 | /// 95 | /// With this flag set to true, when accessibility focus reaches the end of 96 | /// the current page and user attempts to move it to the next element, focus 97 | /// will traverse to the next page in the page view. 98 | final bool allowImplicitScrolling; 99 | 100 | /// The axis along which the page view scrolls. 101 | /// 102 | /// Defaults to [Axis.horizontal]. 103 | final Axis scrollDirection; 104 | 105 | /// Whether the page view scrolls in the reading direction. 106 | /// 107 | /// For example, if the reading direction is left-to-right and 108 | /// [scrollDirection] is [Axis.horizontal], then the page view scrolls from 109 | /// left to right when [reverse] is false and from right to left when 110 | /// [reverse] is true. 111 | /// 112 | /// Similarly, if [scrollDirection] is [Axis.vertical], then the page view 113 | /// scrolls from top to bottom when [reverse] is false and from bottom to top 114 | /// when [reverse] is true. 115 | /// 116 | /// Defaults to false. 117 | final bool reverse; 118 | 119 | /// An object that can be used to control the position to which this page 120 | /// view is scrolled. 121 | final PageController controller; 122 | 123 | /// How the page view should respond to user input. 124 | /// 125 | /// For example, determines how the page view continues to animate after the 126 | /// user stops dragging the page view. 127 | /// 128 | /// The physics are modified to snap to page boundaries using 129 | /// [PageScrollPhysics] prior to being used. 130 | /// 131 | /// Defaults to matching platform conventions. 132 | final ScrollPhysics physics; 133 | 134 | /// Set to false to disable page snapping, useful for custom scroll behavior. 135 | final bool pageSnapping; 136 | 137 | /// Called whenever the page in the center of the viewport changes. 138 | final ValueChanged onPageChanged; 139 | 140 | /// A delegate that provides the children for the [PageView]. 141 | /// 142 | /// The [PageView.custom] constructor lets you specify this delegate 143 | /// explicitly. The [PageView] and [PageView.builder] constructors create a 144 | /// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder], 145 | /// respectively. 146 | final SliverChildDelegate childrenDelegate; 147 | 148 | /// {@macro flutter.widgets.scrollable.dragStartBehavior} 149 | final DragStartBehavior dragStartBehavior; 150 | 151 | @override 152 | _PageViewState createState() => _PageViewState(); 153 | } 154 | 155 | class _PageViewState extends State { 156 | int _lastReportedPage = 0; 157 | 158 | @override 159 | void initState() { 160 | super.initState(); 161 | _lastReportedPage = widget.controller.initialPage; 162 | } 163 | 164 | AxisDirection _getDirection(BuildContext context) { 165 | switch (widget.scrollDirection) { 166 | case Axis.horizontal: 167 | assert(debugCheckHasDirectionality(context)); 168 | final TextDirection textDirection = Directionality.of(context); 169 | final AxisDirection axisDirection = 170 | textDirectionToAxisDirection(textDirection); 171 | return widget.reverse 172 | ? flipAxisDirection(axisDirection) 173 | : axisDirection; 174 | case Axis.vertical: 175 | return widget.reverse ? AxisDirection.up : AxisDirection.down; 176 | } 177 | return null; 178 | } 179 | 180 | bool _loading = false; 181 | 182 | Widget _createDefaultLoadMoreFooter(BuildContext context) { 183 | return Container( 184 | alignment: Alignment.bottomCenter, 185 | height: 80, 186 | child: Row( 187 | mainAxisAlignment: MainAxisAlignment.center, 188 | children: [ 189 | CircularProgressIndicator(), 190 | Column( 191 | mainAxisAlignment: MainAxisAlignment.center, 192 | children: [ 193 | Text( 194 | "下拉可刷新", 195 | style: TextStyle(color: Colors.white), 196 | ), 197 | Text("上次刷新时间10:10", style: TextStyle(color: Colors.white)) 198 | ], 199 | ) 200 | ], 201 | ), 202 | ); 203 | } 204 | 205 | @override 206 | Widget build(BuildContext context) { 207 | final AxisDirection axisDirection = _getDirection(context); 208 | final ScrollPhysics physics = _ForceImplicitScrollPhysics( 209 | allowImplicitScrolling: widget.allowImplicitScrolling, 210 | ).applyTo(widget.pageSnapping 211 | ? _kPagePhysics.applyTo(widget.physics) 212 | : widget.physics); 213 | 214 | GlobalKey childKey = GlobalKey(); 215 | 216 | Widget loadMoreFooter; 217 | 218 | if (widget.loadMoreFooter != null) { 219 | loadMoreFooter = widget.loadMoreFooter; 220 | } else { 221 | loadMoreFooter = _createDefaultLoadMoreFooter(context); 222 | } 223 | 224 | return NotificationListener( 225 | onNotification: (ScrollNotification notification) { 226 | if (notification.depth == 0 && 227 | widget.onPageChanged != null && 228 | notification is ScrollUpdateNotification) { 229 | final PageMetrics metrics = notification.metrics as PageMetrics; 230 | final int currentPage = metrics.page.round(); 231 | if (currentPage != _lastReportedPage) { 232 | _lastReportedPage = currentPage; 233 | widget.onPageChanged(currentPage); 234 | } 235 | 236 | int childCount; 237 | if (widget.childrenDelegate is SliverChildBuilderDelegate) { 238 | childCount = (widget.childrenDelegate as SliverChildBuilderDelegate) 239 | .childCount; 240 | } else if (widget.childrenDelegate is SliverChildListDelegate) { 241 | childCount = (widget.childrenDelegate as SliverChildListDelegate) 242 | .children 243 | .length; 244 | } 245 | 246 | if ((currentPage == childCount - 1) && _loading == false) { 247 | _loading = true; 248 | widget.onLoadMore().then((v) { 249 | _loading = false; 250 | }).catchError((e) { 251 | double childHeight = 252 | (childKey.currentContext.findRenderObject() as RenderSliver) 253 | .geometry 254 | .maxPaintExtent; 255 | widget.controller 256 | .animateTo(notification.metrics.maxScrollExtent - childHeight, 257 | duration: Duration(milliseconds: 500), curve: Curves.ease) 258 | .then((v) { 259 | _loading = false; 260 | }); 261 | }); 262 | } 263 | } 264 | return false; 265 | }, 266 | child: Scrollable( 267 | dragStartBehavior: widget.dragStartBehavior, 268 | axisDirection: axisDirection, 269 | controller: widget.controller, 270 | physics: physics, 271 | viewportBuilder: (BuildContext context, ViewportOffset position) { 272 | return Viewport( 273 | // TODO(dnfield): we should provide a way to set cacheExtent 274 | // independent of implicit scrolling: 275 | // https://github.com/flutter/flutter/issues/45632 276 | cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0, 277 | cacheExtentStyle: CacheExtentStyle.viewport, 278 | axisDirection: axisDirection, 279 | offset: position, 280 | slivers: [ 281 | SliverFillViewport( 282 | viewportFraction: widget.controller.viewportFraction, 283 | delegate: widget.childrenDelegate, 284 | ), 285 | SliverToBoxAdapter(key: childKey, child: loadMoreFooter) 286 | ], 287 | ); 288 | }, 289 | ), 290 | ); 291 | } 292 | 293 | @override 294 | void debugFillProperties(DiagnosticPropertiesBuilder description) { 295 | super.debugFillProperties(description); 296 | description 297 | .add(EnumProperty('scrollDirection', widget.scrollDirection)); 298 | description.add( 299 | FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed')); 300 | description.add(DiagnosticsProperty( 301 | 'controller', widget.controller, 302 | showName: false)); 303 | description.add(DiagnosticsProperty( 304 | 'physics', widget.physics, 305 | showName: false)); 306 | description.add(FlagProperty('pageSnapping', 307 | value: widget.pageSnapping, ifFalse: 'snapping disabled')); 308 | description.add(FlagProperty('allowImplicitScrolling', 309 | value: widget.allowImplicitScrolling, 310 | ifTrue: 'allow implicit scrolling')); 311 | } 312 | } 313 | 314 | ///该Sliver会位于PageView下方 315 | class SliverToBoxAdapter extends SingleChildRenderObjectWidget { 316 | /// Creates a sliver that contains a single box widget. 317 | const SliverToBoxAdapter({ 318 | Key key, 319 | Widget child, 320 | }) : super(key: key, child: child); 321 | 322 | @override 323 | RenderSliverToBoxAdapter createRenderObject(BuildContext context) => 324 | RenderSliverToBoxAdapter(); 325 | } 326 | 327 | class RenderSliverToBoxAdapter extends RenderSliverSingleBoxAdapter { 328 | /// Creates a [RenderSliver] that wraps a [RenderBox]. 329 | RenderSliverToBoxAdapter({ 330 | RenderBox child, 331 | }) : super(child: child); 332 | 333 | @override 334 | void performLayout() { 335 | if (child == null) { 336 | geometry = SliverGeometry.zero; 337 | return; 338 | } 339 | child.layout(constraints.asBoxConstraints(), parentUsesSize: true); 340 | double childExtent; 341 | switch (constraints.axis) { 342 | case Axis.horizontal: 343 | childExtent = child.size.width; 344 | break; 345 | case Axis.vertical: 346 | childExtent = child.size.height; 347 | break; 348 | } 349 | assert(childExtent != null); 350 | double paintedChildSize = 351 | calculatePaintOffset(constraints, from: 0.0, to: childExtent); 352 | final double cacheExtent = 353 | calculateCacheOffset(constraints, from: 0.0, to: childExtent); 354 | 355 | if (constraints.remainingPaintExtent.floor() <= 0.0) { 356 | paintedChildSize = 0.0; 357 | } 358 | 359 | assert(paintedChildSize.isFinite); 360 | assert(paintedChildSize >= 0.0); 361 | 362 | geometry = SliverGeometry( 363 | layoutExtent: 0, 364 | scrollExtent: childExtent, 365 | paintExtent: paintedChildSize, 366 | paintOrigin: -(childExtent - constraints.remainingPaintExtent), 367 | cacheExtent: cacheExtent, 368 | maxPaintExtent: childExtent, 369 | hitTestExtent: paintedChildSize, 370 | visible: paintedChildSize > 0.0, 371 | hasVisualOverflow: childExtent > constraints.remainingPaintExtent || 372 | constraints.scrollOffset > 0.0, 373 | ); 374 | setChildParentData(child, constraints, geometry); 375 | } 376 | } 377 | 378 | class _ForceImplicitScrollPhysics extends ScrollPhysics { 379 | const _ForceImplicitScrollPhysics({ 380 | @required this.allowImplicitScrolling, 381 | ScrollPhysics parent, 382 | }) : assert(allowImplicitScrolling != null), 383 | super(parent: parent); 384 | 385 | @override 386 | _ForceImplicitScrollPhysics applyTo(ScrollPhysics ancestor) { 387 | return _ForceImplicitScrollPhysics( 388 | allowImplicitScrolling: allowImplicitScrolling, 389 | parent: buildParent(ancestor), 390 | ); 391 | } 392 | 393 | @override 394 | final bool allowImplicitScrolling; 395 | } 396 | -------------------------------------------------------------------------------- /lib/app_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_app/page/qqmusic/audio_player.dart'; 2 | import 'package:flutter_sound/flutter_sound.dart'; 3 | 4 | class AppModel { 5 | final AudioPlayer audioPlayer = AudioPlayer(); 6 | 7 | final FlutterSoundRecorder recorder = FlutterSoundRecorder(); 8 | 9 | final FlutterSoundPlayer player = FlutterSoundPlayer(); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /lib/chat_model.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter_sound/flutter_sound.dart'; 6 | import 'package:observable_ui/core2.dart'; 7 | 8 | import 'entities.dart'; 9 | 10 | class ChatModel { 11 | final Friend friend; 12 | 13 | final ScrollController dialogueScrollControl = ScrollController(); 14 | 15 | Stream recorderSubscription; 16 | 17 | String recordUri; 18 | 19 | ListenableList msgList = ListenableList(); 20 | 21 | ///是否正在录音 22 | ValueNotifier recording = ValueNotifier(false); 23 | 24 | //false :录音 true :文本输入 25 | 26 | ValueNotifier voiceLevel = ValueNotifier(0); 27 | 28 | num duration; 29 | 30 | ChatModel(this.friend); 31 | } 32 | -------------------------------------------------------------------------------- /lib/data_source.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_app/page/douban/douban_page.dart'; 4 | import 'package:flutter_app/page/reader/read_page.dart'; 5 | import 'package:flutter_app/page/tiktok/video_feed.dart'; 6 | 7 | import 'entities.dart'; 8 | import 'page/tiktok/main.dart'; 9 | 10 | const USER = Friend( 11 | name: "廖布斯", 12 | avatar: 13 | "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1236308033,3321919462&fm=26&gp=0.jpg", 14 | momentsCover: 15 | "https://ss0.bdstatic.com/94oJfD_bAAcT8t7mm9GUKT-xh_/timg?image&quality=100&size=b4000_4000&sec=1577093270&di=2bd3f20670a6b468b680664dda873c63&src=http://b-ssl.duitang.com/uploads/item/201709/21/20170921103932_vC4NR.jpeg"); 16 | 17 | const List FRIENDS = [ 18 | Friend( 19 | name: "梁朝伟", 20 | avatar: 21 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1576668990899&di=0d6e27b2fe5b27501d6d2dc3533c5e84&imgtype=0&src=http%3A%2F%2Fpic4.zhimg.com%2F914971f36da5e150cb0a61b171f095eb_b.jpg"), 22 | Friend( 23 | name: "长泽雅美", 24 | avatar: 25 | "https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=3607243162,3473478855&fm=26&gp=0.jpg", 26 | momentsCover: 27 | "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3129531823,304476160&fm=26&gp=0.jpg"), 28 | Friend( 29 | name: "刘德华", 30 | avatar: 31 | "https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3846959827,4200674399&fm=26&gp=0.jpg"), 32 | Friend( 33 | name: "周华健", 34 | avatar: 35 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1576668751759&di=c3c34453866761c90a240068950593c1&imgtype=0&src=http%3A%2F%2Fimg.5nd.com%2F250%2FPhoto%2Fsinger%2F1%2F3039.jpg"), 36 | Friend( 37 | name: "宇多田光", 38 | avatar: 39 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1576668699017&di=6a35e609baa061158e4a526fe3a070e2&imgtype=0&src=http%3A%2F%2Fs9.rr.itc.cn%2Fr%2FwapChange%2F201611_24_12%2Fa2hqy75949515717503.jpeg"), 40 | Friend( 41 | name: "仓木麻衣", 42 | avatar: 43 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1576669076568&di=0936f7a7ec4a3bbc7d4d10ff53b689c9&imgtype=0&src=http%3A%2F%2Fpicm.bbzhi.com%2Fmingxingbizhi%2Fcangmumayikurakimai%2Fstar_starjp_198158_m.jpg"), 44 | Friend( 45 | name: "小栗旬", 46 | avatar: 47 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1576669097850&di=55770837e42cd4c2fbbd5c1d6664ac17&imgtype=0&src=http%3A%2F%2Fztd00.photos.bdimg.com%2Fztd%2Fw%3D700%3Bq%3D50%2Fsign%3D2188551bb83533faf5b6912e98e88c22%2F562c11dfa9ec8a135b778bf5fe03918fa0ecc0b2.jpg"), 48 | Friend( 49 | name: "金城武", 50 | avatar: 51 | "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=282634885,4205940362&fm=26&gp=0.jpg"), 52 | Friend( 53 | name: "张卫健", 54 | avatar: 55 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1577263926&di=07c43b047293b8e95f691cbdaa84344f&imgtype=jpg&er=1&src=http%3A%2F%2Fimg.mp.sohu.com%2Fq_mini%2Cc_zoom%2Cw_640%2Fupload%2F20170721%2F54544c99d89a47c685ef461b5bd85a7c_th.jpg"), 56 | Friend( 57 | name: "赵本山", 58 | avatar: 59 | "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2655657748,3681886583&fm=26&gp=0.jpg"), 60 | Friend( 61 | name: "新垣结衣", 62 | avatar: 63 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1577264341&di=e3a569bc386d0eae544a929852597e15&imgtype=jpg&er=1&src=http%3A%2F%2Fdingyue.ws.126.net%2FhkMGnsbJGT1EkIr6zHmPS0QnlEoUxS2VCenK0BBQhDA7i1550203810262.jpg"), 64 | Friend( 65 | name: "滨崎步", 66 | avatar: 67 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1576669772412&di=67f33543d8384c9770028b66916d5032&imgtype=0&src=http%3A%2F%2Fbigtu.eastday.com%2Fimg%2F201211%2F06%2F89%2F8398021580555610609.jpg"), 68 | Friend( 69 | name: "林青霞", 70 | avatar: 71 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1576669741416&di=bab7afbbd07a94779b3800380624bb6a&imgtype=0&src=http%3A%2F%2Fn1.itc.cn%2Fimg8%2Fwb%2Frecom%2F2017%2F01%2F13%2F148429027077417546.JPEG"), 72 | ]; 73 | 74 | final MIN_PROGRAMS = [ 75 | MinProgram("抖音", 76 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1576472833976&di=bfc3b95448eb89321f4e25ea0fbf2054&imgtype=0&src=http%3A%2F%2Fhbimg.huabanimg.com%2Ff2b2ad85a548a22049f10f90cf32dd8cd9f79b0c90c0-gbkg5j_fw658", 77 | (context, item) { 78 | Navigator.push( 79 | context, 80 | MaterialPageRoute( 81 | builder: (context) => TikTokPage(), 82 | ), 83 | ); 84 | }), 85 | MinProgram("QQ音乐", "http://pic.962.net/up/2016-4/2016418917511892.png", 86 | (context, item) {}), 87 | MinProgram("豆瓣", "http://pic.962.net/up/2016-4/2016418917511892.png", 88 | (context, item) { 89 | Navigator.push( 90 | context, 91 | MaterialPageRoute( 92 | builder: (context) => DoubanPage(), 93 | ), 94 | ); 95 | }), 96 | MinProgram( 97 | "今日头条", 98 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1576669446398&di=1cb9308cd9645a4f434bba43d66056ea&imgtype=0&src=http%3A%2F%2Fimg1.sooshong.com%2Fpics%2F201606%2F4%2F201664223036205.jpg", 99 | 100 | (context, item) {}), 101 | MinProgram("微信阅读", 102 | "https://rescdn.qqmail.com/node/wr/wrpage/style/images/independent/favicon/favicon_48h.png", 103 | (context, item) { 104 | Navigator.push( 105 | context, 106 | MaterialPageRoute( 107 | builder: (context) => ReadPage(), 108 | ), 109 | ); 110 | }), 111 | ]; 112 | final VIDEO_FEEDS = [ 113 | VideoFeed( 114 | url: 115 | 'https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4', 116 | userName: "lzj", 117 | text: "好好玩", 118 | voiceSourceText: "@廖布斯创作的原声-廖布斯"), 119 | VideoFeed( 120 | url: 121 | 'https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4', 122 | userName: "lh", 123 | text: "好好玩吗", 124 | voiceSourceText: "@周星驰创作的原声-周星驰") 125 | ]; 126 | 127 | final CHAT_ENTRANCES = [ 128 | FriendEntrance( 129 | extra: FRIENDS[0], 130 | unreadCount: 3, 131 | recentMessage: Message(0, text: "一起拍电影吧", timestamp: 1000000000)), 132 | FriendEntrance( 133 | extra: FRIENDS[1], 134 | unreadCount: 10, 135 | recentMessage: Message(0, text: "我要来中国了", timestamp: 100000000)), 136 | FriendEntrance( 137 | extra: FRIENDS[2], 138 | unreadCount: 10, 139 | recentMessage: Message(0, text: "有什么电影推荐啊?")), 140 | FriendEntrance( 141 | extra: FRIENDS[3], 142 | unreadCount: 10, 143 | recentMessage: Message(0, text: "我是天王杀手,ok?")), 144 | FriendEntrance( 145 | extra: FRIENDS[4], 146 | unreadCount: 10, 147 | recentMessage: Message(0, text: "你做喜欢我的什么歌?")), 148 | FriendEntrance( 149 | extra: FRIENDS[5], 150 | unreadCount: 10, 151 | recentMessage: Message(0, text: "周末一起吃饭吧")), 152 | FriendEntrance( 153 | extra: FRIENDS[6], 154 | unreadCount: 10, 155 | recentMessage: Message(0, text: "我很帅的啊")), 156 | FriendEntrance( 157 | extra: FRIENDS[7], 158 | unreadCount: 10, 159 | recentMessage: Message(0, text: "我可以是日本人啊!")), 160 | FriendEntrance( 161 | extra: FRIENDS[8], 162 | unreadCount: 10, 163 | recentMessage: Message(0, text: "周末一起吃个饭吧")), 164 | FriendEntrance( 165 | extra: FRIENDS[9], 166 | unreadCount: 10, 167 | recentMessage: Message(0, text: "啥时来看二人转?")), 168 | FriendEntrance( 169 | extra: FRIENDS[10], 170 | unreadCount: 10, 171 | recentMessage: Message(0, text: "你喜欢我的什么呢?")), 172 | FriendEntrance( 173 | extra: FRIENDS[11], 174 | unreadCount: 10, 175 | recentMessage: Message(0, text: "我出新歌了,来听听吧!")), 176 | FriendEntrance( 177 | extra: FRIENDS[12], 178 | unreadCount: 10, 179 | recentMessage: Message(0, text: "还记得我的电影吗?")), 180 | ]; 181 | 182 | final TikTokComments = [ 183 | TiTokComment( 184 | Comment( 185 | "你是好样的", 186 | FRIENDS[0], 187 | likeCount: 1000, 188 | ), 189 | subComments: [ 190 | Comment( 191 | "你才是好样的", 192 | FRIENDS[1], 193 | likeCount: 1111, 194 | ), 195 | Comment( 196 | "你最棒", 197 | FRIENDS[2], 198 | likeCount: 1133, 199 | ) 200 | ]), 201 | TiTokComment( 202 | Comment( 203 | "你是好样的", 204 | FRIENDS[2], 205 | likeCount: 100, 206 | ), 207 | subComments: [ 208 | Comment( 209 | "你才是好样的", 210 | FRIENDS[3], 211 | likeCount: 111, 212 | ) 213 | ]), 214 | TiTokComment( 215 | Comment( 216 | "你是好样的", 217 | FRIENDS[0], 218 | likeCount: 10, 219 | ), 220 | subComments: [ 221 | Comment( 222 | "你才是好样的", 223 | FRIENDS[2], 224 | likeCount: 11, 225 | ) 226 | ]), 227 | TiTokComment( 228 | Comment( 229 | "你是好样的", 230 | FRIENDS[3], 231 | likeCount: 110, 232 | ), 233 | subComments: [ 234 | Comment( 235 | "你才是好样的", 236 | FRIENDS[4], 237 | likeCount: 11, 238 | ) 239 | ]), 240 | TiTokComment( 241 | Comment( 242 | "你是好样的", 243 | FRIENDS[0], 244 | likeCount: 10, 245 | ), 246 | subComments: [ 247 | Comment( 248 | "你才是好样的", 249 | FRIENDS[2], 250 | likeCount: 11, 251 | ) 252 | ]), 253 | TiTokComment( 254 | Comment( 255 | "你是好样的", 256 | FRIENDS[3], 257 | likeCount: 110, 258 | ), 259 | subComments: [ 260 | Comment( 261 | "你才是好样的", 262 | FRIENDS[4], 263 | likeCount: 11, 264 | ) 265 | ]), 266 | TiTokComment( 267 | Comment( 268 | "你是好样的", 269 | FRIENDS[0], 270 | likeCount: 10, 271 | ), 272 | subComments: [ 273 | Comment( 274 | "你才是好样的", 275 | FRIENDS[2], 276 | likeCount: 11, 277 | ) 278 | ]), 279 | TiTokComment( 280 | Comment( 281 | "你是好样的", 282 | FRIENDS[3], 283 | likeCount: 110, 284 | ), 285 | subComments: [ 286 | Comment( 287 | "你才是好样的", 288 | FRIENDS[4], 289 | likeCount: 11, 290 | ) 291 | ]) 292 | ]; 293 | 294 | final MOMENTS = [ 295 | Moment( 296 | text: "我喜欢的一首歌", 297 | friend: FRIENDS[0], 298 | type: 4, 299 | timestamp: 1000123, 300 | audioLink: AudioLink( 301 | cover: 302 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1577271400447&di=35b8834221385b63377f2bdbf64b41d4&imgtype=0&src=http%3A%2F%2Fcdn.music.migu.cn%2Fpicture%2F2018%2F0412%2F1800%2FARTL1610171034331187.jpg", 303 | name: "回忆沙漠", 304 | artist: "杨宗纬", 305 | url: 306 | "https://s128.xiami.net/319/7319/33091/2079859_1504591944341.mp3?ccode=xiami_web_web&expire=86400&duration=240&psid=5d941ef50b2cde1e54c20939cb7827c9&ups_client_netip=180.168.34.146&ups_ts=1577245143&ups_userid=0&utid=gRndEezL1FUCAcuc24qbR1GW&vid=2079859&fn=2079859_1504591944341.mp3&vkey=B31b0177eaa1a08dc891fa635b67134c8"), 307 | likes: [], 308 | comments: []), 309 | Moment( 310 | text: "分享一首好听的歌", 311 | friend: FRIENDS[2], 312 | type: 4, 313 | timestamp: 1000123, 314 | audioLink: AudioLink( 315 | cover: 316 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1577873709&di=97b1734cb9ad28ad9b619901610d74ce&imgtype=jpg&er=1&src=http%3A%2F%2Fcdnmusic.migu.cn%2Fpicture%2F2017%2F1109%2F0716%2FAL1609221019437724.jpg", 317 | name: "你不是真正的快乐", 318 | artist: "五月天", 319 | url:"https://s128.xiami.net/308/2308/12362/152042_1592377026108_6224.mp3?ccode=xiami_web_web&expire=86400&duration=280&psid=02e1c36d691411325f3913aefe724469&ups_client_netip=116.233.173.222&ups_ts=1601197062&ups_userid=0&utid=EH8FFQJPcUwCAbSrci4s0dCj&vid=152042&fn=152042_1592377026108_6224.mp3&vkey=Baf736aa59beec55945d39c5cf7abb0bf"), 320 | likes: [], 321 | comments: []), 322 | Moment( 323 | text: "我今天很开心", 324 | friend: FRIENDS[5], 325 | type: 1, 326 | timestamp: 1000123, 327 | likes: [], 328 | comments: []), 329 | Moment( 330 | text: "看看这个新闻", 331 | friend: FRIENDS[3], 332 | type: 3, 333 | timestamp: 1000123, 334 | likes: [], 335 | webPageLink: WebPageLink( 336 | title: "网易绝情踢员工", 337 | cover: 338 | "http://pics1.baidu.com/feed/91529822720e0cf39dbaefe79e693c1abe09aa16.jpeg?token=7ef31617682291d1ad08c5b64621db06&s=BD9A7F9540224AAEBA0828ED03003033", 339 | url: 340 | "http://baijiahao.baidu.com/s?id=1651222227934364431&wfr=spider&for=pc"), 341 | ), 342 | Moment( 343 | text: "看看这个新闻", 344 | friend: FRIENDS[4], 345 | type: 3, 346 | timestamp: 1000123, 347 | likes: [], 348 | webPageLink: WebPageLink( 349 | title: "网易绝情踢员工", 350 | cover: 351 | "http://pics1.baidu.com/feed/91529822720e0cf39dbaefe79e693c1abe09aa16.jpeg?token=7ef31617682291d1ad08c5b64621db06&s=BD9A7F9540224AAEBA0828ED03003033", 352 | url: 353 | "http://baijiahao.baidu.com/s?id=1651222227934364431&wfr=spider&for=pc"), 354 | ), 355 | Moment( 356 | text: "看看这个新闻", 357 | friend: FRIENDS[1], 358 | type: 3, 359 | timestamp: 1000123, 360 | likes: [], 361 | webPageLink: WebPageLink( 362 | title: "网易绝情踢员工", 363 | cover: 364 | "http://pics1.baidu.com/feed/91529822720e0cf39dbaefe79e693c1abe09aa16.jpeg?token=7ef31617682291d1ad08c5b64621db06&s=BD9A7F9540224AAEBA0828ED03003033", 365 | url: 366 | "http://baijiahao.baidu.com/s?id=1651222227934364431&wfr=spider&for=pc"), 367 | ), 368 | Moment( 369 | text: "我今天很开心", 370 | friend: FRIENDS[0], 371 | type: 2, 372 | timestamp: 1000123, 373 | images: [ 374 | "http://b-ssl.duitang.com/uploads/item/201811/04/20181104074412_wcelx.jpg", 375 | "http://b-ssl.duitang.com/uploads/item/201811/04/20181104074412_wcelx.jpg", 376 | "http://b-ssl.duitang.com/uploads/item/201811/04/20181104074412_wcelx.jpg" 377 | ], 378 | likes: [], 379 | comments: [ 380 | Comment("好样的", FRIENDS[1]), 381 | Comment("你是个人才", FRIENDS[1]), 382 | Comment("你才是个人才", FRIENDS[0], replyer: FRIENDS[1]), 383 | ]) 384 | ]; 385 | -------------------------------------------------------------------------------- /lib/entities.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | 5 | class Item {} 6 | 7 | class Message implements Item { 8 | const Message( 9 | this.type, { 10 | this.sender, 11 | this.receiver, 12 | this.file, 13 | this.text, 14 | this.url, 15 | this.duration, 16 | this.timestamp, 17 | }); 18 | 19 | final int type; //0 文本 1 图片 2 声音 3 红包 20 | 21 | final File file; 22 | 23 | final String text; 24 | 25 | final String url; 26 | 27 | final num duration; 28 | 29 | final num timestamp; 30 | 31 | final Friend sender; 32 | 33 | final Friend receiver; 34 | } 35 | 36 | class Marker implements Item { 37 | const Marker(this.type, this.text); 38 | 39 | final int type; 40 | 41 | final String text; 42 | } 43 | 44 | class Friend { 45 | const Friend({ 46 | this.name, 47 | this.avatar, 48 | this.momentsCover, 49 | }); 50 | 51 | final String name; 52 | 53 | final String avatar; 54 | 55 | ///朋友圈封面 56 | final String momentsCover; 57 | } 58 | 59 | class Comment { 60 | Comment(this.text, this.poster, {this.replyer, this.timestamp, this.likeCount=0,this.iLike=false}); 61 | 62 | final String text; 63 | 64 | //评论者 65 | final Friend poster; 66 | 67 | //回复评论者 68 | final Friend replyer; 69 | 70 | final num timestamp; 71 | 72 | num likeCount=0 ; 73 | 74 | bool iLike=false; 75 | } 76 | 77 | class TiTokComment{ 78 | 79 | final Comment mainComment; 80 | 81 | final List subComments; 82 | 83 | TiTokComment(this.mainComment,{ this.subComments}); 84 | 85 | } 86 | 87 | class AudioLink { 88 | final String cover; 89 | 90 | final String name; 91 | 92 | final String artist; 93 | 94 | final String url; 95 | 96 | AudioLink({this.cover, this.name, this.artist, this.url}); 97 | } 98 | 99 | class WebPageLink { 100 | final String cover; 101 | 102 | final String title; 103 | 104 | final String url; 105 | 106 | WebPageLink({this.cover, this.title, this.url}); 107 | } 108 | 109 | class Moment { 110 | final Friend friend; 111 | 112 | final String text; 113 | 114 | final int type; //1 纯文本 2:带有图片 3:网页链接 4 :音频链接 115 | 116 | final AudioLink audioLink; 117 | 118 | final List images; 119 | 120 | final WebPageLink webPageLink; 121 | 122 | final num timestamp; 123 | 124 | final List likes; 125 | 126 | final List comments; 127 | 128 | Moment({ 129 | this.friend, 130 | this.text, 131 | this.type, 132 | this.audioLink, 133 | this.timestamp, 134 | this.images, 135 | this.webPageLink, 136 | this.likes, 137 | this.comments = const [], 138 | }) : assert(!(type == 4 && audioLink == null), 139 | "mement type is 4 ,but audio link is null"), 140 | assert(!(type == 3 && webPageLink == null), 141 | "mement type is 3 ,but web page link is null"), 142 | assert( 143 | !(type == 1 && text == null), "mement type is 1 ,but text is null"), 144 | assert(!(type == 2 && (images == null || images.length <= 0)), 145 | "mement type is 2 ,but images is null"); 146 | } 147 | 148 | abstract class Entrance { 149 | final int unreadCount; 150 | 151 | final Message recentMessage; 152 | 153 | String icon; 154 | 155 | String name; 156 | 157 | final T extra; 158 | 159 | Entrance({this.extra, this.unreadCount, this.recentMessage}); 160 | } 161 | 162 | class FriendEntrance extends Entrance { 163 | FriendEntrance({Friend extra, int unreadCount, Message recentMessage}) 164 | : super( 165 | extra: extra, 166 | unreadCount: unreadCount, 167 | recentMessage: recentMessage); 168 | 169 | @override 170 | String get name => super.extra.name; 171 | 172 | @override 173 | String get icon => super.extra.avatar; 174 | } 175 | 176 | class MinProgram { 177 | final String name; 178 | 179 | final String icon; 180 | 181 | final void Function(BuildContext context, MinProgram minProgram) onEnter; 182 | 183 | const MinProgram(this.name, this.icon, this.onEnter); 184 | } 185 | 186 | class VideoFeed { 187 | final String url; 188 | 189 | final String userName; 190 | 191 | final String voiceSourceText; 192 | 193 | final String text; 194 | 195 | final String voiceSourceCover; 196 | 197 | VideoFeed({ 198 | this.url, 199 | this.userName, 200 | this.voiceSourceText, 201 | this.text, 202 | this.voiceSourceCover, 203 | }); 204 | } 205 | 206 | 207 | -------------------------------------------------------------------------------- /lib/home_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_app/data_source.dart'; 2 | import 'package:flutter_app/entities.dart'; 3 | import 'package:observable_ui/core.dart'; 4 | 5 | class HomeModel { 6 | ObservableList chatItems = 7 | ObservableList(initValue: CHAT_ENTRANCES); 8 | } 9 | -------------------------------------------------------------------------------- /lib/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_app/page/wechat/message_page.dart'; 4 | import 'page/wechat/discovery_page.dart'; 5 | import 'page/wechat/friend_list_page.dart'; 6 | 7 | class HomePage extends StatefulWidget { 8 | @override 9 | State createState() { 10 | return HomeState(); 11 | } 12 | } 13 | 14 | class HomeState extends State { 15 | final PageController _pageController = PageController(); 16 | 17 | int currentIndex = 0; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Scaffold( 22 | body: SafeArea( 23 | child: PageView( 24 | controller: _pageController, 25 | onPageChanged: (index) { 26 | setState(() { 27 | currentIndex = index; 28 | }); 29 | }, 30 | children: [ 31 | MessagePage(), 32 | FriendListPage(), 33 | DiscoveryPage(), 34 | ], 35 | ), 36 | ), 37 | bottomNavigationBar: BottomNavigationBar( 38 | items: const [ 39 | BottomNavigationBarItem( 40 | icon: Icon(Icons.home), 41 | title: Text('聊天'), 42 | ), 43 | BottomNavigationBarItem( 44 | icon: Icon(Icons.contacts), 45 | title: Text('联系人'), 46 | ), 47 | BottomNavigationBarItem( 48 | icon: Icon(Icons.people), 49 | title: Text('发现'), 50 | ), 51 | ], 52 | currentIndex: currentIndex, 53 | selectedItemColor: Color.fromARGB(255, 88, 191, 107), 54 | onTap: (value) { 55 | setState(() { 56 | currentIndex = value; 57 | }); 58 | _pageController.jumpToPage(value); 59 | }, 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/image_banner.dart: -------------------------------------------------------------------------------- 1 | library flutter_package; 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:flutter/widgets.dart'; 6 | 7 | // ignore: must_be_immutable 8 | class ImageBanner extends StatefulWidget { 9 | ImageBanner({Key key, @required this.images}) : super(key: key); 10 | 11 | List images; 12 | 13 | @override 14 | State createState() { 15 | return ImageBannerState(); 16 | } 17 | } 18 | 19 | class ImageBannerState extends State { 20 | PageController _pageController; 21 | Timer _timer; 22 | int _currentPage; 23 | 24 | bool next = true; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | _pageController = PageController(); 30 | _timer = Timer.periodic(Duration(seconds: 2), (timer) { 31 | doScroll(timer); 32 | }); 33 | } 34 | 35 | void doScroll(Timer timer) { 36 | if (widget.images.length == 1) { 37 | return; 38 | } 39 | if (_currentPage == widget.images.length - 1) { 40 | if (next) { 41 | next = false; 42 | } 43 | } 44 | if (_currentPage == 0) { 45 | if (!next) { 46 | next = true; 47 | } 48 | } 49 | if (next) { 50 | _pageController.nextPage( 51 | duration: Duration(seconds: 1), curve: Interval(0, 0.5)); 52 | } else { 53 | _pageController.previousPage( 54 | duration: Duration(seconds: 1), curve: Interval(0, 0.5)); 55 | } 56 | } 57 | 58 | @override 59 | void dispose() { 60 | super.dispose(); 61 | _timer.cancel(); 62 | } 63 | 64 | @override 65 | Widget build(BuildContext context) { 66 | var children = widget.images 67 | .toList() 68 | .map((image) => Image.network( 69 | image, 70 | width: 200, 71 | height: 200, 72 | )) 73 | .toList(); 74 | return Container( 75 | child: PageView( 76 | children: children, 77 | controller: _pageController, 78 | onPageChanged: (page) => {_currentPage = page}, 79 | ), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_app/memonts_model.dart'; 4 | 5 | import 'package:flutter_app/page/wechat/moments_page.dart'; 6 | import 'package:flutter_app/page/wechat/subscription_message_page.dart'; 7 | import 'package:flutter_app/subscription_box_model.dart'; 8 | import 'package:observable_ui/provider.dart'; 9 | 10 | import 'app_model.dart'; 11 | import 'home_model.dart'; 12 | import 'home_page.dart'; 13 | 14 | void main() => runApp(MyApp()); 15 | 16 | class MyApp extends StatelessWidget { 17 | @override 18 | Widget build(BuildContext context) { 19 | return ViewModelProvider( 20 | child: MaterialApp( 21 | title: 'WeChat', 22 | theme: ThemeData( 23 | primarySwatch: Colors.green, 24 | backgroundColor: Colors.transparent), 25 | home:ViewModelProvider( 26 | viewModel: HomeModel(), 27 | child: HomePage(), 28 | ) , 29 | routes: { 30 | "/subscription_box": (context) { 31 | return ViewModelProvider( 32 | child: SubscriptionBoxPage(), 33 | viewModel: SubscriptionBoxModel(), 34 | ); 35 | }, 36 | }, 37 | ), 38 | viewModel: AppModel(), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/memonts_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter_app/data_source.dart'; 3 | import 'package:flutter_app/entities.dart'; 4 | import 'package:observable_ui/core2.dart'; 5 | 6 | 7 | 8 | class MomentsModel { 9 | ListenableList moments = ListenableList(initValue: MOMENTS); 10 | 11 | ValueNotifier showCommentEdit = ValueNotifier(false); 12 | } 13 | -------------------------------------------------------------------------------- /lib/page/douban/detail_page.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:flutter/material.dart'; 3 | 4 | 5 | ///Todo 6 | 7 | class MovieDetailPage extends StatefulWidget{ 8 | @override 9 | State createState() { 10 | return MovieDetailPageState(); 11 | } 12 | 13 | 14 | } 15 | 16 | class MovieDetailPageState extends State{ 17 | @override 18 | Widget build(BuildContext context) { 19 | return Column( 20 | children: [ 21 | DecoratedBox( 22 | decoration: BoxDecoration(image: DecorationImage( 23 | image: NetworkImage("https://movie.douban.com/subject/26348103/photos?type=R"), 24 | )), 25 | ), 26 | Row( 27 | children: [ 28 | Text("小妇人"), 29 | Text("little woman"), 30 | Text("美国/剧情"), 31 | 32 | Column( 33 | 34 | children: [ 35 | 36 | 37 | ], 38 | ) 39 | 40 | 41 | ], 42 | ), 43 | 44 | 45 | ], 46 | ); 47 | 48 | } 49 | } -------------------------------------------------------------------------------- /lib/page/douban/douban_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class DoubanPage extends StatefulWidget { 4 | @override 5 | State createState() { 6 | return DoubanPageState(); 7 | } 8 | } 9 | 10 | class Star extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | return Icon(Icons.star); 14 | } 15 | } 16 | 17 | class DoubanPageState extends State { 18 | Widget _buildHotItem() { 19 | return Column( 20 | children: [ 21 | Stack(children: [ 22 | Image.network("https://img9.doubanio.com/view/photo/l/public/p2580665456.webp"), 23 | 24 | ],), 25 | Text("小妇人"), 26 | Row( 27 | children: [ 28 | Star(), 29 | Star(), 30 | Star(), 31 | Star(), 32 | Text("8.1"), 33 | ], 34 | ), 35 | ], 36 | ); 37 | } 38 | 39 | Widget _buildHotBoard() { 40 | return GridView.count( 41 | crossAxisCount: 3, 42 | children: [_buildHotItem(),_buildHotItem(),_buildHotItem()], 43 | ); 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return Scaffold(body: _buildHotBoard(),); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/page/qqmusic/audio_player.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter_app/utils.dart'; 4 | import 'package:flutter_sound/flutter_sound.dart'; 5 | 6 | import '../../entities.dart'; 7 | 8 | class Response { 9 | final bool success; 10 | 11 | final String message; 12 | 13 | Response(this.success, this.message); 14 | } 15 | 16 | class AudioPlayer { 17 | final FlutterSoundPlayer flutterSound = FlutterSoundPlayer(); 18 | 19 | AudioLink currentSong; 20 | 21 | StreamController _playStreamController = 22 | StreamController.broadcast(); 23 | 24 | Stream get playStream => _playStreamController.stream; 25 | 26 | ///Flutter Sound 不会告诉我们音乐是否暂停,所以需要自己维护 27 | bool _paused = false; 28 | 29 | PlayEvent _lastPlayEvent; 30 | 31 | Future _startPlay(AudioLink song) async { 32 | if (flutterSound.isPlaying) { 33 | await flutterSound.stopPlayer(); 34 | } 35 | var startPlayResponse = await flutterSound.startPlayer(fromURI: song.url); 36 | if (startPlayResponse != null) { 37 | currentSong = song; 38 | var event = PlayEvent(currentSong, 1, 0, 0); 39 | _lastPlayEvent = event; 40 | _playStreamController.add(event); 41 | flutterSound.dispositionStream().listen((PlaybackDisposition v) { 42 | if (v == null || _paused) { 43 | return; 44 | } 45 | PlayEvent event = (v.position == v.duration) 46 | ? PlayEvent(currentSong, -1, v.position.inMilliseconds, 47 | startPlayResponse.inMilliseconds) 48 | : PlayEvent(currentSong, 1, v.position.inMilliseconds, 49 | startPlayResponse.inMilliseconds); 50 | 51 | _lastPlayEvent = event; 52 | _playStreamController.add(event); 53 | }); 54 | 55 | Future.value(true); 56 | } else { 57 | return Future.value(false); 58 | } 59 | } 60 | 61 | Future _stopPlay() async { 62 | if (flutterSound.isPlaying) { 63 | await flutterSound.stopPlayer(); 64 | var event = PlayEvent(null, -1, 0, 0); 65 | _playStreamController.add(event); 66 | currentSong = null; 67 | _lastPlayEvent = null; 68 | _paused = false; 69 | return Future.value(true); 70 | } else { 71 | Future.value(false); 72 | } 73 | 74 | return Future.value(true); 75 | } 76 | 77 | //// time mills 78 | Future seekTo(int time) async { 79 | if (flutterSound.isPlaying) { 80 | await flutterSound.seekToPlayer(Duration(milliseconds: time)); 81 | } 82 | } 83 | 84 | Future _pauseOrResume() async { 85 | if (_lastPlayEvent.status == 1) { 86 | await flutterSound.pausePlayer(); 87 | var event = PlayEvent(currentSong, 0, _lastPlayEvent.currentPosition, 88 | _lastPlayEvent.duration); 89 | _lastPlayEvent = event; 90 | _playStreamController.add(event); 91 | _paused = true; 92 | return Future.value(true); 93 | } else if (_lastPlayEvent.status == 0) { 94 | await flutterSound.resumePlayer(); 95 | var event = PlayEvent(currentSong, 1, _lastPlayEvent.currentPosition, 96 | _lastPlayEvent.duration); 97 | _lastPlayEvent = event; 98 | _playStreamController.add(event); 99 | _paused = false; 100 | return Future.value(true); 101 | } 102 | 103 | } 104 | 105 | Future playOrPause([AudioLink audioLink]) async { 106 | print("play or pause"); 107 | if (audioLink != null) { 108 | ///同一首歌我们认为是暂停,恢复操作 109 | if (audioLink == currentSong) { 110 | return _pauseOrResume(); 111 | } 112 | await _stopPlay(); 113 | return _startPlay(audioLink); 114 | } 115 | 116 | return _pauseOrResume(); 117 | } 118 | 119 | Future playOrStop([AudioLink audioLink, bool replay = false]) async { 120 | if (audioLink == null) { 121 | return _stopPlay(); 122 | } 123 | if (!flutterSound.isPlaying) { 124 | return _startPlay(audioLink); 125 | } else { 126 | if (audioLink == currentSong && !replay) { 127 | return _stopPlay(); 128 | } 129 | await _stopPlay(); 130 | return _startPlay(audioLink); 131 | } 132 | }} 133 | 134 | class PlayEvent { 135 | final AudioLink audio; 136 | 137 | final int status; // 0 pause 1 play -1 stop 138 | 139 | ///mills 140 | final int currentPosition; 141 | 142 | ///mills 143 | final int duration; //音乐总时长 -1未知 144 | 145 | String get currentPositionText => formatHHmmSS(currentPosition / 1000); 146 | 147 | String get durationText => formatHHmmSS(duration / 1000); 148 | 149 | const PlayEvent(this.audio, this.status, this.currentPosition, this.duration); 150 | } 151 | -------------------------------------------------------------------------------- /lib/page/reader/read_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'dart:ui' as ui; 3 | 4 | import 'package:flutter/cupertino.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/rendering.dart'; 7 | 8 | const BG_COLORS = [Color.fromARGB(255, 244, 235, 219)]; 9 | 10 | class ReadPage extends StatefulWidget { 11 | @override 12 | State createState() { 13 | return ReadPageState(); 14 | } 15 | } 16 | 17 | class ReadPageState extends State { 18 | bool operateLayerVisible = false; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Scaffold( 23 | body: Stack( 24 | children: [ 25 | GestureDetector( 26 | child: PageView.builder( 27 | itemCount: 100, 28 | itemBuilder: (context, index) { 29 | return BookPage( 30 | index: index, 31 | ); 32 | }), 33 | onTap: () { 34 | setState(() { 35 | operateLayerVisible = !operateLayerVisible; 36 | }); 37 | }, 38 | ), 39 | Opacity( 40 | opacity: operateLayerVisible ? 1 : 0, 41 | child: OperateLayer(), 42 | ) 43 | ], 44 | ), 45 | ); 46 | } 47 | } 48 | 49 | class BookPage extends StatefulWidget { 50 | final int index; 51 | 52 | const BookPage({Key key, this.index}) : super(key: key); 53 | 54 | @override 55 | State createState() { 56 | return BookPageState(); 57 | } 58 | } 59 | 60 | class BookPageState extends State with AutomaticKeepAliveClientMixin { 61 | GlobalKey globalKey = GlobalKey(); 62 | FocusNode focusNode = FocusNode(); 63 | 64 | AnimationController _animationController; 65 | 66 | TextEditingController controller = TextEditingController( 67 | text: 68 | "前不久听说,业内最近出了《人类简史》这么一本“奇书”,作者是个名叫尤瓦尔·赫拉利的以色列年轻人。此书在2012年以希伯来文出版,很快就被翻译成近30种文字,不仅为全球学术界所瞩目,而且引起了公众的广泛兴趣。一部世界史新著竟能“火”成这样,实在是前所未闻。所以,当中信出版社请我为本书的中文版作序时,我也就出于好奇而暂时应承了下来:“先看看吧。”而这一看,我就立刻“着道”了——拿起了就放不下,几乎是一口气读完。吸引力主要来自作者才思的旷达敏捷,还有译者文笔的生动晓畅。而书中屡屡提及中国的相关史实,也能让人感到一种说不出的亲切,好像自己也融入其中,读来欲罢不能。后来看了策划编辑舒婷的特别说明,才知道该书中文版所参照的英文版,原来是作者特地为中国读者“量身定做”的。他给各国的版本也都下过同样的功夫——作者的功力之深,由此可见一斑。"); 69 | 70 | 71 | OverlayEntry overlayEntry; 72 | 73 | ScrollController scrollController = ScrollController(); 74 | 75 | final MAGNIFIER_SIZE = 150.0; 76 | 77 | @override 78 | void initState() { 79 | super.initState(); 80 | 81 | scrollController.addListener(() { 82 | print("${scrollController.offset}"); 83 | }); 84 | } 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | super.build(context); 89 | return CaptureScreenContainer( 90 | key: globalKey, 91 | child: Container( 92 | child: Listener( 93 | child: TextField( 94 | // selectionControls: , 95 | expands: true, 96 | scrollController: scrollController, 97 | focusNode: focusNode, 98 | cursorColor: Color.fromARGB(255, 150, 118, 60), 99 | // backgroundCursorColor: Colors.redAccent, 100 | controller: controller, 101 | readOnly: true, 102 | maxLines: null, 103 | minLines: null, 104 | style: TextStyle(color: Colors.black, fontSize: 24), 105 | decoration: null, 106 | ), 107 | onPointerDown: (event) async {}, 108 | onPointerMove: (detail) { 109 | RenderBox renderObject = 110 | globalKey.currentContext.findRenderObject(); 111 | if (renderObject is RenderCaptureScreen) { 112 | var image =renderObject.image; 113 | double dx = 114 | -1 + (2 * detail.localPosition.dx / renderObject.size.width); 115 | double dy = -1 + 116 | (2 * 117 | (detail.localPosition.dy + scrollController.offset) / 118 | (scrollController.offset + renderObject.size.height)); 119 | overlayEntry?.remove(); 120 | double left, top; 121 | if (detail.localPosition.dx <= MAGNIFIER_SIZE / 2) { 122 | left = 0; 123 | } else { 124 | left = detail.localPosition.dx - MAGNIFIER_SIZE / 2; 125 | } 126 | 127 | if (detail.localPosition.dy + MAGNIFIER_SIZE > 128 | renderObject.size.height) { 129 | top = detail.localPosition.dy - MAGNIFIER_SIZE; 130 | } else { 131 | top = detail.localPosition.dy; 132 | } 133 | 134 | overlayEntry = OverlayEntry(builder: (context) { 135 | return Positioned( 136 | left: left, 137 | top: top, 138 | width: MAGNIFIER_SIZE, 139 | height: MAGNIFIER_SIZE, 140 | child: ClipOval( 141 | child: Container( 142 | // foregroundDecoration: BoxDecoration( shape: BoxShape.circle,boxShadow: [BoxShadow()]), 143 | child: Image.memory( 144 | image, 145 | fit: BoxFit.none, 146 | scale: 1, 147 | alignment: Alignment(dx, dy), 148 | ), 149 | ), 150 | )); 151 | }); 152 | Overlay.of(context).insert(overlayEntry); 153 | } 154 | }, 155 | onPointerUp: (event) { 156 | overlayEntry?.remove(); 157 | overlayEntry = null; 158 | }, 159 | ), 160 | color: BG_COLORS[0], 161 | ), 162 | ); 163 | } 164 | 165 | @override 166 | bool get wantKeepAlive => true; 167 | } 168 | 169 | class OperateLayer extends StatefulWidget { 170 | @override 171 | State createState() { 172 | return OperateLayerState(); 173 | } 174 | } 175 | 176 | class OperateLayerState extends State { 177 | @override 178 | Widget build(BuildContext context) { 179 | return Stack( 180 | children: [ 181 | Positioned( 182 | top: 0, 183 | child: Row( 184 | children: [ 185 | Icon(Icons.book), 186 | ], 187 | ), 188 | ), 189 | Positioned( 190 | bottom: 0, 191 | child: Row( 192 | children: [ 193 | Icon(Icons.lightbulb_outline), 194 | ], 195 | ), 196 | ), 197 | ], 198 | ); 199 | } 200 | } 201 | 202 | class CaptureScreenContainer extends SingleChildRenderObjectWidget { 203 | const CaptureScreenContainer({Key key, Widget child}) 204 | : super(key: key, child: child); 205 | 206 | @override 207 | RenderObject createRenderObject(BuildContext context) { 208 | return RenderCaptureScreen(); 209 | } 210 | } 211 | 212 | class RenderCaptureScreen extends RenderProxyBox { 213 | @override 214 | bool get isRepaintBoundary => true; 215 | 216 | Uint8List image; 217 | 218 | RenderCaptureScreen({RenderBox child}) : super(child); 219 | 220 | @override 221 | void paint(PaintingContext context, Offset offset) { 222 | super.paint(context, offset); 223 | final OffsetLayer offsetLayer = layer as OffsetLayer; 224 | offsetLayer?.toImage(Offset.zero & size, pixelRatio: 1.0)?.then((value){ 225 | return value.toByteData(format: ui.ImageByteFormat.png); 226 | })?.then((value){ 227 | return value.buffer.asUint8List(); 228 | })?.then((value){ 229 | print("update image"); 230 | image=value; 231 | }); 232 | 233 | } 234 | 235 | } 236 | -------------------------------------------------------------------------------- /lib/page/tiktok/main.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_app/page/tiktok/user_profile.dart'; 6 | import 'package:flutter_app/page/tiktok/video_feed.dart'; 7 | import 'package:observable_ui/provider.dart'; 8 | 9 | class TikTokPage extends StatefulWidget { 10 | @override 11 | State createState() { 12 | return TikTokPageState(); 13 | } 14 | } 15 | 16 | class TikTokPageState extends State { 17 | double _tabBarLeft = 0; 18 | 19 | double _tabBarIndicatorRadio = 0; 20 | 21 | PageController _pageController = PageController(); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | return ViewModelProvider( 26 | viewModel: VideoFeedModel(), 27 | child: Scaffold( 28 | body: Container( 29 | color: Colors.black, 30 | child: NotificationListener( 31 | child: Stack( 32 | children: [ 33 | PageView( 34 | controller: _pageController, 35 | scrollDirection: Axis.horizontal, 36 | children: [ 37 | VideoFeedsScreen(), 38 | VideoFeedsScreen(), 39 | UserProfileScreen() 40 | ], 41 | ), 42 | Positioned( 43 | child: SafeArea( 44 | child: Center( 45 | child: TabBar( 46 | radio: _tabBarIndicatorRadio, 47 | onSelected: (pos) { 48 | _pageController.animateToPage(pos, 49 | duration: Duration(milliseconds: 500), 50 | curve: Curves.ease); 51 | }, 52 | ), 53 | ), 54 | ), 55 | left: _tabBarLeft, 56 | right: -_tabBarLeft, 57 | top: 10, 58 | ), 59 | ], 60 | ), 61 | onNotification: (ScrollNotification notification) { 62 | if (notification.depth == 0 && 63 | notification is ScrollUpdateNotification && 64 | notification.metrics is PageMetrics) { 65 | if (notification.metrics.pixels >= 66 | notification.metrics.viewportDimension) { 67 | var delta = (notification.metrics.pixels - 68 | notification.metrics.viewportDimension); 69 | setState(() { 70 | _tabBarLeft = -delta; 71 | }); 72 | } else { 73 | var radio = (notification.metrics.pixels / 74 | notification.metrics.viewportDimension) 75 | .clamp(0, 1); 76 | setState(() { 77 | _tabBarIndicatorRadio = radio; 78 | }); 79 | } 80 | } 81 | return true; 82 | }, 83 | ), 84 | ), 85 | ), 86 | ); 87 | } 88 | } 89 | 90 | class TabBar extends StatefulWidget { 91 | final double radio; 92 | 93 | final void Function(int pos) onSelected; 94 | 95 | const TabBar({Key key, this.radio, this.onSelected}) : super(key: key); 96 | 97 | @override 98 | State createState() { 99 | return TabBarState(); 100 | } 101 | } 102 | 103 | class TabBarState extends State { 104 | static const TAB_WIDTH = 70.0; 105 | 106 | @override 107 | Widget build(BuildContext context) { 108 | return Column( 109 | children: [ 110 | Row( 111 | mainAxisSize: MainAxisSize.min, 112 | children: [ 113 | GestureDetector( 114 | child: Container( 115 | child: Text( 116 | "关注", 117 | style: TextStyle( 118 | color: Colors.white, 119 | fontSize: 18, 120 | fontWeight: FontWeight.bold), 121 | textAlign: TextAlign.center, 122 | ), 123 | width: TAB_WIDTH, 124 | ), 125 | onTap: () { 126 | widget.onSelected(0); 127 | }, 128 | ), 129 | GestureDetector( 130 | child: Container( 131 | child: Text( 132 | "推荐", 133 | style: TextStyle( 134 | color: Colors.white, 135 | fontSize: 18, 136 | fontWeight: FontWeight.bold), 137 | textAlign: TextAlign.center, 138 | ), 139 | width: TAB_WIDTH, 140 | ), 141 | onTap: () { 142 | widget.onSelected(1); 143 | }, 144 | ) 145 | ], 146 | ), 147 | SizedBox( 148 | height: 6, 149 | ), 150 | SizedBox( 151 | width: TAB_WIDTH * 2, 152 | height: 2, 153 | child: Align( 154 | alignment: Alignment(2 * widget.radio - 1, 0), 155 | child: Container( 156 | width: TAB_WIDTH, 157 | padding: EdgeInsets.only(left: 20, right: 20), 158 | child: Container( 159 | decoration: BoxDecoration(color: Colors.white), 160 | ), 161 | ), 162 | ), 163 | ) 164 | ], 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /lib/page/wechat/discovery_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_app/page/wechat/moments_page.dart'; 4 | 5 | class DiscoveryPage extends StatefulWidget { 6 | @override 7 | State createState() { 8 | return DiscoveryPageState(); 9 | } 10 | } 11 | 12 | class DiscoveryPageState extends State 13 | with AutomaticKeepAliveClientMixin { 14 | @override 15 | Widget build(BuildContext context) { 16 | super.build(context); 17 | return Container( 18 | color: Color(0xfff1f1f1), 19 | child: Column( 20 | children: [ 21 | GestureDetector( 22 | child: Container( 23 | child: Row( 24 | children: [ 25 | Icon(Icons.camera), 26 | SizedBox( 27 | width: 8, 28 | ), 29 | Text( 30 | "朋友圈", 31 | style: TextStyle(fontSize: 16), 32 | ), 33 | Spacer(), 34 | Icon( 35 | Icons.chevron_right, 36 | color: Color(0xffbdbdbd), 37 | ) 38 | ], 39 | ), 40 | padding: EdgeInsets.all(14), 41 | color: Colors.white, 42 | ), 43 | onTap: () { 44 | Navigator.of(context).push(MaterialPageRoute(builder: (context){ 45 | return MomentsPage(); 46 | })); 47 | }, 48 | ), 49 | ], 50 | ), 51 | ); 52 | } 53 | 54 | @override 55 | bool get wantKeepAlive => true; 56 | } 57 | -------------------------------------------------------------------------------- /lib/page/wechat/friend_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_app/rapid_positioning.dart'; 4 | 5 | import '../../data_source.dart'; 6 | import '../../entities.dart'; 7 | 8 | List items = [ 9 | "B", 10 | FRIENDS[11], 11 | "C", 12 | FRIENDS[1], 13 | FRIENDS[5], 14 | "L", 15 | FRIENDS[0], 16 | FRIENDS[2], 17 | FRIENDS[12], 18 | "J", 19 | FRIENDS[7], 20 | "X", 21 | FRIENDS[6], 22 | FRIENDS[10], 23 | "Y", 24 | FRIENDS[4], 25 | "Z", 26 | FRIENDS[3], 27 | FRIENDS[8], 28 | FRIENDS[9], 29 | FRIENDS.length 30 | ]; 31 | 32 | class _Item { 33 | final T data; 34 | 35 | final void Function(BuildContext buildContext, T data) build; 36 | 37 | _Item(this.data, this.build); 38 | } 39 | 40 | Widget _buildTotalFriendCount(BuildContext buildContext, int data) { 41 | return Container( 42 | alignment: Alignment.center, 43 | height: 40, 44 | padding: EdgeInsets.only(top: 10, bottom: 10), 45 | child: Text( 46 | "$data位联系人", 47 | style: TextStyle(fontSize: 16, color: Color(0xffbdbdbd)), 48 | ), 49 | ); 50 | } 51 | 52 | const _kLetterIndicatorHeight = 30; 53 | 54 | const _kFriendItemHeight = 56; 55 | 56 | Widget _buildLetterIndicator(BuildContext buildContext, String data) { 57 | return Container( 58 | height: 30, 59 | child: 60 | Text("$data", style: TextStyle(fontSize: 10, color: Color(0xff000000))), 61 | color: Color(0x88bdbdbd), 62 | padding: EdgeInsets.only(top: 10, bottom: 10, left: 14), 63 | ); 64 | } 65 | 66 | Widget _buildFriendItem(BuildContext buildContext, Friend data) { 67 | return Container( 68 | padding: EdgeInsets.only(left: 14, top: 4, bottom: 4), 69 | child: Row( 70 | children: [ 71 | Container( 72 | width: 40, 73 | height: 40, 74 | decoration: BoxDecoration( 75 | shape: BoxShape.rectangle, 76 | borderRadius: BorderRadius.all(Radius.circular(6)), 77 | image: DecorationImage( 78 | image: NetworkImage(data.avatar), fit: BoxFit.cover)), 79 | ), 80 | SizedBox( 81 | width: 10, 82 | ), 83 | Expanded( 84 | child: SizedBox( 85 | height: 48, 86 | child: Column( 87 | mainAxisAlignment: MainAxisAlignment.center, 88 | crossAxisAlignment: CrossAxisAlignment.start, 89 | children: [ 90 | Expanded( 91 | child: Align( 92 | child: Text( 93 | data.name, 94 | style: TextStyle(fontSize: 15), 95 | ), 96 | alignment: Alignment.centerLeft, 97 | ), 98 | ), 99 | SizedBox( 100 | width: double.infinity, 101 | height: 1, 102 | child: Container( 103 | color: Color(0x55bdbdbd), 104 | ), 105 | ) 106 | ], 107 | ), 108 | ), 109 | ) 110 | ], 111 | ), 112 | ); 113 | } 114 | 115 | class FriendListPage extends StatefulWidget { 116 | @override 117 | State createState() { 118 | return FriendListPageState(); 119 | } 120 | } 121 | 122 | class FriendListPageState extends State 123 | with AutomaticKeepAliveClientMixin { 124 | ScrollController _controller = ScrollController(); 125 | 126 | GlobalKey listViewKey = GlobalKey(); 127 | 128 | double _contactTotalHeight = 0.0; 129 | 130 | @override 131 | Widget build(BuildContext context) { 132 | super.build(context); 133 | for (Object item in items) { 134 | if (item is Friend) { 135 | _contactTotalHeight += _kFriendItemHeight; 136 | } else if (item is String) { 137 | _contactTotalHeight += _kLetterIndicatorHeight; 138 | } 139 | } 140 | 141 | return Container( 142 | color: Colors.white, 143 | child: Stack( 144 | children: [ 145 | ListView.builder( 146 | key: listViewKey, 147 | controller: _controller, 148 | itemBuilder: (context, index) { 149 | var item = items[index]; 150 | if (item is String) { 151 | return _buildLetterIndicator(context, item); 152 | } else if (item is Friend) { 153 | return _buildFriendItem(context, item); 154 | } 155 | return _buildTotalFriendCount(context, item); 156 | }, 157 | itemCount: items.length), 158 | Positioned( 159 | child: Container( 160 | child: RapidPositioning( 161 | textStyle: TextStyle(color: Colors.black, fontSize: 11), 162 | highlightColor: Color.fromARGB(255, 88, 191, 107), 163 | onChanged: (content, index) { 164 | double offset = 0.0; 165 | double listViewHeight = listViewKey.currentContext.size.height; 166 | for (Object item in items) { 167 | if (item is Friend) { 168 | offset += _kFriendItemHeight; 169 | } else if (item is String) { 170 | if (item == content) { 171 | if (_contactTotalHeight <= listViewHeight) { 172 | _controller.jumpTo(0); 173 | } else { 174 | if (_contactTotalHeight - offset < listViewHeight) { 175 | _controller 176 | .jumpTo((_contactTotalHeight - listViewHeight)); 177 | } else { 178 | _controller.jumpTo(offset); 179 | } 180 | } 181 | return; 182 | } else { 183 | offset += _kLetterIndicatorHeight; 184 | } 185 | } 186 | } 187 | }, 188 | ), 189 | margin: EdgeInsets.only(top: 16, bottom: 16), 190 | ), 191 | right: 0, 192 | top: 10, 193 | bottom: 10, 194 | ) 195 | ], 196 | ), 197 | ); 198 | } 199 | 200 | @override 201 | bool get wantKeepAlive => true; 202 | } 203 | -------------------------------------------------------------------------------- /lib/page/wechat/message_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/rendering.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | import 'package:flutter_app/bottom_sheet.dart' as AutoBottomSheet; 7 | import 'package:observable_ui/provider.dart'; 8 | 9 | import '../../chat_model.dart'; 10 | import '../../data_source.dart'; 11 | import '../../entities.dart'; 12 | import '../../home_model.dart'; 13 | import '../../widgets.dart'; 14 | import 'chat_detail_page.dart'; 15 | 16 | class MessagePage extends StatefulWidget { 17 | @override 18 | State createState() { 19 | return MessagePageState(); 20 | } 21 | } 22 | 23 | const _kMinSheetSize=0.15; 24 | 25 | class MessagePageState extends State 26 | with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { 27 | GlobalKey revealHeaderKey = GlobalKey(); 28 | 29 | GlobalKey dotsAnimation = GlobalKey(); 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | } 35 | 36 | @override 37 | void dispose() { 38 | super.dispose(); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | super.build(context); 44 | var model = ViewModelProvider.of(context); 45 | return Scaffold( 46 | body: LayoutBuilder( 47 | builder: (context, constraints) { 48 | return RepaintBoundary( 49 | child: Stack( 50 | children: [ 51 | NotificationListener( 52 | onNotification: (notification) { 53 | print(notification); 54 | if (notification 55 | is AutoBottomSheet.DraggableScrollableNotification) { 56 | RevealHeaderState revealHeaderState = 57 | (revealHeaderKey.currentState as RevealHeaderState); 58 | revealHeaderState.update(notification.extent); 59 | 60 | DotsAnimationState dotsAnimationState = 61 | (dotsAnimation.currentState as DotsAnimationState); 62 | dotsAnimationState.update(notification.extent); 63 | } 64 | return true; 65 | }, 66 | child: AutoBottomSheet.DraggableScrollableSheet( 67 | minChildSize: _kMinSheetSize, 68 | initialChildSize: 1, 69 | builder: (context, scrollControl) { 70 | return Column( 71 | children: [ 72 | _buildHeader(), 73 | Expanded( 74 | child: ListView.builder( 75 | controller: scrollControl, 76 | itemCount: model.chatItems.length, 77 | itemBuilder: (context, index) { 78 | return _buildChatItem( 79 | context, model.chatItems[index]); 80 | }), 81 | ) 82 | ], 83 | ); 84 | }, 85 | ), 86 | ), 87 | RevealHeader(revealHeaderKey, constraints.maxHeight), 88 | DotsAnimation(dotsAnimation, constraints.maxHeight), 89 | ], 90 | ), 91 | ); 92 | }, 93 | ), 94 | ); 95 | } 96 | 97 | Widget _buildHeader() { 98 | return Container( 99 | padding: EdgeInsets.fromLTRB(8, 0, 8, 0), 100 | decoration: BoxDecoration( 101 | color: Color.fromARGB(255, 237, 237, 23), 102 | borderRadius: BorderRadius.only( 103 | topLeft: Radius.circular(10), topRight: Radius.circular(10))), 104 | child: Row( 105 | children: [ 106 | Text( 107 | "微信(130)", 108 | style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), 109 | ), 110 | Spacer(), 111 | IconButton( 112 | icon: Icon(Icons.search), 113 | onPressed: () {}, 114 | ) 115 | ], 116 | ), 117 | ); 118 | } 119 | 120 | Widget _buildChatItem(BuildContext context, Entrance item) { 121 | return GestureDetector( 122 | child: Container( 123 | padding: EdgeInsets.fromLTRB(16, 8, 16, 8), 124 | color: Colors.white, 125 | child: Row( 126 | children: [ 127 | Subscript( 128 | //圆角头像 129 | content: Container( 130 | width: 46, 131 | height: 46, 132 | decoration: BoxDecoration( 133 | borderRadius: BorderRadius.all(Radius.circular(6)), 134 | image: DecorationImage( 135 | image: NetworkImage(item.icon), fit: BoxFit.cover)), 136 | ), 137 | subscript: Container( 138 | width: 16, 139 | height: 16, 140 | alignment: Alignment.center, 141 | child: Text( 142 | "${item.unreadCount}", 143 | textAlign: TextAlign.center, 144 | style: TextStyle(color: Colors.white, fontSize: 8), 145 | ), 146 | decoration: 147 | ShapeDecoration(shape: CircleBorder(), color: Colors.red), 148 | ), 149 | width: 54, 150 | height: 54, 151 | ), 152 | SizedBox( 153 | width: 8, 154 | ), 155 | Expanded( 156 | child: Container( 157 | height: 60, 158 | child: Column( 159 | mainAxisAlignment: MainAxisAlignment.spaceAround, 160 | children: [ 161 | Row( 162 | children: [ 163 | Expanded( 164 | child: Text( 165 | item.name, 166 | style: TextStyle(fontSize: 16, color: Colors.black), 167 | )), 168 | Text( 169 | item.recentMessage?.timestamp != null 170 | ? "${item.recentMessage.timestamp}" 171 | : "", 172 | style: TextStyle( 173 | fontSize: 12, 174 | color: Color.fromARGB(255, 185, 185, 185)), 175 | ) 176 | ], 177 | ), 178 | Row( 179 | children: [ 180 | Expanded( 181 | child: Text( 182 | item.recentMessage == null 183 | ? "暂无最近消息" 184 | : item.recentMessage.text, 185 | style: TextStyle( 186 | fontSize: 12, 187 | color: Color.fromARGB(255, 185, 185, 185)), 188 | ), 189 | ) 190 | ], 191 | ), 192 | Divider( 193 | height: 1, 194 | ) 195 | ], 196 | ), 197 | ), 198 | ) 199 | ], 200 | )), 201 | onTap: () { 202 | if (item.name == "订阅号消息") { 203 | Navigator.of(context).pushNamed("/subscription_box"); 204 | return; 205 | } 206 | 207 | Navigator.of(context).push(MaterialPageRoute(builder: (context) { 208 | return ViewModelProvider( 209 | viewModel: ChatModel(item.extra as Friend), 210 | child: ChatDetailPage(), 211 | ); 212 | })); 213 | }, 214 | ); 215 | } 216 | 217 | @override 218 | bool get wantKeepAlive => true; 219 | } 220 | 221 | 222 | class RevealHeader extends StatefulWidget { 223 | final double stackHeight; 224 | 225 | @override 226 | State createState() { 227 | return RevealHeaderState(); 228 | } 229 | const RevealHeader(Key key, this.stackHeight) : super(key: key); 230 | } 231 | 232 | 233 | class RevealHeaderState extends State { 234 | 235 | double offset = 1; 236 | 237 | bool expand = false; 238 | 239 | void update(double offset) { 240 | setState(() { 241 | if (offset == 1) { 242 | expand = false; 243 | } 244 | if (offset == _kMinSheetSize) { 245 | expand = true; 246 | } 247 | this.offset = offset; 248 | }); 249 | } 250 | 251 | @override 252 | Widget build(BuildContext context) { 253 | if (!expand) { 254 | return Positioned( 255 | top: 0, 256 | left: 0, 257 | right: 0, 258 | bottom: widget.stackHeight - (1 - offset) * widget.stackHeight, 259 | child: CustomPaint( 260 | child: Transform.scale( 261 | scale: 1 - offset + _kMinSheetSize, 262 | child: MinProgramHeader(), 263 | ), 264 | painter: BottomMaskLayer(offset), 265 | ), 266 | ); 267 | } 268 | 269 | return Positioned( 270 | top: -(1-_kMinSheetSize) * widget.stackHeight - 271 | widget.stackHeight * offset + 272 | widget.stackHeight, 273 | height: (1-_kMinSheetSize) * widget.stackHeight, 274 | left: 0, 275 | right: 0, 276 | child: CustomPaint( 277 | child: MinProgramHeader(), 278 | painter: BottomMaskLayer(offset), 279 | ), 280 | ); 281 | } 282 | } 283 | 284 | 285 | class DotsAnimation extends StatefulWidget { 286 | 287 | final double stackHeight; 288 | 289 | const DotsAnimation(Key key, this.stackHeight) : super(key: key); 290 | 291 | @override 292 | State createState() { 293 | return DotsAnimationState(); 294 | } 295 | } 296 | 297 | class DotsAnimationState extends State { 298 | 299 | double offset = 1; 300 | 301 | void update(double offset) { 302 | setState(() { 303 | this.offset = offset; 304 | }); 305 | } 306 | 307 | @override 308 | Widget build(BuildContext context) { 309 | return Positioned( 310 | top: 0, 311 | left: 0, 312 | right: 0, 313 | bottom: widget.stackHeight - (1 - offset) * widget.stackHeight, 314 | child: CustomPaint( 315 | foregroundPainter: TopMaskLayer(offset), 316 | ), 317 | ); 318 | } 319 | } 320 | 321 | class TopMaskLayer extends CustomPainter { 322 | final double shrinkOffset; 323 | 324 | Paint p = Paint(); 325 | 326 | TopMaskLayer(this.shrinkOffset); 327 | 328 | @override 329 | void paint(Canvas canvas, Size size) { 330 | //此时三个小点显示,左右两个小点向内聚合 331 | double lowLine = 0.6; 332 | 333 | //此时三个点聚合为一个点,该点逐渐变小 334 | double highLine = 0.8; 335 | 336 | double maxGap = 20; 337 | 338 | double maxSize = 5; 339 | 340 | int alpha=255; 341 | 342 | double bgLowLine=0.8; 343 | 344 | if(shrinkOffset>=bgLowLine){ 345 | alpha=255; 346 | }else{ 347 | alpha = (255 * (1/(bgLowLine-_kMinSheetSize)*shrinkOffset-1/(bgLowLine-_kMinSheetSize)*_kMinSheetSize)).toInt().clamp(0, 255); 348 | } 349 | canvas.save(); 350 | canvas.clipRect(Rect.fromLTRB(0, 0, size.width, size.height)); 351 | canvas.drawColor(Color.fromARGB(alpha, 255, 255, 255), BlendMode.srcATop); 352 | canvas.restore(); 353 | 354 | int dotAlpha=255; 355 | if(shrinkOffset lowLine && shrinkOffset < highLine) { 362 | double gap = maxGap / (lowLine - highLine) * shrinkOffset + 363 | (maxGap * highLine) / (highLine - lowLine); 364 | 365 | canvas.drawCircle(Offset(size.width / 2 - gap, size.height / 2), 4, p); 366 | canvas.drawCircle(Offset(size.width / 2, size.height / 2), maxSize, p); 367 | canvas.drawCircle(Offset(size.width / 2 + gap, size.height / 2), 4, p); 368 | 369 | } else if (shrinkOffset >= highLine) { 370 | canvas.drawCircle(Offset(size.width / 2, size.height / 2), 371 | maxSize * (-5 * shrinkOffset + 5), p); 372 | } 373 | } 374 | 375 | @override 376 | bool shouldRepaint(CustomPainter oldDelegate) { 377 | return true; 378 | } 379 | } 380 | 381 | class BottomMaskLayer extends CustomPainter { 382 | 383 | final double offset; 384 | 385 | Paint p = Paint(); 386 | 387 | BottomMaskLayer(this.offset); 388 | 389 | static const int MAX_ALPHA = 255; 390 | 391 | @override 392 | void paint(Canvas canvas, Size size) { 393 | canvas.save(); 394 | canvas.drawColor( 395 | Color.fromARGB(((1 - this.offset) * MAX_ALPHA).toInt(), 66, 64, 88), 396 | BlendMode.srcATop); 397 | canvas.restore(); 398 | } 399 | 400 | @override 401 | bool shouldRepaint(BottomMaskLayer oldDelegate) { 402 | return oldDelegate.offset != offset; 403 | } 404 | } 405 | 406 | 407 | class OverScrollEndNotification extends Notification {} 408 | 409 | const _kOverScrollCriticalRadio = 3 / 2; 410 | 411 | class MinProgramHeader extends StatefulWidget { 412 | @override 413 | State createState() { 414 | return MinProgramHeaderState(); 415 | } 416 | } 417 | 418 | class MinProgramHeaderState extends State { 419 | ScrollController scrollController = ScrollController(); 420 | 421 | @override 422 | Widget build(BuildContext context) { 423 | return Column( 424 | children: [ 425 | Container( 426 | child: Text( 427 | "小程序", 428 | style: TextStyle(color: Colors.white, fontSize: 16), 429 | ), 430 | alignment: Alignment.centerLeft, 431 | padding: EdgeInsets.only(left: 10, top: 10, bottom: 10), 432 | ), 433 | Expanded( 434 | child: Listener( 435 | onPointerUp: (e) { 436 | if (scrollController.position.pixels > 437 | (scrollController.position.maxScrollExtent * 438 | _kOverScrollCriticalRadio)) { 439 | OverScrollEndNotification().dispatch(context); 440 | } 441 | }, 442 | child: CustomScrollView( 443 | controller: scrollController, 444 | physics: BouncingScrollPhysics(), 445 | slivers: [ 446 | SliverToBoxAdapter( 447 | child: Container( 448 | child: Column( 449 | children: [ 450 | Container( 451 | decoration: BoxDecoration( 452 | borderRadius: 453 | BorderRadius.all(Radius.circular(2)), 454 | color: Color(0x66ffffff)), 455 | child: Row( 456 | children: [ 457 | Icon( 458 | Icons.search, 459 | color: Color(0x33000000), 460 | ), 461 | Text( 462 | "搜索小程序", 463 | style: TextStyle( 464 | fontSize: 18, 465 | color: Color(0xaaffffff), 466 | ), 467 | ) 468 | ], 469 | ), 470 | margin: EdgeInsets.only(top: 30, bottom: 30), 471 | padding: EdgeInsets.only( 472 | left: 6, right: 6, top: 8, bottom: 8), 473 | ), 474 | _buildGridWithLabel("最近使用", minPrograms: [...MIN_PROGRAMS,...MIN_PROGRAMS,...MIN_PROGRAMS]), 475 | SizedBox( 476 | height: 10, 477 | ), 478 | _buildGridWithLabel("我的小程序", minPrograms: [...MIN_PROGRAMS,...MIN_PROGRAMS,...MIN_PROGRAMS]) 479 | ], 480 | ), 481 | margin: EdgeInsets.only(left: 16, right: 16), 482 | ), 483 | ) 484 | ], 485 | ), 486 | ), 487 | ) 488 | ], 489 | ); 490 | } 491 | 492 | Widget _buildGridWithLabel(String label, 493 | {List minPrograms, countForRow = 4}) { 494 | int rowCount = (minPrograms.length / countForRow).round(); 495 | var rows = [ 496 | Align( 497 | child: Text( 498 | label, 499 | style: TextStyle(color: Color(0xffffbdbdbd), fontSize: 10), 500 | ), 501 | alignment: Alignment.centerLeft, 502 | ) 503 | ]; 504 | 505 | for (int i = 0; i < rowCount; i++) { 506 | var widgets = []; 507 | for (int j = countForRow * i; 508 | j < min(minPrograms.length, countForRow * (i + 1)); 509 | j++) { 510 | var item = minPrograms[j]; 511 | 512 | widgets.add( 513 | Expanded( 514 | child: GestureDetector( 515 | child: Align( 516 | alignment: Alignment.center, 517 | child: Column( 518 | children: [ 519 | Container( 520 | width: 46, 521 | height: 46, 522 | child: CircleAvatar( 523 | backgroundImage: NetworkImage(item.icon), 524 | )), 525 | SizedBox( 526 | height: 4, 527 | ), 528 | Text( 529 | item.name, 530 | style: TextStyle(color: Colors.white, fontSize: 12), 531 | ) 532 | ], 533 | ), 534 | ), 535 | onTap: () { 536 | item.onEnter(context, item); 537 | }, 538 | ), 539 | ), 540 | ); 541 | } 542 | 543 | if (i == rowCount - 1) { 544 | for (int k = minPrograms.length; k < (rowCount) * countForRow; k++) { 545 | widgets.add(Spacer()); 546 | } 547 | } 548 | 549 | rows.add(Container( 550 | child: Row( 551 | mainAxisAlignment: MainAxisAlignment.center, 552 | children: widgets, 553 | ), 554 | padding: EdgeInsets.only(top: 4, bottom: 4), 555 | )); 556 | } 557 | 558 | return Column( 559 | children: rows, 560 | ); 561 | } 562 | } 563 | -------------------------------------------------------------------------------- /lib/page/wechat/subscription_message_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:observable_ui/core.dart'; 4 | import 'package:observable_ui/widgets.dart'; 5 | 6 | class SubscriptionBoxPage extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | return Scaffold( 10 | appBar: AppBar( 11 | title: Text("订阅号消息"), 12 | ), 13 | body: Center( 14 | child: ListView.builder( 15 | itemCount: 4, 16 | itemBuilder: (context, index) { 17 | if (index == 0) { 18 | return _buildOftenRead(); 19 | } 20 | return _buildSubscriptionCard(); 21 | })), 22 | ); 23 | } 24 | 25 | Widget _buildOftenRead() { 26 | return Column( 27 | crossAxisAlignment: CrossAxisAlignment.start, 28 | mainAxisAlignment: MainAxisAlignment.center, 29 | children: [ 30 | Container( 31 | child: Text("常读的订阅号"), 32 | margin: EdgeInsets.only(left: 12, top: 14, bottom: 0), 33 | ), 34 | Container( 35 | alignment: Alignment.center, 36 | child: ListViewEx.builder( 37 | items: ObservableList(initValue: ["", "", "", "", "", "", "", ""]), 38 | itemBuilder: (context, item) { 39 | return Container( 40 | child: Column( 41 | mainAxisAlignment: MainAxisAlignment.center, 42 | children: [ 43 | CircleAvatar(), 44 | Row( 45 | children: [Text("环球时报")], 46 | ) 47 | ], 48 | ), 49 | padding: EdgeInsets.fromLTRB(10, 0, 0, 0), 50 | ); 51 | }, 52 | scrollDirection: Axis.horizontal, 53 | ), 54 | height: 80, 55 | ), 56 | ], 57 | ); 58 | } 59 | 60 | Widget _buildSubscriptionCard() { 61 | return Card( 62 | margin: EdgeInsets.only(left: 8, top: 0, bottom: 16, right: 8), 63 | child: Container( 64 | child: Column( 65 | children: [ 66 | //卡片头 67 | Container( 68 | child: Row( 69 | children: [ 70 | CircleAvatar( 71 | backgroundImage: NetworkImage( 72 | "http://b-ssl.duitang.com/uploads/item/201811/04/20181104074412_wcelx.jpg"), 73 | ), 74 | Text("央视新闻"), 75 | Spacer(), 76 | Text("一分钟前") 77 | ], 78 | ), 79 | padding: EdgeInsets.fromLTRB(8, 8, 8, 8), 80 | ), 81 | Stack( 82 | children: [ 83 | Image.network( 84 | "https://pics2.baidu.com/feed/8601a18b87d6277fbc988af78a6ada35eb24fccb.jpeg?token=9a42458de603221ff28fc45ea0ac197a&s=65925B9E4C71469CC6B171D003005035", 85 | fit: BoxFit.cover, 86 | height: 200, 87 | width: double.infinity, 88 | ), 89 | Positioned( 90 | child: Column( 91 | crossAxisAlignment: CrossAxisAlignment.start, 92 | children: [ 93 | Text( 94 | "两位朋友读过", 95 | style: TextStyle(color: Colors.white), 96 | ), 97 | Text("谋杀李伯现场的两人被抓", style: TextStyle(color: Colors.white)) 98 | ], 99 | ), 100 | left: 10, 101 | bottom: 10, 102 | ) 103 | ], 104 | ), 105 | Container( 106 | child: Row( 107 | children: [ 108 | Expanded( 109 | child: Text( 110 | "【关注】全身五成烧伤,至今昏迷不醒..李伯家人:要撑住", 111 | style: TextStyle(fontSize: 16), 112 | textAlign: TextAlign.left, 113 | ), 114 | ), 115 | Image.network( 116 | "http://b-ssl.duitang.com/uploads/item/201811/04/20181104074412_wcelx.jpg", 117 | width: 60, 118 | height: 60, 119 | ) 120 | ], 121 | ), 122 | padding: EdgeInsets.fromLTRB(12, 16, 12, 16), 123 | ), 124 | //卡片尾 125 | Row( 126 | children: [ 127 | Text("余下两篇"), 128 | Spacer(), 129 | Icon(Icons.arrow_drop_down) 130 | ], 131 | ) 132 | ], 133 | ), 134 | ), 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /lib/photo_preview.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | 6 | class PhotoPreviewPage extends StatelessWidget { 7 | const PhotoPreviewPage(this.photo); 8 | 9 | final String photo; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | 14 | return Container( 15 | color: Color(0x99000000), 16 | padding: const EdgeInsets.all(16.0), 17 | alignment: Alignment.center, 18 | child: PhotoHero( 19 | photo: photo, 20 | width: 300.0, 21 | onTap: () { 22 | Navigator.of(context).pop(); 23 | }, 24 | ), 25 | ); 26 | } 27 | } 28 | 29 | class PhotoHero extends StatelessWidget { 30 | const PhotoHero({Key key, this.photo, this.onTap, this.width}) 31 | : super(key: key); 32 | 33 | final String photo; 34 | final VoidCallback onTap; 35 | final double width; 36 | 37 | Widget build(BuildContext context) { 38 | return SizedBox( 39 | width: width, 40 | child: Hero( 41 | tag: photo, 42 | child: Material( 43 | color: Colors.transparent, 44 | child: InkWell( 45 | onTap: onTap, 46 | child: Image.file( 47 | File(photo), 48 | fit: BoxFit.cover, 49 | ), 50 | ), 51 | ), 52 | ), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/rapid_positioning.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/gestures.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/rendering.dart'; 6 | import 'package:flutter/widgets.dart'; 7 | 8 | const DEFAULT_DATA = [ 9 | '↑', 10 | '★', 11 | 'A', 12 | 'B', 13 | 'C', 14 | 'D', 15 | 'E', 16 | 'F', 17 | 'G', 18 | 'H', 19 | 'I', 20 | 'J', 21 | 'K', 22 | 'L', 23 | 'M', 24 | 'N', 25 | 'O', 26 | 'P', 27 | 'Q', 28 | 'R', 29 | 'S', 30 | 'T', 31 | 'U', 32 | 'V', 33 | 'W', 34 | 'X', 35 | 'Y', 36 | 'Z', 37 | '#' 38 | ]; 39 | 40 | class SelectedChangeNotification extends Notification { 41 | final String content; 42 | 43 | final int index; 44 | 45 | final bool reset; 46 | 47 | SelectedChangeNotification(this.reset, {this.content, this.index}); 48 | } 49 | 50 | typedef HandleEventCallback = void Function(PointerEvent pointerEvent); 51 | 52 | /// 类似于微信,纵向的可滑动的字母定位器,放置于可横向滑动的PageView中。 53 | /// 如果不额外处理,在Flutter中会出现一个奇怪现象。字母定位器和PageView都会接受到触摸事件。 54 | /// 为了解决这个问题,我们需要在与PageView的手势竞争中获胜。 55 | class _SimpleGestureRecognizer extends OneSequenceGestureRecognizer { 56 | _SimpleGestureRecognizer({PointerDeviceKind kind}) : super(kind: kind); 57 | 58 | HandleEventCallback handleEventCallback; 59 | 60 | PointerEvent _initPointerEvent; 61 | 62 | bool _accepted = false; 63 | 64 | @override 65 | void handleEvent(PointerEvent event) { 66 | if (event.pointer != _initPointerEvent.pointer) { 67 | return; 68 | } 69 | 70 | if (handleEventCallback != null) { 71 | handleEventCallback(event); 72 | } 73 | 74 | if (event is PointerMoveEvent && !_accepted) { 75 | var arc = atan((event.position.dy - _initPointerEvent.position.dy) / 76 | (event.position.dx - _initPointerEvent.position.dx)); 77 | //down事件之后的第一次move事件 夹角小于45度 ,我们就放弃跟踪手势,因为我们认为是横向移动事件,让其他widget接受事件,比如PageView 78 | if (arc.abs() < (pi / 4)) { 79 | resolve(GestureDisposition.rejected); 80 | stopTrackingPointer(event.pointer); 81 | _initPointerEvent = null; 82 | _accepted = false; 83 | if (handleEventCallback != null) { 84 | handleEventCallback(PointerCancelEvent()); 85 | } 86 | } else { 87 | _accepted = true; 88 | resolve(GestureDisposition.accepted); 89 | } 90 | } else if (event is PointerUpEvent) { 91 | stopTrackingPointer(event.pointer); 92 | _initPointerEvent = null; 93 | _accepted = false; 94 | } 95 | } 96 | 97 | @override 98 | void addAllowedPointer(PointerDownEvent event) { 99 | _accepted = false; 100 | if (_initPointerEvent != null) { 101 | return; 102 | } 103 | _initPointerEvent = event; 104 | startTrackingPointer(event.pointer, event.transform); 105 | } 106 | 107 | @override 108 | String get debugDescription => 'simple gesture'; 109 | 110 | @override 111 | void didStopTrackingLastPointer(int pointer) {} 112 | } 113 | 114 | class RapidPositioning extends StatefulWidget { 115 | final Color highlightColor; 116 | 117 | final List data; 118 | 119 | final void Function(String content, int index) onChanged; 120 | 121 | final Color backgroundColor; 122 | 123 | final Color markerBackgroundColor; 124 | 125 | final double markerSize; 126 | 127 | final TextStyle markerTextStyle; 128 | 129 | final TextStyle textStyle; 130 | 131 | final double width; 132 | 133 | RapidPositioning( 134 | {Key key, 135 | this.highlightColor = const Color(0xff3367ff), 136 | this.data = DEFAULT_DATA, 137 | this.markerSize = 30.0, 138 | this.onChanged, 139 | this.textStyle = const TextStyle(color: Color(0xff000000)), 140 | this.markerTextStyle = 141 | const TextStyle(color: Color(0xff000000), fontSize: 20), 142 | this.markerBackgroundColor = const Color(0x66343434), 143 | this.backgroundColor = const Color(0x00343434), 144 | this.width = 20.0}) 145 | : super(key: key); 146 | 147 | @override 148 | State createState() { 149 | return RapidPositioningState(); 150 | } 151 | } 152 | 153 | class RapidPositioningState extends State { 154 | Map _gestureRecognizers = 155 | const {}; 156 | 157 | GlobalKey _rapidPositioningRenderKey = GlobalKey(); 158 | String _lastReportedContent; 159 | int _lastReportedIndex; 160 | 161 | @override 162 | void initState() { 163 | super.initState(); 164 | _gestureRecognizers = { 165 | _SimpleGestureRecognizer: 166 | GestureRecognizerFactoryWithHandlers<_SimpleGestureRecognizer>( 167 | () => _SimpleGestureRecognizer(), 168 | (_SimpleGestureRecognizer instance) { 169 | instance 170 | ..handleEventCallback = (e) { 171 | (_rapidPositioningRenderKey.currentContext.findRenderObject() 172 | as RapidPositioningRenderObject) 173 | .onHandleEvent(e); 174 | }; 175 | }), 176 | }; 177 | } 178 | 179 | @override 180 | Widget build(BuildContext context) { 181 | return NotificationListener( 182 | onNotification: (notification) { 183 | if (notification.reset) { 184 | _lastReportedIndex = null; 185 | _lastReportedContent = null; 186 | return true; 187 | } 188 | 189 | if (notification.index != _lastReportedIndex && 190 | notification.content != _lastReportedContent) { 191 | _lastReportedIndex = notification.index; 192 | _lastReportedContent = notification.content; 193 | this.widget.onChanged(_lastReportedContent, _lastReportedIndex); 194 | } 195 | return true; 196 | }, 197 | child: RawGestureDetector( 198 | gestures: _gestureRecognizers, 199 | behavior: HitTestBehavior.opaque, 200 | child: RapidPositioningRenderWidget( 201 | key: _rapidPositioningRenderKey, 202 | highlightColor: this.widget.highlightColor, 203 | backgroundColor: this.widget.backgroundColor, 204 | markerBackgroundColor: this.widget.markerBackgroundColor, 205 | markerSize: this.widget.markerSize, 206 | markerTextStyle: this.widget.markerTextStyle, 207 | data: this.widget.data, 208 | width: this.widget.width, 209 | textStyle: this.widget.textStyle, 210 | ), 211 | ), 212 | ); 213 | } 214 | } 215 | 216 | class RapidPositioningRenderWidget extends LeafRenderObjectWidget { 217 | final Color highlightColor; 218 | 219 | final List data; 220 | 221 | final void Function(String text) onChanged; 222 | 223 | final Color backgroundColor; 224 | 225 | final Color markerBackgroundColor; 226 | 227 | final double markerSize; 228 | 229 | final TextStyle markerTextStyle; 230 | 231 | final TextStyle textStyle; 232 | 233 | final double width; 234 | 235 | RapidPositioningRenderWidget( 236 | {Key key, 237 | this.highlightColor = const Color(0xff3367ff), 238 | this.data = DEFAULT_DATA, 239 | this.markerSize = 30.0, 240 | this.onChanged, 241 | this.textStyle = const TextStyle(color: Color(0xff000000)), 242 | this.markerTextStyle = 243 | const TextStyle(color: Color(0xff000000), fontSize: 20), 244 | this.markerBackgroundColor = const Color(0x66343434), 245 | this.backgroundColor = const Color(0x00343434), 246 | this.width = 20.0}) 247 | : super(key: key); 248 | 249 | @override 250 | LeafRenderObjectElement createElement() { 251 | return LeafRenderObjectElement(this); 252 | } 253 | 254 | @override 255 | RenderObject createRenderObject(BuildContext context) { 256 | return RapidPositioningRenderObject() 257 | .._markerSize = this.markerSize 258 | .._data = this.data 259 | .._highlightColor = this.highlightColor 260 | .._markerBackgroundColor = this.markerBackgroundColor 261 | .._textStyle = this.textStyle 262 | .._markerTextStyle = this.markerTextStyle 263 | .._backgroundColor = this.backgroundColor 264 | .._width = this.width 265 | .._buildContext = context; 266 | } 267 | } 268 | 269 | class RapidPositioningRenderObject extends RenderBox { 270 | RapidPositioningRenderObject( 271 | {List data, 272 | Color markerBackgroundColor, 273 | Color highlightColor, 274 | Color markerTextColor, 275 | Color backgroundColor, 276 | double markerSize, 277 | TextStyle markerTextStyle, 278 | double width, 279 | TextStyle textStyle, 280 | BuildContext context}) 281 | : _markerBackgroundColor = markerBackgroundColor, 282 | _highlightColor = highlightColor, 283 | _data = data, 284 | _markerSize = markerSize, 285 | _markerTextStyle = markerTextStyle, 286 | _backgroundColor = backgroundColor, 287 | _textStyle = textStyle, 288 | _width = width, 289 | _buildContext = context; 290 | 291 | PointerEvent _currentEvent; 292 | 293 | BuildContext _buildContext; 294 | 295 | Paint _circlePaint = Paint(); 296 | 297 | Paint _markerPaint = Paint(); 298 | 299 | Paint _backgroundPaint = Paint(); 300 | 301 | Color _markerBackgroundColor; 302 | 303 | Color _backgroundColor; 304 | 305 | Color _highlightColor; 306 | 307 | double _markerSize; 308 | 309 | TextStyle _markerTextStyle; 310 | 311 | TextStyle _textStyle; 312 | 313 | double _width; 314 | 315 | List get data => _data; 316 | 317 | List _data; 318 | 319 | set data(List newData) { 320 | assert(newData != null); 321 | if (_data != newData) { 322 | _data = newData; 323 | markNeedsPaint(); 324 | } 325 | } 326 | 327 | @override 328 | void performLayout() { 329 | size = Size(_width, constraints.maxHeight); 330 | } 331 | 332 | @override 333 | void paint(PaintingContext context, Offset offset) { 334 | super.paint(context, offset); 335 | 336 | var charHeight = size.height / data.length; 337 | var charWidth = size.width; 338 | var circleSize = min(charWidth, charHeight); 339 | 340 | var highlightPos = -1; 341 | if (_currentEvent != null && !(_currentEvent is PointerCancelEvent)) { 342 | if (_currentEvent is PointerUpEvent) { 343 | highlightPos = -1; 344 | _currentEvent = null; 345 | SelectedChangeNotification(true).dispatch(_buildContext); 346 | } else { 347 | highlightPos = (_currentEvent.localPosition.dy ~/ charHeight) 348 | .clamp(0, data.length - 1); 349 | } 350 | } 351 | 352 | context.canvas.save(); 353 | context.canvas.translate(offset.dx, offset.dy); 354 | 355 | _backgroundPaint.color = _backgroundColor; 356 | context.canvas.drawRect( 357 | Rect.fromLTRB(0, 0, size.width, size.height), _backgroundPaint); 358 | 359 | if (highlightPos >= 0) { 360 | _circlePaint.color = _highlightColor; 361 | context.canvas.drawCircle( 362 | Offset(charWidth / 2, (highlightPos + 0.5) * charHeight), 363 | circleSize / 2, 364 | _circlePaint); 365 | } 366 | 367 | for (num i = 0; i < _data.length; i++) { 368 | var textPainter = TextPainter( 369 | text: TextSpan(text: DEFAULT_DATA[i], style: _textStyle), 370 | textDirection: TextDirection.ltr) 371 | ..layout(); 372 | textPainter.paint(context.canvas, 373 | Offset((charWidth - textPainter.width) / 2, charHeight * i)); 374 | } 375 | context.canvas.restore(); 376 | 377 | // draw marker 378 | if (highlightPos >= 0) { 379 | context.canvas.save(); 380 | var path = Path(); 381 | var radius = _markerSize; 382 | context.canvas.translate(offset.dx - radius * 2.5, 383 | offset.dy + (highlightPos + 0.5) * charHeight - radius); 384 | path.addArc( 385 | Rect.fromLTRB(0, 0, radius * 2, radius * 2), pi / 4, pi * 3 / 2); 386 | path.lineTo(radius + sqrt(2) * radius, radius); 387 | path.close(); 388 | 389 | _markerPaint.color = _markerBackgroundColor; 390 | context.canvas.drawPath(path, _markerPaint); 391 | var textPainter = TextPainter( 392 | text: TextSpan( 393 | text: DEFAULT_DATA[highlightPos], style: _markerTextStyle), 394 | textDirection: TextDirection.ltr) 395 | ..layout(); 396 | textPainter.paint( 397 | context.canvas, 398 | Offset( 399 | radius - textPainter.width / 2, radius - textPainter.height / 2)); 400 | context.canvas.restore(); 401 | 402 | SelectedChangeNotification(false, 403 | content: data[highlightPos], index: highlightPos) 404 | .dispatch(_buildContext); 405 | } 406 | } 407 | 408 | void onHandleEvent(PointerEvent event) { 409 | if (event is PointerDownEvent || 410 | event is PointerMoveEvent || 411 | event is PointerUpEvent || 412 | event is PointerCancelEvent) { 413 | _currentEvent = event; 414 | markNeedsPaint(); 415 | } 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /lib/server/ChatAI.dart: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import 'dart:async'; 5 | import 'dart:isolate'; 6 | 7 | class ChatAI { 8 | 9 | 10 | static listenMessage(void onData(var message)) async{ 11 | ReceivePort receivePort = ReceivePort(); 12 | await Isolate.spawn(main2, receivePort.sendPort); 13 | SendPort sendPort = await receivePort.first; 14 | ReceivePort response = ReceivePort(); 15 | sendPort.send(["my message", response.sendPort]); 16 | response.listen(onData); 17 | } 18 | 19 | static Future main2(SendPort sendPort) async{ 20 | ReceivePort port = ReceivePort(); 21 | sendPort.send(port.sendPort); 22 | await for(var msg in port){ 23 | SendPort replyTo = msg[1]; 24 | delayedMessage(replyTo); 25 | } 26 | } 27 | 28 | 29 | static void delayedMessage(SendPort replyTo){ 30 | Future.delayed(Duration(seconds: 10),(){ 31 | replyTo.send("my reply"); 32 | delayedMessage(replyTo); 33 | }); 34 | } 35 | 36 | 37 | static Future main(SendPort sendPort) async{ 38 | ReceivePort port = ReceivePort(); 39 | sendPort.send(port.sendPort); 40 | await for(var msg in port){ 41 | SendPort replyTo = msg[1]; 42 | replyTo.send("my reply"); 43 | 44 | } 45 | } 46 | 47 | 48 | static Future sendMessage() async{ 49 | ReceivePort receivePort = ReceivePort(); 50 | await Isolate.spawn(main, receivePort.sendPort); 51 | SendPort sendPort = await receivePort.first; 52 | ReceivePort response = ReceivePort(); 53 | sendPort.send(["my message", response.sendPort]); 54 | return response.first; 55 | } 56 | 57 | 58 | } -------------------------------------------------------------------------------- /lib/subscription_box_model.dart: -------------------------------------------------------------------------------- 1 | class SubscriptionBoxModel {} 2 | -------------------------------------------------------------------------------- /lib/utils.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const AVATAR = const [ 4 | "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1236308033,3321919462&fm=26&gp=0.jpg", 5 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1575338510&di=2c4ccaf42a260b8463d8744ff1184da1&imgtype=jpg&er=1&src=http%3A%2F%2Fy2.ifengimg.com%2Fa13eecb1dba8cce3%2F2014%2F0925%2Frdn_542371e0404c5.png", 6 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1575338736&di=59553e505a6fd221c24ae06c4629506e&imgtype=jpg&er=1&src=http%3A%2F%2Fimg.ifeng.com%2Fres%2F200811%2F1126_500745.jpg", 7 | "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1576472833976&di=bfc3b95448eb89321f4e25ea0fbf2054&imgtype=0&src=http%3A%2F%2Fhbimg.huabanimg.com%2Ff2b2ad85a548a22049f10f90cf32dd8cd9f79b0c90c0-gbkg5j_fw658", 8 | "http://img0.imgtn.bdimg.com/it/u=1944742345,2511093610&fm=214&gp=0.jpg" 9 | ]; 10 | 11 | const colors = [Colors.orange, Colors.red, Colors.blue, Colors.white]; 12 | 13 | bool isNullOrEmpty(List list) { 14 | return list == null || list.isEmpty; 15 | } 16 | 17 | //11:22:08 18 | String formatHHmmSS(double time) { 19 | if (time < 0) { 20 | return "--:--:--"; 21 | } 22 | 23 | int hour = time ~/ 3600; 24 | String s; 25 | if (hour < 10) { 26 | s = "0$hour:"; 27 | } else { 28 | s = "$hour:"; 29 | } 30 | int minute = (time - (hour * 3600)) ~/ 60; 31 | if (minute < 10) { 32 | s += "0$minute:"; 33 | } else { 34 | s += "$minute:"; 35 | } 36 | var second = ((time - hour * 3600 - minute * 60) % 60).toInt(); 37 | if (second < 10) { 38 | s += "0$second"; 39 | } else { 40 | s += "$second"; 41 | } 42 | return s; 43 | } 44 | -------------------------------------------------------------------------------- /lib/widgets.dart: -------------------------------------------------------------------------------- 1 | //实现角标 2 | import 'dart:math'; 3 | 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/gestures.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter/rendering.dart'; 8 | import 'package:flutter/widgets.dart'; 9 | import 'package:palette_generator/palette_generator.dart'; 10 | 11 | class Subscript extends StatelessWidget { 12 | final double width; 13 | 14 | final double height; 15 | 16 | final Widget content; 17 | 18 | final Widget subscript; 19 | 20 | const Subscript( 21 | {Key key, this.width, this.height, this.content, this.subscript}) 22 | : super(key: key); 23 | 24 | @override 25 | Widget build(BuildContext context) { 26 | return Stack( 27 | children: [ 28 | Container( 29 | child: this.content, 30 | width: this.width, 31 | height: this.height, 32 | alignment: Alignment.center), 33 | Container( 34 | child: this.subscript, 35 | width: this.width, 36 | height: this.height, 37 | alignment: Alignment.topRight) 38 | ], 39 | ); 40 | } 41 | } 42 | 43 | ///跑马灯效果 44 | class MarqueeText extends LeafRenderObjectWidget { 45 | final double width; 46 | 47 | final double height; 48 | 49 | final String text; 50 | 51 | final TextStyle style; 52 | 53 | const MarqueeText({ 54 | Key key, 55 | this.text, 56 | this.width, 57 | this.height, 58 | this.style, 59 | }) : super(key: key); 60 | 61 | @override 62 | RenderObject createRenderObject(BuildContext context) { 63 | return MarqueeTextRenderObject() 64 | ..style = style 65 | ..width = width 66 | ..height = height 67 | ..text = text; 68 | } 69 | } 70 | 71 | class MarqueeTextRenderObject extends RenderBox { 72 | double width; 73 | 74 | double height; 75 | 76 | String text; 77 | 78 | TextStyle style; 79 | 80 | double _dx = 0.0; 81 | 82 | MarqueeTextRenderObject({this.width, this.height, this.text, this.style}); 83 | 84 | @override 85 | void performLayout() { 86 | size = Size(width, height); 87 | } 88 | 89 | @override 90 | void paint(PaintingContext context, Offset offset) { 91 | final String newText = text + " "; 92 | var textPainter = TextPainter( 93 | text: TextSpan(text: newText, style: style), 94 | textDirection: TextDirection.ltr) 95 | ..layout(); 96 | 97 | var count = size.width ~/ textPainter.width + 2; 98 | String lastText = ""; 99 | 100 | for (int i = 1; i <= count; i++) { 101 | lastText += newText; 102 | } 103 | var lastPainter = TextPainter( 104 | text: TextSpan(text: lastText, style: style), 105 | textDirection: TextDirection.ltr) 106 | ..layout(); 107 | 108 | context.canvas.save(); 109 | 110 | context.canvas.clipRect(Rect.fromLTRB( 111 | offset.dx, offset.dy, offset.dx + size.width, offset.dy + size.height)); 112 | var newOffset = Offset(offset.dx + _dx, offset.dy); 113 | lastPainter.paint(context.canvas, newOffset); 114 | context.canvas.restore(); 115 | 116 | if (_dx <= -(lastPainter.width - size.width)) { 117 | _dx = -((count - 1) * textPainter.width - size.width); 118 | } else { 119 | _dx -= 1.0; 120 | } 121 | 122 | Future.delayed(Duration(milliseconds: 16), () { 123 | markNeedsPaint(); 124 | }); 125 | } 126 | } 127 | 128 | ////仅仅识别上下方向滚动 129 | enum VerticalEvent { UP, DOWN } 130 | 131 | class VerticalGestureDetector extends StatefulWidget { 132 | final Widget child; 133 | 134 | final void Function(VerticalEvent event) onEvent; 135 | 136 | VerticalGestureDetector({Key key, this.child, this.onEvent}) 137 | : super(key: key); 138 | 139 | @override 140 | State createState() { 141 | return VerticalGestureDetectorState(); 142 | } 143 | } 144 | 145 | class VerticalGestureDetectorState extends State { 146 | PointerEvent startPointEvent; 147 | 148 | @override 149 | Widget build(BuildContext context) { 150 | return Listener( 151 | onPointerDown: _handlePointerDown, 152 | onPointerMove: _handlePointerMove, 153 | onPointerUp: _handlePointerUp, 154 | child: widget.child, 155 | ); 156 | } 157 | 158 | void _handlePointerMove(PointerMoveEvent event) { 159 | if (startPointEvent == null) { 160 | startPointEvent = event; 161 | return; 162 | } 163 | if ((event.position.dy - startPointEvent.position.dy).abs() < 20) { 164 | return; 165 | } 166 | 167 | var arc = atan((event.position.dy - startPointEvent.position.dy).abs() / 168 | (event.position.dx - startPointEvent.position.dx).abs()); 169 | if (arc > pi / 4) { 170 | if (event.position.dy > startPointEvent.position.dy) { 171 | this.widget.onEvent(VerticalEvent.DOWN); 172 | } else { 173 | this.widget.onEvent(VerticalEvent.UP); 174 | } 175 | startPointEvent = null; 176 | } 177 | } 178 | 179 | void _handlePointerDown(PointerDownEvent event) { 180 | startPointEvent = event; 181 | } 182 | 183 | void _handlePointerUp(PointerUpEvent event) { 184 | startPointEvent = null; 185 | } 186 | } 187 | 188 | ///圆形图片 189 | 190 | Widget buildCircleImage(double size, ImageProvider provider) { 191 | return SizedBox( 192 | width: size, 193 | height: size, 194 | child: DecoratedBox( 195 | decoration: BoxDecoration( 196 | shape: BoxShape.circle, 197 | image: DecorationImage(image: provider, fit: BoxFit.cover)), 198 | ), 199 | ); 200 | } 201 | 202 | Widget buildCircleImage2(double size, ImageProvider provider) { 203 | return ClipOval( 204 | child: Image( 205 | image: provider, 206 | width: size, 207 | height: size, 208 | fit: BoxFit.cover, 209 | )); 210 | } 211 | 212 | ///颜色析取 213 | class PalettePanel extends StatelessWidget { 214 | final PaletteGenerator paletteGenerator; 215 | 216 | const PalettePanel({Key key, this.paletteGenerator}) : super(key: key); 217 | 218 | @override 219 | Widget build(BuildContext context) { 220 | return IgnorePointer( 221 | child: ListView( 222 | children: [ 223 | Row( 224 | children: [ 225 | Container( 226 | width: 20, 227 | height: 20, 228 | color: paletteGenerator.lightMutedColor?.color, 229 | ), 230 | Text( 231 | "lightMuted", 232 | style: TextStyle( 233 | color: paletteGenerator.lightMutedColor?.titleTextColor, 234 | fontSize: 20), 235 | ), 236 | SizedBox( 237 | width: 4, 238 | ), 239 | Text( 240 | "lightMuted", 241 | style: TextStyle( 242 | color: paletteGenerator.lightMutedColor?.bodyTextColor, 243 | fontSize: 16), 244 | ) 245 | ], 246 | ), 247 | Row( 248 | children: [ 249 | Container( 250 | width: 20, 251 | height: 20, 252 | color: paletteGenerator.lightVibrantColor?.color, 253 | ), 254 | Text( 255 | "lightVibrant", 256 | style: TextStyle( 257 | color: paletteGenerator.lightVibrantColor?.titleTextColor, 258 | fontSize: 20), 259 | ), 260 | SizedBox( 261 | width: 4, 262 | ), 263 | Text( 264 | "lightMuted", 265 | style: TextStyle( 266 | color: paletteGenerator.lightVibrantColor?.bodyTextColor, 267 | fontSize: 16), 268 | ) 269 | ], 270 | ), 271 | Row( 272 | children: [ 273 | Container( 274 | width: 20, 275 | height: 20, 276 | color: paletteGenerator.darkMutedColor?.color, 277 | ), 278 | Text( 279 | "darkMutedColor", 280 | style: TextStyle( 281 | color: paletteGenerator.lightMutedColor?.titleTextColor, 282 | fontSize: 20), 283 | ), 284 | SizedBox( 285 | width: 4, 286 | ), 287 | Text( 288 | "darkMutedColor", 289 | style: TextStyle( 290 | color: paletteGenerator.darkMutedColor?.bodyTextColor, 291 | fontSize: 16), 292 | ) 293 | ], 294 | ), 295 | Row( 296 | children: [ 297 | Container( 298 | width: 20, 299 | height: 20, 300 | color: paletteGenerator.darkVibrantColor?.color, 301 | ), 302 | Text( 303 | "darkVibrant", 304 | style: TextStyle( 305 | color: paletteGenerator.darkVibrantColor?.titleTextColor, 306 | fontSize: 20), 307 | ), 308 | SizedBox( 309 | width: 4, 310 | ), 311 | Text( 312 | "darkVibrant", 313 | style: TextStyle( 314 | color: paletteGenerator.darkVibrantColor?.bodyTextColor, 315 | fontSize: 16), 316 | ) 317 | ], 318 | ), 319 | Row( 320 | children: [ 321 | Container( 322 | width: 20, 323 | height: 20, 324 | color: paletteGenerator.dominantColor?.color, 325 | ), 326 | Text( 327 | "dominant", 328 | style: TextStyle( 329 | color: paletteGenerator.dominantColor?.titleTextColor, 330 | fontSize: 20), 331 | ), 332 | SizedBox( 333 | width: 4, 334 | ), 335 | Text( 336 | "dominant", 337 | style: TextStyle( 338 | color: paletteGenerator.dominantColor?.bodyTextColor, 339 | fontSize: 16), 340 | ) 341 | ], 342 | ), 343 | Row( 344 | children: [ 345 | Container( 346 | width: 20, 347 | height: 20, 348 | color: paletteGenerator.mutedColor?.color, 349 | ), 350 | Text( 351 | "muted", 352 | style: TextStyle( 353 | color: paletteGenerator.mutedColor?.titleTextColor, 354 | fontSize: 20), 355 | ), 356 | SizedBox( 357 | width: 4, 358 | ), 359 | Text( 360 | "muted", 361 | style: TextStyle( 362 | color: paletteGenerator.mutedColor?.bodyTextColor, 363 | fontSize: 16), 364 | ) 365 | ], 366 | ), 367 | Row( 368 | children: [ 369 | Container( 370 | width: 20, 371 | height: 20, 372 | color: paletteGenerator.vibrantColor?.color, 373 | ), 374 | Text( 375 | "vibrant", 376 | style: TextStyle( 377 | color: paletteGenerator.vibrantColor?.titleTextColor, 378 | fontSize: 20), 379 | ), 380 | SizedBox( 381 | width: 4, 382 | ), 383 | Text( 384 | "vibrant", 385 | style: TextStyle( 386 | color: paletteGenerator.vibrantColor?.bodyTextColor, 387 | fontSize: 16), 388 | ) 389 | ], 390 | ), 391 | ], 392 | ), 393 | ); 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /observable_ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | build/ 31 | 32 | # Android related 33 | **/android/**/gradle-wrapper.jar 34 | **/android/.gradle 35 | **/android/captures/ 36 | **/android/gradlew 37 | **/android/gradlew.bat 38 | **/android/local.properties 39 | **/android/**/GeneratedPluginRegistrant.java 40 | 41 | # iOS/XCode related 42 | **/ios/**/*.mode1v3 43 | **/ios/**/*.mode2v3 44 | **/ios/**/*.moved-aside 45 | **/ios/**/*.pbxuser 46 | **/ios/**/*.perspectivev3 47 | **/ios/**/*sync/ 48 | **/ios/**/.sconsign.dblite 49 | **/ios/**/.tags* 50 | **/ios/**/.vagrant/ 51 | **/ios/**/DerivedData/ 52 | **/ios/**/Icon? 53 | **/ios/**/Pods/ 54 | **/ios/**/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/app.flx 62 | **/ios/Flutter/app.zip 63 | **/ios/Flutter/flutter_assets/ 64 | **/ios/Flutter/flutter_export_environment.sh 65 | **/ios/ServiceDefinitions.json 66 | **/ios/Runner/GeneratedPluginRegistrant.* 67 | 68 | # Exceptions to above rules. 69 | !**/ios/**/default.mode1v3 70 | !**/ios/**/default.mode2v3 71 | !**/ios/**/default.pbxuser 72 | !**/ios/**/default.perspectivev3 73 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 74 | -------------------------------------------------------------------------------- /observable_ui/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 83a8a575eed882c49b629effedf7c95b6184515d 8 | channel: master 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /observable_ui/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.0.1] - TODO: Add release date. 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /observable_ui/LICENSE: -------------------------------------------------------------------------------- 1 | TODO: Add your license here. 2 | -------------------------------------------------------------------------------- /observable_ui/README.md: -------------------------------------------------------------------------------- 1 | # observable_ui 2 | 3 | wrap flutter widget for react data change 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Dart 8 | [package](https://flutter.dev/developing-packages/), 9 | a library module containing code that can be shared easily across 10 | multiple Flutter or Dart projects. 11 | 12 | For help getting started with Flutter, view our 13 | [online documentation](https://flutter.dev/docs), which offers tutorials, 14 | samples, guidance on mobile development, and a full API reference. 15 | -------------------------------------------------------------------------------- /observable_ui/lib/core.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | 5 | abstract class Observer { 6 | void onChanged(); 7 | } 8 | 9 | class CommonObserver implements Observer { 10 | final VoidCallback callback; 11 | 12 | CommonObserver(this.callback); 13 | 14 | @override 15 | void onChanged() { 16 | this.callback(); 17 | } 18 | } 19 | 20 | mixin ObserverMixin on State implements Observer { 21 | @override 22 | void onChanged() { 23 | setState(() {}); 24 | } 25 | } 26 | 27 | class ObservableList with ListMixin, Observable { 28 | final List _value = []; 29 | 30 | ObservableList({List initValue}) { 31 | if (initValue != null) { 32 | _value.addAll(initValue); 33 | } 34 | } 35 | 36 | @override 37 | int get length => _value.length; 38 | 39 | @override 40 | T operator [](int index) { 41 | return _value[index]; 42 | } 43 | 44 | @override 45 | void operator []=(int index, T value) { 46 | _value[index] = value; 47 | notifyObservers(); 48 | } 49 | 50 | @override 51 | set length(int newLength) { 52 | _value.length = newLength; 53 | notifyObservers(); 54 | } 55 | } 56 | 57 | class ObservableValue with Observable { 58 | ObservableValue(T initValue) { 59 | this._value = initValue; 60 | } 61 | 62 | T _oldValue; 63 | 64 | T get oldValue => _oldValue; 65 | 66 | T _value; 67 | 68 | T get value => _value; 69 | 70 | set value(newValue) { 71 | _oldValue = _value; 72 | _value = newValue; 73 | notifyObservers(); 74 | } 75 | } 76 | 77 | mixin Observable { 78 | List _observers; 79 | 80 | void notifyObservers() { 81 | _observers?.forEach((observer) { 82 | observer.onChanged(); 83 | }); 84 | } 85 | 86 | void removeObservers() { 87 | _observers?.clear(); 88 | } 89 | 90 | void removeObserver(Observer observer) { 91 | _observers?.remove(observer); 92 | } 93 | 94 | void addObserver(Observer observer) { 95 | if (_observers == null) { 96 | _observers = []; 97 | } 98 | this._observers.add(observer); 99 | } 100 | } 101 | 102 | abstract class StateMixinObserver extends State 103 | with ObserverMixin { 104 | List _observables = []; 105 | 106 | List collectObservables(); 107 | 108 | @mustCallSuper 109 | @override 110 | void initState() { 111 | super.initState(); 112 | addObservables(collectObservables()); 113 | } 114 | 115 | void addObservables(List observables) { 116 | observables?.forEach((observable) { 117 | observable.addObserver(this); 118 | }); 119 | _observables.addAll(observables); 120 | } 121 | 122 | @mustCallSuper 123 | @override 124 | void dispose() { 125 | super.dispose(); 126 | _observables.forEach((observable) { 127 | observable.removeObserver(this); 128 | }); 129 | _observables.clear(); 130 | } 131 | } 132 | 133 | ///ObservableBridge 134 | class ObservableBridge extends StatefulWidget { 135 | final Widget Function(BuildContext context) childBuilder; 136 | 137 | final List data; 138 | 139 | const ObservableBridge({Key key, @required this.data, this.childBuilder}) 140 | : assert( 141 | childBuilder != null, 142 | ' childBuilder are null.', 143 | ), 144 | super(key: key); 145 | 146 | @override 147 | State createState() { 148 | return _ObservableBridgeState(); 149 | } 150 | } 151 | 152 | class _ObservableBridgeState extends StateMixinObserver { 153 | @override 154 | Widget build(BuildContext context) { 155 | return widget.childBuilder(context); 156 | } 157 | 158 | @override 159 | List collectObservables() { 160 | final observables = []; 161 | observables.addAll(widget.data); 162 | return observables; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /observable_ui/lib/core2.dart: -------------------------------------------------------------------------------- 1 | import 'dart:collection'; 2 | 3 | import 'package:flutter/widgets.dart'; 4 | 5 | class ListenableList extends ChangeNotifier with ListMixin { 6 | final List _value = []; 7 | 8 | ListenableList({List initValue}) { 9 | if (initValue != null) { 10 | _value.addAll(initValue); 11 | } 12 | } 13 | 14 | @override 15 | int get length => _value.length; 16 | 17 | @override 18 | T operator [](int index) { 19 | return _value[index]; 20 | } 21 | 22 | @override 23 | void operator []=(int index, T value) { 24 | _value[index] = value; 25 | notifyListeners(); 26 | } 27 | 28 | @override 29 | set length(int newLength) { 30 | _value.length = newLength; 31 | notifyListeners(); 32 | } 33 | } 34 | 35 | mixin ListenerMixin on State { 36 | void onChanged() { 37 | setState(() {}); 38 | } 39 | } 40 | 41 | abstract class StateMixinListener extends State 42 | with ListenerMixin { 43 | List _listenables = []; 44 | 45 | List collectListenable(); 46 | 47 | @mustCallSuper 48 | @override 49 | void initState() { 50 | super.initState(); 51 | _addListenables(collectListenable()); 52 | } 53 | 54 | void _addListenables(List listenables) { 55 | listenables?.forEach((listenable) { 56 | listenable.addListener(this.onChanged); 57 | }); 58 | _listenables.addAll(listenables); 59 | } 60 | 61 | @mustCallSuper 62 | @override 63 | void dispose() { 64 | super.dispose(); 65 | _listenables.forEach((listenable) { 66 | listenable.removeListener(this.onChanged); 67 | }); 68 | _listenables.clear(); 69 | } 70 | } 71 | 72 | class ListenableBridge extends StatefulWidget { 73 | final Widget Function(BuildContext context) childBuilder; 74 | 75 | final List data; 76 | 77 | const ListenableBridge({Key key, @required this.data, this.childBuilder}) 78 | : assert( 79 | childBuilder != null, 80 | ' childBuilder are null.', 81 | ), 82 | super(key: key); 83 | 84 | @override 85 | State createState() { 86 | return _ListenableBridgeState(); 87 | } 88 | } 89 | 90 | class _ListenableBridgeState extends StateMixinListener { 91 | @override 92 | Widget build(BuildContext context) { 93 | return widget.childBuilder(context); 94 | } 95 | 96 | @override 97 | List collectListenable() { 98 | final listenables = []; 99 | listenables.addAll(widget.data); 100 | return listenables; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /observable_ui/lib/provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | Type _typeOf() => T; 4 | 5 | class ViewModelNotFoundError extends Error { 6 | /// The type of the value being retrieved 7 | final Type valueType; 8 | 9 | /// The type of the Widget requesting the value 10 | final Type widgetType; 11 | 12 | /// Create a ProviderNotFound error with the type represented as a String. 13 | ViewModelNotFoundError( 14 | this.valueType, 15 | this.widgetType, 16 | ); 17 | 18 | @override 19 | String toString() { 20 | return ''' 21 | Error: Could not find the correct ViewModelProvider<$valueType> above this $widgetType Widget 22 | '''; 23 | } 24 | } 25 | 26 | class ViewModelProvider extends InheritedWidget { 27 | final T viewModel; 28 | 29 | const ViewModelProvider({@required this.viewModel, Key key, Widget child}) 30 | : assert(viewModel != null, "ViewModel can not be null"), 31 | super(key: key, child: child); 32 | 33 | @override 34 | bool updateShouldNotify(ViewModelProvider oldWidget) { 35 | return this.viewModel != oldWidget.viewModel; 36 | } 37 | 38 | static T of(BuildContext context, {bool listen = true}) { 39 | final type = _typeOf>(); 40 | final provider = listen 41 | ? context.inheritFromWidgetOfExactType(type) as ViewModelProvider 42 | : context.ancestorInheritedElementForWidgetOfExactType(type)?.widget 43 | as ViewModelProvider; 44 | 45 | if (provider == null) { 46 | throw ViewModelNotFoundError(T, context.widget.runtimeType); 47 | } 48 | return provider.viewModel; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /observable_ui/lib/widgets.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/gestures.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'core.dart'; 8 | 9 | ///EditableTextEx support two-way binding 10 | class EditableTextEx extends StatefulWidget { 11 | final EditableText child; 12 | 13 | final ObservableValue data; 14 | 15 | const EditableTextEx({Key key, this.child, this.data}) : super(key: key); 16 | 17 | @override 18 | State createState() { 19 | return EditableTextExState(); 20 | } 21 | } 22 | 23 | class EditableTextExState extends StateMixinObserver { 24 | EditableText wrapperEditable; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | var editableText = this.widget.child; 30 | var controller = editableText.controller; 31 | if (controller == null) { 32 | controller = TextEditingController(); 33 | } 34 | controller.text = this.widget.data.value; 35 | wrapperEditable = EditableText( 36 | style: editableText.style, 37 | backgroundCursorColor: editableText.backgroundCursorColor, 38 | enableInteractiveSelection: editableText.enableInteractiveSelection, 39 | expands: editableText.expands, 40 | cursorWidth: editableText.cursorWidth, 41 | cursorOffset: editableText.cursorOffset, 42 | cursorColor: editableText.cursorColor, 43 | cursorOpacityAnimates: editableText.cursorOpacityAnimates, 44 | focusNode: editableText.focusNode, 45 | inputFormatters: editableText.inputFormatters, 46 | textInputAction: editableText.textInputAction, 47 | textAlign: editableText.textAlign, 48 | textCapitalization: editableText.textCapitalization, 49 | textDirection: editableText.textDirection, 50 | textScaleFactor: editableText.textScaleFactor, 51 | controller: controller, 52 | onChanged: (text) { 53 | this.widget.data.value = text; 54 | }, 55 | obscureText: editableText.obscureText, 56 | onEditingComplete: editableText.onEditingComplete, 57 | onSelectionChanged: editableText.onSelectionChanged, 58 | onSelectionHandleTapped: editableText.onSelectionHandleTapped, 59 | scrollController: editableText.scrollController, 60 | scrollPadding: editableText.scrollPadding, 61 | scrollPhysics: editableText.scrollPhysics, 62 | showCursor: editableText.showCursor, 63 | showSelectionHandles: editableText.showSelectionHandles, 64 | strutStyle: editableText.strutStyle, 65 | selectionColor: editableText.selectionColor, 66 | selectionControls: editableText.selectionControls, 67 | autofocus: editableText.autofocus, 68 | autocorrect: editableText.autocorrect, 69 | paintCursorAboveText: editableText.paintCursorAboveText, 70 | dragStartBehavior: editableText.dragStartBehavior, 71 | enableSuggestions: editableText.enableSuggestions, 72 | rendererIgnoresPointer: editableText.rendererIgnoresPointer, 73 | minLines: editableText.minLines, 74 | maxLines: editableText.maxLines, 75 | forceLine: editableText.forceLine, 76 | ); 77 | } 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | wrapperEditable.controller.text = this.widget.data.value; 82 | return wrapperEditable; 83 | } 84 | 85 | @override 86 | void setState(fn) { 87 | if (this.wrapperEditable.controller.text == this.widget.data.value) { 88 | return; 89 | } 90 | super.setState(fn); 91 | } 92 | 93 | @override 94 | List collectObservables() => [this.widget.data]; 95 | } 96 | 97 | ///ImageEx 98 | class ImageEx extends StatefulWidget { 99 | const ImageEx({Key key, this.src, this.width, this.height}) : super(key: key); 100 | 101 | ///file path , network url ,asset name 102 | final ObservableValue src; 103 | 104 | final double width; 105 | 106 | final double height; 107 | 108 | @override 109 | State createState() { 110 | return ImageExState(); 111 | } 112 | } 113 | 114 | class ImageExState extends StateMixinObserver { 115 | @override 116 | List collectObservables() => [this.widget.src]; 117 | 118 | @override 119 | Widget build(BuildContext context) { 120 | var img = this.widget.src.value; 121 | if (img.startsWith("http")) { 122 | return Image.network( 123 | img, 124 | width: this.widget.width, 125 | height: this.widget.height, 126 | ); 127 | } 128 | if (img.startsWith("/")) { 129 | return Image.file( 130 | File(img), 131 | width: this.widget.width, 132 | height: this.widget.height, 133 | ); 134 | } 135 | return Image.asset( 136 | img, 137 | width: this.widget.width, 138 | height: this.widget.height, 139 | ); 140 | } 141 | } 142 | 143 | ///CheckBoxEx support two-way binding 144 | class CheckboxEx extends StatefulWidget { 145 | final Checkbox child; 146 | 147 | final ObservableValue data; 148 | 149 | const CheckboxEx({Key key, this.data, this.child}) : super(key: key); 150 | 151 | @override 152 | State createState() { 153 | return _CheckboxExState(); 154 | } 155 | } 156 | 157 | class _CheckboxExState extends StateMixinObserver { 158 | Checkbox wrapperCheckbox; 159 | 160 | @override 161 | void initState() { 162 | super.initState(); 163 | var cb = this.widget.child; 164 | wrapperCheckbox = Checkbox( 165 | tristate: cb.tristate, 166 | materialTapTargetSize: cb.materialTapTargetSize, 167 | value: this.widget.data.value, 168 | activeColor: cb.activeColor, 169 | checkColor: cb.checkColor, 170 | onChanged: (v) { 171 | this.widget.data.value = v; 172 | }, 173 | ); 174 | } 175 | 176 | @override 177 | Widget build(BuildContext context) { 178 | return wrapperCheckbox; 179 | } 180 | 181 | @override 182 | void setState(fn) { 183 | if (this.wrapperCheckbox.value == this.widget.data.value) { 184 | return; 185 | } 186 | super.setState(fn); 187 | } 188 | 189 | @override 190 | List collectObservables() { 191 | return [this.widget.data]; 192 | } 193 | } 194 | 195 | ///ListViewEx 196 | 197 | typedef ItemWidgetBuilder = Widget Function(BuildContext context, T item); 198 | 199 | class _ListViewBuilder { 200 | final WidgetBuilder builder; 201 | 202 | _ListViewBuilder(this.builder); 203 | } 204 | 205 | class ListViewEx extends StatefulWidget { 206 | final ObservableList items; 207 | 208 | final _ListViewBuilder listViewBuilder; 209 | 210 | const ListViewEx({Key key, this.items, this.listViewBuilder}) 211 | : super(key: key); 212 | 213 | ListViewEx.builder({ 214 | Key key, 215 | this.items, 216 | Axis scrollDirection = Axis.vertical, 217 | bool reverse = false, 218 | ScrollController controller, 219 | bool primary, 220 | ScrollPhysics physics, 221 | bool shrinkWrap = false, 222 | EdgeInsetsGeometry padding, 223 | double itemExtent, 224 | @required ItemWidgetBuilder itemBuilder, 225 | bool addAutomaticKeepAlives = true, 226 | bool addRepaintBoundaries = true, 227 | bool addSemanticIndexes = true, 228 | double cacheExtent, 229 | int semanticChildCount, 230 | DragStartBehavior dragStartBehavior = DragStartBehavior.start, 231 | }) : listViewBuilder = _ListViewBuilder((context) { 232 | return ListView.builder( 233 | itemBuilder: (context, index) { 234 | return itemBuilder(context, items[index]); 235 | }, 236 | scrollDirection: scrollDirection, 237 | reverse: reverse, 238 | controller: controller, 239 | primary: primary, 240 | physics: physics, 241 | shrinkWrap: shrinkWrap, 242 | padding: padding, 243 | itemCount: items.length, 244 | addAutomaticKeepAlives: addAutomaticKeepAlives, 245 | addRepaintBoundaries: addRepaintBoundaries, 246 | addSemanticIndexes: addSemanticIndexes, 247 | cacheExtent: cacheExtent, 248 | semanticChildCount: semanticChildCount, 249 | dragStartBehavior: dragStartBehavior, 250 | ); 251 | }), 252 | super(key: key); 253 | 254 | @override 255 | State createState() { 256 | return _ListViewExState(); 257 | } 258 | } 259 | 260 | class _ListViewExState extends StateMixinObserver { 261 | @override 262 | Widget build(BuildContext context) { 263 | return this.widget.listViewBuilder.builder(context); 264 | } 265 | 266 | @override 267 | List collectObservables() => [this.widget.items]; 268 | } 269 | 270 | ///ExchangeEx child1 visible when status is true 271 | class ExchangeEx extends StatefulWidget { 272 | final Widget child1; 273 | 274 | final Widget child2; 275 | 276 | final ObservableValue status; 277 | 278 | const ExchangeEx( 279 | {Key key, 280 | @required this.child1, 281 | @required this.child2, 282 | @required this.status}) 283 | : super(key: key); 284 | 285 | @override 286 | State createState() { 287 | return ExchangeExState(); 288 | } 289 | } 290 | 291 | class ExchangeExState extends StateMixinObserver { 292 | @override 293 | Widget build(BuildContext context) { 294 | return Stack( 295 | children: [ 296 | Visibility( 297 | visible: this.widget.status.value, 298 | child: this.widget.child1, 299 | ), 300 | Visibility( 301 | visible: !this.widget.status.value, 302 | child: this.widget.child2, 303 | ) 304 | ], 305 | ); 306 | } 307 | 308 | @override 309 | List collectObservables() => [this.widget.status]; 310 | } 311 | -------------------------------------------------------------------------------- /observable_ui/lib/widgets2.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/gestures.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | import 'core2.dart'; 8 | 9 | ///EditableTextEx support two-way binding 10 | class EditableTextEx extends StatefulWidget { 11 | final EditableText child; 12 | 13 | final ValueNotifier data; 14 | 15 | const EditableTextEx({Key key, this.child, this.data}) : super(key: key); 16 | 17 | @override 18 | State createState() { 19 | return EditableTextExState(); 20 | } 21 | } 22 | 23 | class EditableTextExState extends StateMixinListener { 24 | EditableText wrapperEditable; 25 | 26 | @override 27 | void initState() { 28 | super.initState(); 29 | var editableText = this.widget.child; 30 | var controller = editableText.controller; 31 | if (controller == null) { 32 | controller = TextEditingController(); 33 | } 34 | controller.text = this.widget.data.value; 35 | wrapperEditable = EditableText( 36 | style: editableText.style, 37 | backgroundCursorColor: editableText.backgroundCursorColor, 38 | enableInteractiveSelection: editableText.enableInteractiveSelection, 39 | expands: editableText.expands, 40 | cursorWidth: editableText.cursorWidth, 41 | cursorOffset: editableText.cursorOffset, 42 | cursorColor: editableText.cursorColor, 43 | cursorOpacityAnimates: editableText.cursorOpacityAnimates, 44 | focusNode: editableText.focusNode, 45 | inputFormatters: editableText.inputFormatters, 46 | textInputAction: editableText.textInputAction, 47 | textAlign: editableText.textAlign, 48 | textCapitalization: editableText.textCapitalization, 49 | textDirection: editableText.textDirection, 50 | textScaleFactor: editableText.textScaleFactor, 51 | controller: controller, 52 | onChanged: (text) { 53 | this.widget.data.value = text; 54 | }, 55 | obscureText: editableText.obscureText, 56 | onEditingComplete: editableText.onEditingComplete, 57 | onSelectionChanged: editableText.onSelectionChanged, 58 | onSelectionHandleTapped: editableText.onSelectionHandleTapped, 59 | scrollController: editableText.scrollController, 60 | scrollPadding: editableText.scrollPadding, 61 | scrollPhysics: editableText.scrollPhysics, 62 | showCursor: editableText.showCursor, 63 | showSelectionHandles: editableText.showSelectionHandles, 64 | strutStyle: editableText.strutStyle, 65 | selectionColor: editableText.selectionColor, 66 | selectionControls: editableText.selectionControls, 67 | autofocus: editableText.autofocus, 68 | autocorrect: editableText.autocorrect, 69 | paintCursorAboveText: editableText.paintCursorAboveText, 70 | dragStartBehavior: editableText.dragStartBehavior, 71 | enableSuggestions: editableText.enableSuggestions, 72 | rendererIgnoresPointer: editableText.rendererIgnoresPointer, 73 | minLines: editableText.minLines, 74 | maxLines: editableText.maxLines, 75 | forceLine: editableText.forceLine, 76 | ); 77 | } 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | wrapperEditable.controller.text = this.widget.data.value; 82 | return wrapperEditable; 83 | } 84 | 85 | @override 86 | void setState(fn) { 87 | if (this.wrapperEditable.controller.text == this.widget.data.value) { 88 | return; 89 | } 90 | super.setState(fn); 91 | } 92 | 93 | @override 94 | List collectListenable() => [this.widget.data]; 95 | } 96 | 97 | ///ImageEx 98 | class ImageEx extends StatefulWidget { 99 | const ImageEx({Key key, this.src, this.width, this.height}) : super(key: key); 100 | 101 | ///file path , network url ,asset name 102 | final ValueNotifier src; 103 | 104 | final double width; 105 | 106 | final double height; 107 | 108 | @override 109 | State createState() { 110 | return ImageExState(); 111 | } 112 | } 113 | 114 | class ImageExState extends StateMixinListener { 115 | @override 116 | List collectListenable() => [this.widget.src]; 117 | 118 | @override 119 | Widget build(BuildContext context) { 120 | var img = this.widget.src.value; 121 | if (img.startsWith("http")) { 122 | return Image.network( 123 | img, 124 | width: this.widget.width, 125 | height: this.widget.height, 126 | ); 127 | } 128 | if (img.startsWith("/")) { 129 | return Image.file( 130 | File(img), 131 | width: this.widget.width, 132 | height: this.widget.height, 133 | ); 134 | } 135 | return Image.asset( 136 | img, 137 | width: this.widget.width, 138 | height: this.widget.height, 139 | ); 140 | } 141 | } 142 | 143 | ///CheckBoxEx support two-way binding 144 | class CheckboxEx extends StatefulWidget { 145 | final Checkbox child; 146 | 147 | final ValueNotifier data; 148 | 149 | const CheckboxEx({Key key, this.data, this.child}) : super(key: key); 150 | 151 | @override 152 | State createState() { 153 | return _CheckboxExState(); 154 | } 155 | } 156 | 157 | class _CheckboxExState extends StateMixinListener { 158 | Checkbox wrapperCheckbox; 159 | 160 | @override 161 | void initState() { 162 | super.initState(); 163 | var cb = this.widget.child; 164 | wrapperCheckbox = Checkbox( 165 | tristate: cb.tristate, 166 | materialTapTargetSize: cb.materialTapTargetSize, 167 | value: this.widget.data.value, 168 | activeColor: cb.activeColor, 169 | checkColor: cb.checkColor, 170 | onChanged: (v) { 171 | this.widget.data.value = v; 172 | }, 173 | ); 174 | } 175 | 176 | @override 177 | Widget build(BuildContext context) { 178 | return wrapperCheckbox; 179 | } 180 | 181 | @override 182 | void setState(fn) { 183 | if (this.wrapperCheckbox.value == this.widget.data.value) { 184 | return; 185 | } 186 | super.setState(fn); 187 | } 188 | 189 | @override 190 | List collectListenable() { 191 | return [this.widget.data]; 192 | } 193 | } 194 | 195 | ///ListViewEx 196 | 197 | typedef ItemWidgetBuilder = Widget Function(BuildContext context, T item); 198 | 199 | class _ListViewBuilder { 200 | final WidgetBuilder builder; 201 | 202 | _ListViewBuilder(this.builder); 203 | } 204 | 205 | class ListViewEx extends StatefulWidget { 206 | final ListenableList items; 207 | 208 | final _ListViewBuilder listViewBuilder; 209 | 210 | const ListViewEx({Key key, this.items, this.listViewBuilder}) 211 | : super(key: key); 212 | 213 | ListViewEx.builder({ 214 | Key key, 215 | this.items, 216 | Axis scrollDirection = Axis.vertical, 217 | bool reverse = false, 218 | ScrollController controller, 219 | bool primary, 220 | ScrollPhysics physics, 221 | bool shrinkWrap = false, 222 | EdgeInsetsGeometry padding, 223 | double itemExtent, 224 | @required ItemWidgetBuilder itemBuilder, 225 | bool addAutomaticKeepAlives = true, 226 | bool addRepaintBoundaries = true, 227 | bool addSemanticIndexes = true, 228 | double cacheExtent, 229 | int semanticChildCount, 230 | DragStartBehavior dragStartBehavior = DragStartBehavior.start, 231 | }) : listViewBuilder = _ListViewBuilder((context) { 232 | return ListView.builder( 233 | itemBuilder: (context, index) { 234 | return itemBuilder(context, items[index]); 235 | }, 236 | scrollDirection: scrollDirection, 237 | reverse: reverse, 238 | controller: controller, 239 | primary: primary, 240 | physics: physics, 241 | shrinkWrap: shrinkWrap, 242 | padding: padding, 243 | itemCount: items.length, 244 | addAutomaticKeepAlives: addAutomaticKeepAlives, 245 | addRepaintBoundaries: addRepaintBoundaries, 246 | addSemanticIndexes: addSemanticIndexes, 247 | cacheExtent: cacheExtent, 248 | semanticChildCount: semanticChildCount, 249 | dragStartBehavior: dragStartBehavior, 250 | ); 251 | }), 252 | super(key: key); 253 | 254 | @override 255 | State createState() { 256 | return _ListViewExState(); 257 | } 258 | } 259 | 260 | class _ListViewExState extends StateMixinListener { 261 | @override 262 | Widget build(BuildContext context) { 263 | return this.widget.listViewBuilder.builder(context); 264 | } 265 | 266 | @override 267 | List collectListenable() => [this.widget.items]; 268 | } 269 | 270 | ///ExchangeEx child1 visible when status is true 271 | class ExchangeEx extends StatefulWidget { 272 | final Widget child1; 273 | 274 | final Widget child2; 275 | 276 | final ValueNotifier status; 277 | 278 | const ExchangeEx( 279 | {Key key, 280 | @required this.child1, 281 | @required this.child2, 282 | @required this.status}) 283 | : super(key: key); 284 | 285 | @override 286 | State createState() { 287 | return ExchangeExState(); 288 | } 289 | } 290 | 291 | class ExchangeExState extends StateMixinListener { 292 | @override 293 | Widget build(BuildContext context) { 294 | return Stack( 295 | children: [ 296 | Visibility( 297 | visible: this.widget.status.value, 298 | child: this.widget.child1, 299 | ), 300 | Visibility( 301 | visible: !this.widget.status.value, 302 | child: this.widget.child2, 303 | ) 304 | ], 305 | ); 306 | } 307 | 308 | @override 309 | List collectListenable() => [this.widget.status]; 310 | } 311 | -------------------------------------------------------------------------------- /observable_ui/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.5.0-nullsafety.1" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "2.1.0-nullsafety.1" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "1.1.0-nullsafety.3" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.2.0-nullsafety.1" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.1.0-nullsafety.1" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.15.0-nullsafety.3" 46 | fake_async: 47 | dependency: transitive 48 | description: 49 | name: fake_async 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "1.2.0-nullsafety.1" 53 | flutter: 54 | dependency: "direct main" 55 | description: flutter 56 | source: sdk 57 | version: "0.0.0" 58 | flutter_test: 59 | dependency: "direct dev" 60 | description: flutter 61 | source: sdk 62 | version: "0.0.0" 63 | matcher: 64 | dependency: transitive 65 | description: 66 | name: matcher 67 | url: "https://pub.flutter-io.cn" 68 | source: hosted 69 | version: "0.12.10-nullsafety.1" 70 | meta: 71 | dependency: transitive 72 | description: 73 | name: meta 74 | url: "https://pub.flutter-io.cn" 75 | source: hosted 76 | version: "1.3.0-nullsafety.3" 77 | path: 78 | dependency: transitive 79 | description: 80 | name: path 81 | url: "https://pub.flutter-io.cn" 82 | source: hosted 83 | version: "1.8.0-nullsafety.1" 84 | sky_engine: 85 | dependency: transitive 86 | description: flutter 87 | source: sdk 88 | version: "0.0.99" 89 | source_span: 90 | dependency: transitive 91 | description: 92 | name: source_span 93 | url: "https://pub.flutter-io.cn" 94 | source: hosted 95 | version: "1.8.0-nullsafety.2" 96 | stack_trace: 97 | dependency: transitive 98 | description: 99 | name: stack_trace 100 | url: "https://pub.flutter-io.cn" 101 | source: hosted 102 | version: "1.10.0-nullsafety.1" 103 | stream_channel: 104 | dependency: transitive 105 | description: 106 | name: stream_channel 107 | url: "https://pub.flutter-io.cn" 108 | source: hosted 109 | version: "2.1.0-nullsafety.1" 110 | string_scanner: 111 | dependency: transitive 112 | description: 113 | name: string_scanner 114 | url: "https://pub.flutter-io.cn" 115 | source: hosted 116 | version: "1.1.0-nullsafety.1" 117 | term_glyph: 118 | dependency: transitive 119 | description: 120 | name: term_glyph 121 | url: "https://pub.flutter-io.cn" 122 | source: hosted 123 | version: "1.2.0-nullsafety.1" 124 | test_api: 125 | dependency: transitive 126 | description: 127 | name: test_api 128 | url: "https://pub.flutter-io.cn" 129 | source: hosted 130 | version: "0.2.19-nullsafety.2" 131 | typed_data: 132 | dependency: transitive 133 | description: 134 | name: typed_data 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "1.3.0-nullsafety.3" 138 | vector_math: 139 | dependency: transitive 140 | description: 141 | name: vector_math 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "2.1.0-nullsafety.3" 145 | sdks: 146 | dart: ">=2.10.1 <2.11.0" 147 | -------------------------------------------------------------------------------- /observable_ui/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: observable_ui 2 | description: wrap flutter widget for react data change 3 | version: 0.0.1 4 | author: 5 | homepage: 6 | 7 | environment: 8 | sdk: ^2.10.1 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | # For information on the generic Dart part of this file, see the 19 | # following page: https://dart.dev/tools/pub/pubspec 20 | 21 | # The following section is specific to Flutter. 22 | flutter: 23 | 24 | # To add assets to your package, add an assets section, like this: 25 | # assets: 26 | # - images/a_dot_burr.jpeg 27 | # - images/a_dot_ham.jpeg 28 | # 29 | # For details regarding assets in packages, see 30 | # https://flutter.dev/assets-and-images/#from-packages 31 | # 32 | # An image asset can refer to one or more resolution-specific "variants", see 33 | # https://flutter.dev/assets-and-images/#resolution-aware. 34 | 35 | # To add custom fonts to your package, add a fonts section here, 36 | # in this "flutter" section. Each entry in this list should have a 37 | # "family" key with the font family name, and a "fonts" key with a 38 | # list giving the asset and other descriptors for the font. For 39 | # example: 40 | # fonts: 41 | # - family: Schyler 42 | # fonts: 43 | # - asset: fonts/Schyler-Regular.ttf 44 | # - asset: fonts/Schyler-Italic.ttf 45 | # style: italic 46 | # - family: Trajan Pro 47 | # fonts: 48 | # - asset: fonts/TrajanPro.ttf 49 | # - asset: fonts/TrajanPro_Bold.ttf 50 | # weight: 700 51 | # 52 | # For details regarding fonts in packages, see 53 | # https://flutter.dev/custom-fonts/#from-packages 54 | -------------------------------------------------------------------------------- /observable_ui/test/observable_ui_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.flutter-io.cn" 9 | source: hosted 10 | version: "2.5.0-nullsafety.1" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.flutter-io.cn" 16 | source: hosted 17 | version: "2.1.0-nullsafety.1" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.flutter-io.cn" 23 | source: hosted 24 | version: "1.1.0-nullsafety.3" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.flutter-io.cn" 30 | source: hosted 31 | version: "1.2.0-nullsafety.1" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.flutter-io.cn" 37 | source: hosted 38 | version: "1.1.0-nullsafety.1" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.flutter-io.cn" 44 | source: hosted 45 | version: "1.15.0-nullsafety.3" 46 | convert: 47 | dependency: transitive 48 | description: 49 | name: convert 50 | url: "https://pub.flutter-io.cn" 51 | source: hosted 52 | version: "2.1.1" 53 | crypto: 54 | dependency: transitive 55 | description: 56 | name: crypto 57 | url: "https://pub.flutter-io.cn" 58 | source: hosted 59 | version: "2.1.5" 60 | cupertino_icons: 61 | dependency: "direct main" 62 | description: 63 | name: cupertino_icons 64 | url: "https://pub.flutter-io.cn" 65 | source: hosted 66 | version: "0.1.3" 67 | fake_async: 68 | dependency: transitive 69 | description: 70 | name: fake_async 71 | url: "https://pub.flutter-io.cn" 72 | source: hosted 73 | version: "1.2.0-nullsafety.1" 74 | file: 75 | dependency: transitive 76 | description: 77 | name: file 78 | url: "https://pub.flutter-io.cn" 79 | source: hosted 80 | version: "5.2.1" 81 | flutter: 82 | dependency: "direct main" 83 | description: flutter 84 | source: sdk 85 | version: "0.0.0" 86 | flutter_sound: 87 | dependency: "direct main" 88 | description: 89 | name: flutter_sound 90 | url: "https://pub.flutter-io.cn" 91 | source: hosted 92 | version: "6.0.1" 93 | flutter_spinkit: 94 | dependency: transitive 95 | description: 96 | name: flutter_spinkit 97 | url: "https://pub.flutter-io.cn" 98 | source: hosted 99 | version: "4.1.2+1" 100 | flutter_test: 101 | dependency: "direct dev" 102 | description: flutter 103 | source: sdk 104 | version: "0.0.0" 105 | flutter_web_plugins: 106 | dependency: transitive 107 | description: flutter 108 | source: sdk 109 | version: "0.0.0" 110 | http: 111 | dependency: "direct main" 112 | description: 113 | name: http 114 | url: "https://pub.flutter-io.cn" 115 | source: hosted 116 | version: "0.11.3+17" 117 | http_parser: 118 | dependency: transitive 119 | description: 120 | name: http_parser 121 | url: "https://pub.flutter-io.cn" 122 | source: hosted 123 | version: "3.1.3" 124 | image_picker: 125 | dependency: "direct main" 126 | description: 127 | name: image_picker 128 | url: "https://pub.flutter-io.cn" 129 | source: hosted 130 | version: "0.6.1" 131 | intl: 132 | dependency: transitive 133 | description: 134 | name: intl 135 | url: "https://pub.flutter-io.cn" 136 | source: hosted 137 | version: "0.16.1" 138 | logger: 139 | dependency: transitive 140 | description: 141 | name: logger 142 | url: "https://pub.flutter-io.cn" 143 | source: hosted 144 | version: "0.7.0+2" 145 | matcher: 146 | dependency: transitive 147 | description: 148 | name: matcher 149 | url: "https://pub.flutter-io.cn" 150 | source: hosted 151 | version: "0.12.10-nullsafety.1" 152 | meta: 153 | dependency: transitive 154 | description: 155 | name: meta 156 | url: "https://pub.flutter-io.cn" 157 | source: hosted 158 | version: "1.3.0-nullsafety.3" 159 | nested: 160 | dependency: transitive 161 | description: 162 | name: nested 163 | url: "https://pub.flutter-io.cn" 164 | source: hosted 165 | version: "0.0.4" 166 | observable_ui: 167 | dependency: "direct main" 168 | description: 169 | path: observable_ui 170 | relative: true 171 | source: path 172 | version: "0.0.1" 173 | palette_generator: 174 | dependency: "direct main" 175 | description: 176 | name: palette_generator 177 | url: "https://pub.flutter-io.cn" 178 | source: hosted 179 | version: "0.2.3" 180 | path: 181 | dependency: transitive 182 | description: 183 | name: path 184 | url: "https://pub.flutter-io.cn" 185 | source: hosted 186 | version: "1.8.0-nullsafety.1" 187 | path_provider: 188 | dependency: "direct main" 189 | description: 190 | name: path_provider 191 | url: "https://pub.flutter-io.cn" 192 | source: hosted 193 | version: "1.6.11" 194 | path_provider_linux: 195 | dependency: transitive 196 | description: 197 | name: path_provider_linux 198 | url: "https://pub.flutter-io.cn" 199 | source: hosted 200 | version: "0.0.1+2" 201 | path_provider_macos: 202 | dependency: transitive 203 | description: 204 | name: path_provider_macos 205 | url: "https://pub.flutter-io.cn" 206 | source: hosted 207 | version: "0.0.4" 208 | path_provider_platform_interface: 209 | dependency: transitive 210 | description: 211 | name: path_provider_platform_interface 212 | url: "https://pub.flutter-io.cn" 213 | source: hosted 214 | version: "1.0.1" 215 | permission_handler: 216 | dependency: "direct main" 217 | description: 218 | name: permission_handler 219 | url: "https://pub.flutter-io.cn" 220 | source: hosted 221 | version: "5.0.1+1" 222 | permission_handler_platform_interface: 223 | dependency: transitive 224 | description: 225 | name: permission_handler_platform_interface 226 | url: "https://pub.flutter-io.cn" 227 | source: hosted 228 | version: "2.0.1" 229 | platform: 230 | dependency: transitive 231 | description: 232 | name: platform 233 | url: "https://pub.flutter-io.cn" 234 | source: hosted 235 | version: "2.2.1" 236 | plugin_platform_interface: 237 | dependency: transitive 238 | description: 239 | name: plugin_platform_interface 240 | url: "https://pub.flutter-io.cn" 241 | source: hosted 242 | version: "1.0.2" 243 | process: 244 | dependency: transitive 245 | description: 246 | name: process 247 | url: "https://pub.flutter-io.cn" 248 | source: hosted 249 | version: "3.0.13" 250 | provider: 251 | dependency: "direct main" 252 | description: 253 | name: provider 254 | url: "https://pub.flutter-io.cn" 255 | source: hosted 256 | version: "4.3.2+2" 257 | recase: 258 | dependency: transitive 259 | description: 260 | name: recase 261 | url: "https://pub.flutter-io.cn" 262 | source: hosted 263 | version: "2.0.1" 264 | sky_engine: 265 | dependency: transitive 266 | description: flutter 267 | source: sdk 268 | version: "0.0.99" 269 | source_span: 270 | dependency: transitive 271 | description: 272 | name: source_span 273 | url: "https://pub.flutter-io.cn" 274 | source: hosted 275 | version: "1.8.0-nullsafety.2" 276 | sqflite: 277 | dependency: "direct main" 278 | description: 279 | name: sqflite 280 | url: "https://pub.flutter-io.cn" 281 | source: hosted 282 | version: "1.3.0" 283 | sqflite_common: 284 | dependency: transitive 285 | description: 286 | name: sqflite_common 287 | url: "https://pub.flutter-io.cn" 288 | source: hosted 289 | version: "1.0.0+1" 290 | stack_trace: 291 | dependency: transitive 292 | description: 293 | name: stack_trace 294 | url: "https://pub.flutter-io.cn" 295 | source: hosted 296 | version: "1.10.0-nullsafety.1" 297 | stream_channel: 298 | dependency: transitive 299 | description: 300 | name: stream_channel 301 | url: "https://pub.flutter-io.cn" 302 | source: hosted 303 | version: "2.1.0-nullsafety.1" 304 | string_scanner: 305 | dependency: transitive 306 | description: 307 | name: string_scanner 308 | url: "https://pub.flutter-io.cn" 309 | source: hosted 310 | version: "1.1.0-nullsafety.1" 311 | synchronized: 312 | dependency: transitive 313 | description: 314 | name: synchronized 315 | url: "https://pub.flutter-io.cn" 316 | source: hosted 317 | version: "2.2.0" 318 | term_glyph: 319 | dependency: transitive 320 | description: 321 | name: term_glyph 322 | url: "https://pub.flutter-io.cn" 323 | source: hosted 324 | version: "1.2.0-nullsafety.1" 325 | test_api: 326 | dependency: transitive 327 | description: 328 | name: test_api 329 | url: "https://pub.flutter-io.cn" 330 | source: hosted 331 | version: "0.2.19-nullsafety.2" 332 | typed_data: 333 | dependency: transitive 334 | description: 335 | name: typed_data 336 | url: "https://pub.flutter-io.cn" 337 | source: hosted 338 | version: "1.3.0-nullsafety.3" 339 | uuid: 340 | dependency: transitive 341 | description: 342 | name: uuid 343 | url: "https://pub.flutter-io.cn" 344 | source: hosted 345 | version: "2.2.2" 346 | vector_math: 347 | dependency: transitive 348 | description: 349 | name: vector_math 350 | url: "https://pub.flutter-io.cn" 351 | source: hosted 352 | version: "2.1.0-nullsafety.3" 353 | video_player: 354 | dependency: "direct main" 355 | description: 356 | name: video_player 357 | url: "https://pub.flutter-io.cn" 358 | source: hosted 359 | version: "0.10.8+1" 360 | video_player_platform_interface: 361 | dependency: transitive 362 | description: 363 | name: video_player_platform_interface 364 | url: "https://pub.flutter-io.cn" 365 | source: hosted 366 | version: "1.0.5" 367 | video_player_web: 368 | dependency: transitive 369 | description: 370 | name: video_player_web 371 | url: "https://pub.flutter-io.cn" 372 | source: hosted 373 | version: "0.1.2+1" 374 | webview_flutter: 375 | dependency: "direct main" 376 | description: 377 | name: webview_flutter 378 | url: "https://pub.flutter-io.cn" 379 | source: hosted 380 | version: "0.3.19+9" 381 | xdg_directories: 382 | dependency: transitive 383 | description: 384 | name: xdg_directories 385 | url: "https://pub.flutter-io.cn" 386 | source: hosted 387 | version: "0.1.0" 388 | sdks: 389 | dart: ">=2.10.1 <2.11.0" 390 | flutter: ">=1.16.0 <2.0.0" 391 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_app 2 | description: A new Flutter application. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ^2.10.1 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | image_picker: 24 | 0.6.1 25 | provider: 26 | ^4.3.2+2 27 | permission_handler: ^5.0.1 28 | 29 | flutter_sound: 30 | ^6.0.1 31 | 32 | video_player: 33 | ^0.10.5 34 | http: 35 | ^0.11.3+16 36 | palette_generator: 37 | 0.2.3 38 | path_provider: '1.6.11' 39 | 40 | webview_flutter: 41 | ^0.3.19 42 | 43 | observable_ui: 44 | path: observable_ui 45 | 46 | sqflite: ^1.1.6 47 | 48 | 49 | 50 | 51 | # The following adds the Cupertino Icons font to your application. 52 | # Use with the CupertinoIcons class for iOS style icons. 53 | cupertino_icons: ^0.1.2 54 | 55 | dev_dependencies: 56 | flutter_test: 57 | sdk: flutter 58 | 59 | 60 | # For information on the generic Dart part of this file, see the 61 | # following page: https://dart.dev/tools/pub/pubspec 62 | 63 | # The following section is specific to Flutter. 64 | flutter: 65 | 66 | # The following line ensures that the Material Icons font is 67 | # included with your application, so that you can use the icons in 68 | # the material Icons class. 69 | uses-material-design: true 70 | 71 | # To add assets to your application, add an assets section, like this: 72 | # assets: 73 | # - images/a_dot_burr.jpeg 74 | # - images/a_dot_ham.jpeg 75 | 76 | # An image asset can refer to one or more resolution-specific "variants", see 77 | # https://flutter.dev/assets-and-images/#resolution-aware. 78 | 79 | # For details regarding adding assets from package dependencies, see 80 | # https://flutter.dev/assets-and-images/#from-packages 81 | 82 | # To add custom fonts to your application, add a fonts section here, 83 | # in this "flutter" section. Each entry in this list should have a 84 | # "family" key with the font family name, and a "fonts" key with a 85 | # list giving the asset and other descriptors for the font. For 86 | # example: 87 | # fonts: 88 | # - family: Schyler 89 | # fonts: 90 | # - asset: fonts/Schyler-Regular.ttf 91 | # - asset: fonts/Schyler-Italic.ttf 92 | # style: italic 93 | # - family: Trajan Pro 94 | # fonts: 95 | # - asset: fonts/TrajanPro.ttf 96 | # - asset: fonts/TrajanPro_Bold.ttf 97 | # weight: 700 98 | # 99 | # For details regarding fonts from package dependencies, 100 | # see https://flutter.dev/custom-fonts/#from-packages 101 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | void main() {} 9 | --------------------------------------------------------------------------------