├── .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 | 
7 |
8 | ## 微信消息列表界面
9 |
10 | 
11 |
12 | ## 微信音乐播放界面
13 |
14 | 
15 |
16 | ## 微信聊天界面
17 |
18 | 
19 |
20 | ## 微信朋友圈界面
21 |
22 | 
23 |
24 | ## 抖音详情页面
25 |
26 | 
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 |
--------------------------------------------------------------------------------