├── .gitignore
├── README.md
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── doubanmovieflutter
│ │ │ └── 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
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── key.properties
└── settings.gradle
├── doc
└── TOC.md
├── images
├── flutter_logo.png
├── icon_account_normal.png
├── icon_account_selected.png
├── icon_api.png
├── icon_blog.png
├── icon_explore_normal.png
├── icon_explore_selected.png
├── icon_github.png
├── icon_hot_normal.png
├── icon_hot_selected.png
├── icon_more_normal.png
├── icon_more_selected.png
├── icon_qq.png
├── icon_ranking_down.png
├── icon_ranking_up.png
├── icon_upcoming_normal.png
├── icon_upcoming_selected.png
└── icon_wx.png
├── ios
├── .gitignore
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ └── Release.xcconfig
├── Podfile
├── Podfile.lock
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
└── Runner
│ ├── AppDelegate.swift
│ ├── 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
│ ├── ImagePalette
│ ├── ColorCutQuantizer.swift
│ ├── ColorHistogram.swift
│ ├── DefaultPaletteGenerator.swift
│ ├── HSLColor.swift
│ ├── HexColor.swift
│ ├── Palette.swift
│ ├── PaletteConfiguration.swift
│ ├── PaletteGenerator.swift
│ ├── PaletteSwatch.swift
│ ├── RGBColor.swift
│ ├── SwiftPriorityQueue.swift
│ └── UIImage+PaletteExtension.swift
│ ├── Info.plist
│ ├── Runner-Bridging-Header.h
│ └── SwiftPalettePlugin.swift
├── lib
├── CustomView.dart
├── NestScrollSample.dart
├── main.dart
├── model
│ ├── MovieActor.dart
│ ├── MovieActor.g.dart
│ ├── MovieDetail.dart
│ ├── MovieDetail.g.dart
│ ├── MovieImgs.dart
│ ├── MovieImgs.g.dart
│ ├── MovieIntro.dart
│ ├── MovieIntro.g.dart
│ ├── MovieIntroList.dart
│ ├── MovieIntroList.g.dart
│ ├── MovieRate.dart
│ └── MovieRate.g.dart
├── net
│ ├── DateSource.dart
│ └── api.dart
├── pages
│ ├── explore_page.dart
│ ├── hotlist_page.dart
│ ├── more_page.dart
│ ├── movie_detail_page.dart
│ └── upcoming_page.dart
├── palette.dart
└── utils
│ └── NetUtils.dart
├── pubspec.yaml
└── screenshots
├── IMG_0309.PNG
├── IMG_0310.PNG
├── IMG_0311.PNG
├── IMG_0312.PNG
├── IMG_0313.PNG
├── IMG_0319.PNG
└── IMG_0320.PNG
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .dart_tool/
3 | .idea/
4 | .packages
5 | .pub/
6 |
7 | build/
8 |
9 | .flutter-plugins
10 | .gitattributes
11 | .metadata
12 |
13 | doubanmovie_flutter.iml
14 | doubanmovie_flutter_android.iml
15 | pubspec.lock
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 使用豆瓣 Api 实现了一个精美的 Flutter 版豆瓣电影客户端,细节很完善,更像一个经过完整设计的产品而非 Demo,期间踩了不少坑,文档和功能会持续更新,文档包括从最开始环境安装到最后打包App的过程中涉及的知识点以及一些个人思考。
2 |
3 | [:blue_book:文档入口](https://github.com/zcoderr/Flutter_Douban/blob/master/doc/TOC.md)
4 |
5 | > 下载方式:
6 |
7 | **Android:**
8 | [APK 下载地址](https://fir.im/douba)
9 |
10 | 或 :
11 |
12 | 扫描二维码下载
13 |
14 |
15 |
16 | **iOS:**
17 | 暂无
18 |
19 |
20 |
21 | #### To-Do List:
22 |
23 | - [ ] 添加影人详情页面
24 | - [ ] 在电影详情页面添加热门评论
25 | - [ ] 电影详情页里添加电影海报模块
26 | - [ ] 添加评论详情页面
27 |
28 |
29 |
30 | > App 截图
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.class
3 | .gradle
4 | /local.properties
5 | /.idea/workspace.xml
6 | /.idea/libraries
7 | .DS_Store
8 | /build
9 | /captures
10 | GeneratedPluginRegistrant.java
11 |
--------------------------------------------------------------------------------
/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 | throw new GradleException("versionCode not found. Define flutter.versionCode in the local.properties file.")
17 | }
18 |
19 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
20 | if (flutterVersionName == null) {
21 | throw new GradleException("versionName not found. Define flutter.versionName in the local.properties file.")
22 | }
23 |
24 | apply plugin: 'com.android.application'
25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
26 |
27 | def keystorePropertiesFile = rootProject.file("key.properties")
28 | def keystoreProperties = new Properties()
29 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
30 |
31 | android {
32 | compileSdkVersion 28
33 |
34 | lintOptions {
35 | disable 'InvalidPackage'
36 | }
37 |
38 | defaultConfig {
39 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
40 | applicationId "com.example.doubanmovieflutter"
41 | minSdkVersion 16
42 | targetSdkVersion 28
43 | versionCode flutterVersionCode.toInteger()
44 | versionName flutterVersionName
45 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
46 | }
47 |
48 | signingConfigs {
49 | release {
50 | keyAlias keystoreProperties['keyAlias']
51 | keyPassword keystoreProperties['keyPassword']
52 | storeFile file(keystoreProperties['storeFile'])
53 | storePassword keystoreProperties['storePassword']
54 | }
55 | }
56 | buildTypes {
57 | release {
58 | signingConfig signingConfigs.release
59 | }
60 | }
61 |
62 | }
63 |
64 | flutter {
65 | source '../..'
66 | }
67 |
68 | dependencies {
69 | testImplementation 'junit:junit:4.12'
70 | androidTestImplementation 'androidx.test:runner:1.1.0'
71 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
72 | implementation 'androidx.palette:palette:1.0.0'
73 |
74 | // https://mvnrepository.com/artifact/com.github.bumptech.glide/glide
75 | implementation group: 'com.github.bumptech.glide', name: 'glide', version: '4.7.1'
76 | }
77 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
8 |
9 |
10 |
15 |
19 |
26 |
30 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/example/doubanmovieflutter/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.doubanmovieflutter;
2 |
3 | import android.os.Bundle;
4 | import io.flutter.app.FlutterActivity;
5 | import io.flutter.plugin.common.MethodCall;
6 | import io.flutter.plugin.common.MethodChannel;
7 | import io.flutter.plugins.GeneratedPluginRegistrant;
8 |
9 | public class MainActivity extends FlutterActivity {
10 | @Override
11 | protected void onCreate(Bundle savedInstanceState) {
12 | super.onCreate(savedInstanceState);
13 | GeneratedPluginRegistrant.registerWith(this);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | jcenter()
5 | }
6 |
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:3.4.1'
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 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jun 20 14:39:22 CST 2019
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.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/android/key.properties:
--------------------------------------------------------------------------------
1 | storePassword=7311531
2 | keyPassword=7311531
3 | keyAlias=key
4 | storeFile=/Users/zachary/code/flutter/sign/key.jks
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/doc/TOC.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | > 涉及知识点目录
4 |
5 | ###### 1.环境安装以及 Flutter 常用命令:
6 |
7 | ###### 2. UI 部分:
8 |
9 | - 基础控件用法
10 | - 复杂布局
11 | - "首页"中的卡片
12 | - "更多"页中的水平三等分布局
13 | - 嵌套滚动布局
14 | - "发现"页中多个水平和垂直滚动布局嵌套
15 | - 路由跳转
16 | - 自定义绘制控件
17 | - "五角星展示电影评分" 控件
18 |
19 | ###### 3.数据部分:
20 |
21 | - 网络请求
22 | - 实体类的编写和自动生成解析代码
23 | - 客户端中用爬虫思路爬取需要的数据
24 |
25 | ###### 4.Flutter 插件部分:
26 |
27 | - 在 Android Studio 和 Xcode 中集成原生的第三方库
28 | - 使用 Java/Kotlin 和 objective-c/Swift 编写桥接类
29 |
30 | ###### 5.打包部分:
31 |
32 | - Android 打包配置
33 | - ios 打包配置
34 |
35 | ------
36 |
37 | 持续更新中 >>>
38 |
39 |
--------------------------------------------------------------------------------
/images/flutter_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/flutter_logo.png
--------------------------------------------------------------------------------
/images/icon_account_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_account_normal.png
--------------------------------------------------------------------------------
/images/icon_account_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_account_selected.png
--------------------------------------------------------------------------------
/images/icon_api.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_api.png
--------------------------------------------------------------------------------
/images/icon_blog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_blog.png
--------------------------------------------------------------------------------
/images/icon_explore_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_explore_normal.png
--------------------------------------------------------------------------------
/images/icon_explore_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_explore_selected.png
--------------------------------------------------------------------------------
/images/icon_github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_github.png
--------------------------------------------------------------------------------
/images/icon_hot_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_hot_normal.png
--------------------------------------------------------------------------------
/images/icon_hot_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_hot_selected.png
--------------------------------------------------------------------------------
/images/icon_more_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_more_normal.png
--------------------------------------------------------------------------------
/images/icon_more_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_more_selected.png
--------------------------------------------------------------------------------
/images/icon_qq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_qq.png
--------------------------------------------------------------------------------
/images/icon_ranking_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_ranking_down.png
--------------------------------------------------------------------------------
/images/icon_ranking_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_ranking_up.png
--------------------------------------------------------------------------------
/images/icon_upcoming_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_upcoming_normal.png
--------------------------------------------------------------------------------
/images/icon_upcoming_selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_upcoming_selected.png
--------------------------------------------------------------------------------
/images/icon_wx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/images/icon_wx.png
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vagrant/
3 | .sconsign.dblite
4 | .svn/
5 |
6 | .DS_Store
7 | *.swp
8 | profile
9 |
10 | DerivedData/
11 | build/
12 | GeneratedPluginRegistrant.h
13 | GeneratedPluginRegistrant.m
14 |
15 | .generated/
16 |
17 | *.pbxuser
18 | *.mode1v3
19 | *.mode2v3
20 | *.perspectivev3
21 |
22 | !default.pbxuser
23 | !default.mode1v3
24 | !default.mode2v3
25 | !default.perspectivev3
26 |
27 | xcuserdata
28 |
29 | *.moved-aside
30 |
31 | *.pyc
32 | *sync/
33 | Icon?
34 | .tags*
35 |
36 | /Flutter/app.flx
37 | /Flutter/app.zip
38 | /Flutter/flutter_assets/
39 | /Flutter/App.framework
40 | /Flutter/Flutter.framework
41 | /Flutter/Generated.xcconfig
42 | /ServiceDefinitions.json
43 |
44 | Pods/
45 | .symlinks/
46 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
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 "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | def parse_KV_file(file, separator='=')
8 | file_abs_path = File.expand_path(file)
9 | if !File.exists? file_abs_path
10 | return [];
11 | end
12 | pods_ary = []
13 | skip_line_start_symbols = ["#", "/"]
14 | File.foreach(file_abs_path) { |line|
15 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
16 | plugin = line.split(pattern=separator)
17 | if plugin.length == 2
18 | podname = plugin[0].strip()
19 | path = plugin[1].strip()
20 | podpath = File.expand_path("#{path}", file_abs_path)
21 | pods_ary.push({:name => podname, :path => podpath});
22 | else
23 | puts "Invalid plugin specification: #{line}"
24 | end
25 | }
26 | return pods_ary
27 | end
28 |
29 | target 'Runner' do
30 | use_frameworks!
31 |
32 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
33 | # referring to absolute paths on developers' machines.
34 | system('rm -rf .symlinks')
35 | system('mkdir -p .symlinks/plugins')
36 |
37 | # Flutter Pods
38 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig')
39 | if generated_xcode_build_settings.empty?
40 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first."
41 | end
42 | generated_xcode_build_settings.map { |p|
43 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR'
44 | symlink = File.join('.symlinks', 'flutter')
45 | File.symlink(File.dirname(p[:path]), symlink)
46 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path]))
47 | end
48 | }
49 |
50 | # Plugin Pods
51 | plugin_pods = parse_KV_file('../.flutter-plugins')
52 | plugin_pods.map { |p|
53 | symlink = File.join('.symlinks', 'plugins', p[:name])
54 | File.symlink(p[:path], symlink)
55 | pod p[:name], :path => File.join(symlink, 'ios')
56 | }
57 | end
58 |
59 | post_install do |installer|
60 | installer.pods_project.targets.each do |target|
61 | target.build_configurations.each do |config|
62 | config.build_settings['ENABLE_BITCODE'] = 'NO'
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/ios/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - Flutter (1.0.0)
3 | - flutter_webview_plugin (0.0.1):
4 | - Flutter
5 | - image_picker (0.0.1):
6 | - Flutter
7 | - shared_preferences (0.0.1):
8 | - Flutter
9 |
10 | DEPENDENCIES:
11 | - Flutter (from `.symlinks/flutter/ios`)
12 | - flutter_webview_plugin (from `.symlinks/plugins/flutter_webview_plugin/ios`)
13 | - image_picker (from `.symlinks/plugins/image_picker/ios`)
14 | - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`)
15 |
16 | EXTERNAL SOURCES:
17 | Flutter:
18 | :path: ".symlinks/flutter/ios"
19 | flutter_webview_plugin:
20 | :path: ".symlinks/plugins/flutter_webview_plugin/ios"
21 | image_picker:
22 | :path: ".symlinks/plugins/image_picker/ios"
23 | shared_preferences:
24 | :path: ".symlinks/plugins/shared_preferences/ios"
25 |
26 | SPEC CHECKSUMS:
27 | Flutter: 9d0fac939486c9aba2809b7982dfdbb47a7b0296
28 | flutter_webview_plugin: 0b491d31c34ab5c86a71c9f1a57ac000dd1b75e9
29 | image_picker: ee00aab0487cedc80a304085219503cc6d0f2e22
30 | shared_preferences: 5a1d487c427ee18fcd3ea1f2a131569481834b53
31 |
32 | PODFILE CHECKSUM: 7765ea4305eaab0b3dfd384c7de11902aa3195fd
33 |
34 | COCOAPODS: 1.5.2
35 |
--------------------------------------------------------------------------------
/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 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildSystemType
6 | Original
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | SwiftPalettePlugin.register(with:registrar(forPlugin: "channel:com.postmuseapp.designer/palette"))
12 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zcoderr/Flutter_Douban/e7d3d595292bca5197376aa42509cb35d8c136eb/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/ImagePalette/ColorCutQuantizer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorCutQuantizer.swift
3 | // ImagePalette
4 | //
5 | // Original created by Google/Android
6 | // Ported to Swift/iOS by Shaun Harrison
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | private let COMPONENT_RED = -3
13 | private let COMPONENT_GREEN = -2
14 | private let COMPONENT_BLUE = -1
15 |
16 | private let BLACK_MAX_LIGHTNESS = CGFloat(0.05)
17 | private let WHITE_MIN_LIGHTNESS = CGFloat(0.95)
18 |
19 | private typealias VboxPriorityQueue = PriorityQueue
20 |
21 | internal final class ColorCutQuantizer {
22 |
23 | fileprivate var colors = [Int64]()
24 | fileprivate var colorPopulations = [Int64: Int64]()
25 |
26 | /** list of quantized colors */
27 | private(set) var quantizedColors = [PaletteSwatch]()
28 |
29 | /**
30 | Factory-method to generate a ColorCutQuantizer from a UIImage.
31 |
32 | :param: image Image to extract the pixel data from
33 | :param: maxColors The maximum number of colors that should be in the result palette.
34 | */
35 | internal static func from(image: UIImage, maxColors: Int) -> ColorCutQuantizer {
36 | let pixels = image.pixels
37 | return ColorCutQuantizer(colorHistogram: ColorHistogram(pixels: pixels), maxColors: maxColors)
38 | }
39 |
40 | /**
41 | :param: colorHistogram histogram representing an image’s pixel data
42 | :param maxColors The maximum number of colors that should be in the result palette.
43 | */
44 | private init(colorHistogram: ColorHistogram, maxColors: Int) {
45 | let rawColorCount = colorHistogram.numberOfColors
46 | let rawColors = colorHistogram.colors
47 | let rawColorCounts = colorHistogram.colorCounts
48 |
49 | // First, lets pack the populations into a SparseIntArray so that they can be easily
50 | // retrieved without knowing a color’s index
51 | self.colorPopulations = Dictionary(minimumCapacity: rawColorCount)
52 | for i in 0 ..< rawColors.count {
53 | self.colorPopulations[rawColors[i]] = rawColorCounts[i]
54 | }
55 |
56 | // Now go through all of the colors and keep those which we do not want to ignore
57 | var validColorCount = 0
58 | self.colors = [ ]
59 | self.colors.reserveCapacity(rawColorCount)
60 |
61 | for color in rawColors {
62 | guard !self.shouldIgnore(color: color) else {
63 | continue
64 | }
65 |
66 | self.colors.append(color)
67 | validColorCount += 1
68 | }
69 |
70 | if validColorCount <= maxColors {
71 | // The image has fewer colors than the maximum requested, so just return the colors
72 | self.quantizedColors = [ ]
73 | for color in self.colors {
74 | guard let populations = self.colorPopulations[color] else {
75 | continue
76 | }
77 |
78 | self.quantizedColors.append(PaletteSwatch(color: HexColor.toUIColor(color), population: populations))
79 | }
80 | } else {
81 | // We need use quantization to reduce the number of colors
82 | self.quantizedColors = self.quantizePixels(maxColorIndex: validColorCount - 1, maxColors: maxColors)
83 | }
84 | }
85 |
86 | private func quantizePixels(maxColorIndex: Int, maxColors: Int) -> [PaletteSwatch] {
87 | // Create the priority queue which is sorted by volume descending. This means we always
88 | // split the largest box in the queue
89 | var pq = PriorityQueue(ascending: false, startingValues: Array())
90 |
91 | // final PriorityQueue pq = PriorityQueue(maxColors, VBOX_COMPARATOR_VOLUME)
92 |
93 | // To start, offer a box which contains all of the colors
94 | pq.push(Vbox(quantizer: self, lowerIndex: 0, upperIndex: maxColorIndex))
95 |
96 | // Now go through the boxes, splitting them until we have reached maxColors or there are no
97 | // more boxes to split
98 | self.splitBoxes(queue: &pq, maxSize: maxColors)
99 |
100 | // Finally, return the average colors of the color boxes
101 | return generateAverageColors(vboxes: pq)
102 | }
103 |
104 | /**
105 | Iterate through the Queue, popping Vbox objects from the queue and splitting them. Once
106 | split, the new box and the remaining box are offered back to the queue.
107 |
108 | :param: queue Priority queue to poll for boxes
109 | :param: maxSize Maximum amount of boxes to split
110 | */
111 | private func splitBoxes(queue: inout VboxPriorityQueue, maxSize: Int) {
112 | while queue.count < maxSize {
113 | guard let vbox = queue.pop(), vbox.canSplit else {
114 | // If we get here then there are no more boxes to split, so return
115 | return
116 | }
117 |
118 | // First split the box, and offer the result
119 | queue.push(vbox.splitBox())
120 | // Then offer the box back
121 | queue.push(vbox)
122 | }
123 | }
124 |
125 | private func generateAverageColors(vboxes: VboxPriorityQueue) -> [PaletteSwatch] {
126 | var colors = [PaletteSwatch]()
127 |
128 | for vbox in vboxes {
129 | let color = vbox.averageColor
130 |
131 | guard !type(of: self).shouldIgnore(color: color) else {
132 | continue
133 | }
134 |
135 | // As we’re averaging a color box, we can still get colors which we do not want, so
136 | // we check again here
137 | colors.append(color)
138 | }
139 |
140 | return colors
141 | }
142 |
143 | /**
144 | Modify the significant octet in a packed color int. Allows sorting based on the value of a
145 | single color component.
146 | */
147 | private func modifySignificantOctet(dimension: Int, lowerIndex: Int, upperIndex: Int) {
148 | switch (dimension) {
149 | case COMPONENT_RED:
150 | // Already in RGB, no need to do anything
151 | break
152 |
153 | case COMPONENT_GREEN:
154 | // We need to do a RGB to GRB swap, or vice-versa
155 | for i in lowerIndex ... upperIndex {
156 | let color = self.colors[i]
157 | self.colors[i] = HexColor.fromRGB((color >> 8) & 0xFF, green: (color >> 16) & 0xFF, blue: color & 0xFF)
158 | }
159 | case COMPONENT_BLUE:
160 | // We need to do a RGB to BGR swap, or vice-versa
161 | for i in lowerIndex ... upperIndex {
162 | let color = self.colors[i]
163 | self.colors[i] = HexColor.fromRGB(color & 0xFF, green: (color >> 8) & 0xFF, blue: (color >> 16) & 0xFF)
164 | }
165 | default:
166 | break
167 | }
168 | }
169 |
170 | private func shouldIgnore(color: Int64) -> Bool {
171 | return HexColor.toHSL(color).shouldIgnore
172 | }
173 |
174 | private static func shouldIgnore(color: PaletteSwatch) -> Bool {
175 | return color.hsl.shouldIgnore
176 | }
177 |
178 | }
179 |
180 | extension HSLColor {
181 |
182 | fileprivate var shouldIgnore: Bool {
183 | return self.isWhite || self.isBlack || self.isNearRedILine
184 | }
185 |
186 | /**
187 | :return: true if the color represents a color which is close to black.
188 | */
189 | fileprivate var isBlack: Bool {
190 | return self.lightness <= BLACK_MAX_LIGHTNESS
191 | }
192 |
193 | /**
194 | :return: true if the color represents a color which is close to white.
195 | */
196 | fileprivate var isWhite: Bool {
197 | return self.lightness >= WHITE_MIN_LIGHTNESS
198 | }
199 |
200 | /**
201 | :return: true if the color lies close to the red side of the I line.
202 | */
203 | fileprivate var isNearRedILine: Bool {
204 | return self.hue >= 10.0 && self.hue <= 37.0 && self.saturation <= 0.82
205 | }
206 |
207 | }
208 |
209 | /** Represents a tightly fitting box around a color space. */
210 | private class Vbox: Hashable {
211 | // lower and upper index are inclusive
212 | private let lowerIndex: Int
213 | private var upperIndex: Int
214 |
215 | private var minRed = Int64(2)
216 | private var maxRed = Int64(2)
217 |
218 | private var minGreen = Int64(0)
219 | private var maxGreen = Int64(0)
220 |
221 | private var minBlue = Int64(2)
222 | private var maxBlue = Int64(2)
223 |
224 | private let quantizer: ColorCutQuantizer
225 |
226 | private static var ordinal = Int32(0)
227 |
228 | let hashValue = Int(OSAtomicIncrement32(&Vbox.ordinal))
229 |
230 | init(quantizer: ColorCutQuantizer, lowerIndex: Int, upperIndex: Int) {
231 | self.quantizer = quantizer
232 |
233 | self.lowerIndex = lowerIndex
234 | self.upperIndex = upperIndex
235 | assert(self.lowerIndex <= self.upperIndex, "lowerIndex (\(self.lowerIndex)) can’t be > upperIndex (\(self.upperIndex))")
236 | self.fitBox()
237 | }
238 |
239 | var volume: Int64 {
240 | let red = Double((self.maxRed - self.minRed) + 1)
241 | let green = Double((self.maxGreen - self.minGreen) + 1)
242 | let blue = Double((self.maxBlue - self.minBlue) + 1)
243 |
244 | return Int64(red * green * blue)
245 | }
246 |
247 | var canSplit: Bool {
248 | return self.colorCount > 1
249 | }
250 |
251 | var colorCount: Int {
252 | return self.upperIndex - self.lowerIndex + 1
253 | }
254 |
255 | /** Recomputes the boundaries of this box to tightly fit the colors within the box. */
256 | func fitBox() {
257 | // Reset the min and max to opposite values
258 | self.minRed = 0xFF
259 | self.minGreen = 0xFF
260 | self.minBlue = 0xFF
261 |
262 | self.maxRed = 0x0
263 | self.maxGreen = 0x0
264 | self.maxBlue = 0x0
265 |
266 | for i in self.lowerIndex ... self.upperIndex {
267 | let color = HexColor.toRGB(self.quantizer.colors[i])
268 |
269 | self.maxRed = max(self.maxRed, color.red)
270 | self.minRed = min(self.minRed, color.red)
271 |
272 | self.maxGreen = max(self.maxGreen, color.green)
273 | self.minGreen = min(self.minGreen, color.green)
274 |
275 | self.maxBlue = max(self.maxBlue, color.blue)
276 | self.minBlue = min(self.minBlue, color.blue)
277 | }
278 | }
279 |
280 | /**
281 | Split this color box at the mid-point along it’s longest dimension
282 |
283 | :return: the new ColorBox
284 | */
285 | func splitBox() -> Vbox {
286 | guard self.canSplit else {
287 | fatalError("Can not split a box with only 1 color")
288 | }
289 |
290 | // find median along the longest dimension
291 | let splitPoint = self.findSplitPoint()
292 |
293 | assert(splitPoint + 1 <= self.upperIndex, "splitPoint (\(splitPoint + 1)) can’t be > upperIndex (\(self.upperIndex)), lowerIndex: \(self.lowerIndex)")
294 |
295 | let newBox = Vbox(quantizer: self.quantizer, lowerIndex: splitPoint + 1, upperIndex: self.upperIndex)
296 |
297 | // Now change this box’s upperIndex and recompute the color boundaries
298 | self.upperIndex = splitPoint
299 | assert(self.lowerIndex <= self.upperIndex, "lowerIndex (\(self.lowerIndex)) can’t be > upperIndex (\(self.upperIndex))")
300 | self.fitBox()
301 |
302 | return newBox
303 | }
304 |
305 | /**
306 | :return: the dimension which this box is largest in
307 | */
308 | var longestColorDimension: Int {
309 | let redLength = self.maxRed - self.minRed
310 | let greenLength = self.maxGreen - self.minGreen
311 | let blueLength = self.maxBlue - self.minBlue
312 |
313 | if redLength >= greenLength && redLength >= blueLength {
314 | return COMPONENT_RED
315 | } else if greenLength >= redLength && greenLength >= blueLength {
316 | return COMPONENT_GREEN
317 | } else {
318 | return COMPONENT_BLUE
319 | }
320 | }
321 |
322 | /**
323 | Finds the point within this box’s lowerIndex and upperIndex index of where to split.
324 |
325 | This is calculated by finding the longest color dimension, and then sorting the
326 | sub-array based on that dimension value in each color. The colors are then iterated over
327 | until a color is found with at least the midpoint of the whole box’s dimension midpoint.
328 |
329 | :return: the index of the colors array to split from
330 | */
331 | func findSplitPoint() -> Int {
332 | let longestDimension = self.longestColorDimension
333 |
334 | // Sort the colors in this box based on the longest color dimension.
335 |
336 | var sorted = self.quantizer.colors[self.lowerIndex...self.upperIndex]
337 | sorted.sort()
338 |
339 | if longestDimension == COMPONENT_RED {
340 | sorted.sort() { HexColor.red($0) < HexColor.red($1) }
341 | } else if longestDimension == COMPONENT_GREEN {
342 | sorted.sort() { HexColor.green($0) < HexColor.green($1) }
343 | } else {
344 | sorted.sort() { HexColor.blue($0) < HexColor.blue($1) }
345 | }
346 |
347 | self.quantizer.colors[self.lowerIndex...self.upperIndex] = sorted
348 |
349 | let dimensionMidPoint = self.midPoint(longestDimension)
350 |
351 | for i in self.lowerIndex ..< self.upperIndex {
352 | let color = self.quantizer.colors[i]
353 |
354 | switch (longestDimension) {
355 | case COMPONENT_RED where HexColor.red(color) >= dimensionMidPoint:
356 | return i
357 | case COMPONENT_GREEN where HexColor.green(color) >= dimensionMidPoint:
358 | return i
359 | case COMPONENT_BLUE where HexColor.blue(color) > dimensionMidPoint:
360 | return i
361 | default:
362 | continue
363 | }
364 | }
365 |
366 | return self.lowerIndex
367 | }
368 |
369 | /**
370 | * @return the average color of this box.
371 | */
372 | var averageColor: PaletteSwatch {
373 | var redSum = Int64(0)
374 | var greenSum = Int64(0)
375 | var blueSum = Int64(0)
376 | var totalPopulation = Int64(0)
377 |
378 | for i in self.lowerIndex ... self.upperIndex {
379 | let color = self.quantizer.colors[i]
380 |
381 | guard let colorPopulation = self.quantizer.colorPopulations[color] else {
382 | continue
383 | }
384 |
385 | totalPopulation += colorPopulation
386 | redSum += colorPopulation * HexColor.red(color)
387 | greenSum += colorPopulation * HexColor.green(color)
388 | blueSum += colorPopulation * HexColor.blue(color)
389 | }
390 |
391 | let redAverage = Int64(round(Double(redSum) / Double(totalPopulation)))
392 | let greenAverage = Int64(round(Double(greenSum) / Double(totalPopulation)))
393 | let blueAverage = Int64(round(Double(blueSum) / Double(totalPopulation)))
394 |
395 | return PaletteSwatch(rgbColor: RGBColor(red: redAverage, green: greenAverage, blue: blueAverage, alpha: 255), population: totalPopulation)
396 | }
397 |
398 | /**
399 | * @return the midpoint of this box in the given {@code dimension}
400 | */
401 | func midPoint(_ dimension: Int) -> Int64 {
402 | switch (dimension) {
403 | case COMPONENT_GREEN:
404 | return (self.minGreen + self.maxGreen) / Int64(2)
405 | case COMPONENT_BLUE:
406 | return (self.minBlue + self.maxBlue) / Int64(2)
407 | case COMPONENT_RED:
408 | return (self.minRed + self.maxRed) / Int64(2)
409 | default:
410 | return (self.minRed + self.maxRed) / Int64(2)
411 | }
412 | }
413 | }
414 |
415 | extension Vbox: Comparable { }
416 |
417 | private func ==(lhs: Vbox, rhs: Vbox) -> Bool {
418 | return lhs.hashValue == rhs.hashValue
419 | }
420 |
421 | private func <=(lhs: Vbox, rhs: Vbox) -> Bool {
422 | return lhs.volume <= rhs.volume
423 | }
424 |
425 | private func >=(lhs: Vbox, rhs: Vbox) -> Bool {
426 | return lhs.volume >= rhs.volume
427 | }
428 |
429 | private func <(lhs: Vbox, rhs: Vbox) -> Bool {
430 | return lhs.volume < rhs.volume
431 | }
432 |
433 | private func >(lhs: Vbox, rhs: Vbox) -> Bool {
434 | return lhs.volume > rhs.volume
435 | }
436 |
--------------------------------------------------------------------------------
/ios/Runner/ImagePalette/ColorHistogram.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorHistogram.swift
3 | // ImagePalette
4 | //
5 | // Original created by Google/Android
6 | // Ported to Swift/iOS by Shaun Harrison
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | /**
13 | Class which provides a histogram for RGB values.
14 | */
15 | public struct ColorHistogram {
16 |
17 | /**
18 | An array containing all of the distinct colors in the image.
19 | */
20 | private(set) public var colors = [Int64]()
21 |
22 | /**
23 | An array containing the frequency of a distinct colors within the image.
24 | */
25 | private(set) public var colorCounts = [Int64]()
26 |
27 | /**
28 | Number of distinct colors in the image.
29 | */
30 | private(set) public var numberOfColors: Int
31 |
32 | /**
33 | A new ColorHistogram instance.
34 |
35 | :param: Pixels array of image contents
36 | */
37 | public init(pixels: [Int64]) {
38 | // Sort the pixels to enable counting below
39 | var pixels = pixels
40 | pixels.sort()
41 |
42 | // Count number of distinct colors
43 | self.numberOfColors = type(of: self).countDistinctColors(pixels)
44 |
45 | // Finally count the frequency of each color
46 | self.countFrequencies(pixels)
47 | }
48 |
49 | private static func countDistinctColors(_ pixels: [Int64]) -> Int {
50 | if pixels.count < 2 {
51 | // If we have less than 2 pixels we can stop here
52 | return pixels.count
53 | }
54 |
55 | // If we have at least 2 pixels, we have a minimum of 1 color...
56 | var colorCount = 1
57 | var currentColor = pixels[0]
58 |
59 | // Now iterate from the second pixel to the end, counting distinct colors
60 | for pixel in pixels {
61 | // If we encounter a new color, increase the population
62 | if pixel != currentColor {
63 | currentColor = pixel
64 | colorCount += 1
65 | }
66 | }
67 |
68 | return colorCount
69 | }
70 |
71 | private mutating func countFrequencies(_ pixels: [Int64]) {
72 | if pixels.count == 0 {
73 | return
74 | }
75 |
76 | var currentColorIndex = 0
77 | var currentColor = pixels[0]
78 |
79 | self.colors.append(currentColor)
80 | self.colorCounts.append(1)
81 |
82 | if pixels.count == 1 {
83 | // If we only have one pixel, we can stop here
84 | return
85 | }
86 |
87 | // Now iterate from the second pixel to the end, population distinct colors
88 | for pixel in pixels {
89 | if pixel == currentColor {
90 | // We’ve hit the same color as before, increase population
91 | self.colorCounts[currentColorIndex] += 1
92 | } else {
93 | // We’ve hit a new color, increase index
94 | currentColor = pixel
95 |
96 | currentColorIndex += 1
97 | self.colors.append(currentColor)
98 | self.colorCounts.append(1)
99 | }
100 | }
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/ios/Runner/ImagePalette/DefaultPaletteGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DefaultGenerator.swift
3 | // ImagePalette
4 | //
5 | // Original created by Google/Android
6 | // Ported to Swift/iOS by Shaun Harrison
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | private let TARGET_DARK_LUMA = CGFloat(0.26)
13 | private let MAX_DARK_LUMA = CGFloat(0.45)
14 |
15 | private let MIN_LIGHT_LUMA = CGFloat(0.55)
16 | private let TARGET_LIGHT_LUMA = CGFloat(0.74)
17 |
18 | private let MIN_NORMAL_LUMA = CGFloat(0.3)
19 | private let TARGET_NORMAL_LUMA = CGFloat(0.5)
20 | private let MAX_NORMAL_LUMA = CGFloat(0.7)
21 |
22 | private let TARGET_MUTED_SATURATION = CGFloat(0.3)
23 | private let MAX_MUTED_SATURATION = CGFloat(0.4)
24 |
25 | private let TARGET_VIBRANT_SATURATION = CGFloat(1)
26 | private let MIN_VIBRANT_SATURATION = CGFloat(0.35)
27 |
28 | private let WEIGHT_SATURATION = CGFloat(3.0)
29 | private let WEIGHT_LUMA = CGFloat(6.0)
30 | private let WEIGHT_POPULATION = CGFloat(1.0)
31 |
32 | internal class DefaultPaletteGenerator: PaletteGenerator {
33 |
34 | private var swatches = [PaletteSwatch]()
35 | private var highestPopulation = Int64(0)
36 |
37 | private(set) var vibrantSwatch: PaletteSwatch?
38 | private(set) var lightVibrantSwatch: PaletteSwatch?
39 | private(set) var darkVibrantSwatch: PaletteSwatch?
40 | private(set) var mutedSwatch: PaletteSwatch?
41 | private(set) var lightMutedSwatch: PaletteSwatch?
42 | private(set) var darkMutedSwatch: PaletteSwatch?
43 |
44 | func generate(swatches: [PaletteSwatch]) {
45 | self.swatches = swatches
46 |
47 | self.highestPopulation = findMaxPopulation()
48 |
49 | self.generateVariationColors()
50 |
51 | // Now try and generate any missing colors
52 | self.generateEmptySwatches()
53 | }
54 |
55 | private func generateVariationColors() {
56 | self.vibrantSwatch = self.findColorVariation(targetLuma: TARGET_NORMAL_LUMA, minLuma: MIN_NORMAL_LUMA, maxLuma: MAX_NORMAL_LUMA, targetSaturation: TARGET_VIBRANT_SATURATION, minSaturation: MIN_VIBRANT_SATURATION, maxSaturation: 1.0)
57 | self.lightVibrantSwatch = self.findColorVariation(targetLuma: TARGET_LIGHT_LUMA, minLuma: MIN_LIGHT_LUMA, maxLuma: 1.0, targetSaturation: TARGET_VIBRANT_SATURATION, minSaturation: MIN_VIBRANT_SATURATION, maxSaturation: 1.0)
58 | self.darkVibrantSwatch = self.findColorVariation(targetLuma: TARGET_DARK_LUMA, minLuma: 0.0, maxLuma: MAX_DARK_LUMA, targetSaturation: TARGET_VIBRANT_SATURATION, minSaturation: MIN_VIBRANT_SATURATION, maxSaturation: 1.0)
59 | self.mutedSwatch = self.findColorVariation(targetLuma: TARGET_NORMAL_LUMA, minLuma: MIN_NORMAL_LUMA, maxLuma: MAX_NORMAL_LUMA, targetSaturation: TARGET_MUTED_SATURATION, minSaturation: 0.0, maxSaturation: MAX_MUTED_SATURATION)
60 | self.lightMutedSwatch = self.findColorVariation(targetLuma: TARGET_LIGHT_LUMA, minLuma: MIN_LIGHT_LUMA, maxLuma: 1.0, targetSaturation: TARGET_MUTED_SATURATION, minSaturation: 0.0, maxSaturation: MAX_MUTED_SATURATION)
61 | self.darkMutedSwatch = self.findColorVariation(targetLuma: TARGET_DARK_LUMA, minLuma: 0.0, maxLuma: MAX_DARK_LUMA, targetSaturation: TARGET_MUTED_SATURATION, minSaturation: 0.0, maxSaturation: MAX_MUTED_SATURATION)
62 | }
63 |
64 | /** Try and generate any missing swatches from the swatches we did find. */
65 | private func generateEmptySwatches() {
66 | if self.vibrantSwatch == nil {
67 | // If we do not have a vibrant color...
68 | if let swatch = self.darkVibrantSwatch {
69 | // ...but we do have a dark vibrant, generate the value by modifying the luma
70 | self.vibrantSwatch = PaletteSwatch(color: swatch.hsl.colorWith(lightness: TARGET_NORMAL_LUMA), population: 0)
71 | }
72 | }
73 |
74 | if self.darkVibrantSwatch == nil {
75 | // If we do not have a dark vibrant color...
76 | if let swatch = self.vibrantSwatch {
77 | // ...but we do have a vibrant, generate the value by modifying the luma
78 | self.darkVibrantSwatch = PaletteSwatch(color: swatch.hsl.colorWith(lightness: TARGET_DARK_LUMA), population: 0)
79 | }
80 | }
81 | }
82 |
83 | /** Find the PaletteSwatch with the highest population value and return the population. */
84 | private func findMaxPopulation() -> Int64 {
85 | var population = Int64(0)
86 |
87 | for swatch in self.swatches {
88 | population = max(population, swatch.population)
89 | }
90 |
91 | return population
92 | }
93 |
94 | private func findColorVariation(targetLuma: CGFloat, minLuma: CGFloat, maxLuma: CGFloat, targetSaturation: CGFloat, minSaturation: CGFloat, maxSaturation: CGFloat) -> PaletteSwatch? {
95 | var max: PaletteSwatch? = nil
96 | var maxValue = 0.0
97 |
98 | for swatch in self.swatches {
99 | let sat = swatch.hsl.saturation
100 | let luma = swatch.hsl.lightness
101 |
102 | guard sat >= minSaturation && sat <= maxSaturation && luma >= minLuma && luma <= maxLuma && !isAlreadySelected(swatch: swatch) else {
103 | continue
104 | }
105 |
106 | let value = Double(type(of: self).createComparisonValue(saturation: sat, targetSaturation: targetSaturation, luma: luma, targetLuma: targetLuma, population: swatch.population, maxPopulation: self.highestPopulation))
107 |
108 | guard max == nil || value > maxValue else {
109 | continue
110 | }
111 |
112 | max = swatch
113 | maxValue = value
114 | }
115 |
116 | return max
117 | }
118 |
119 | /**
120 | :return: true if we have already selected PaletteSwatch
121 | */
122 | private func isAlreadySelected(swatch: PaletteSwatch) -> Bool {
123 | return self.vibrantSwatch == swatch || self.darkVibrantSwatch == swatch || self.lightVibrantSwatch == swatch || self.mutedSwatch == swatch || self.darkMutedSwatch == swatch || self.lightMutedSwatch == swatch
124 | }
125 |
126 | private static func createComparisonValue(saturation: CGFloat, targetSaturation: CGFloat, luma: CGFloat, targetLuma: CGFloat, population: Int64, maxPopulation: Int64) -> CGFloat {
127 | return self.createComparisonValue(saturation: saturation, targetSaturation: targetSaturation, saturationWeight: WEIGHT_SATURATION, luma: luma, targetLuma: targetLuma, lumaWeight: WEIGHT_LUMA, population: population, maxPopulation: maxPopulation, populationWeight: WEIGHT_POPULATION)
128 | }
129 |
130 | private static func createComparisonValue(saturation: CGFloat, targetSaturation: CGFloat, saturationWeight: CGFloat, luma: CGFloat, targetLuma: CGFloat, lumaWeight: CGFloat, population: Int64, maxPopulation: Int64, populationWeight: CGFloat) -> CGFloat {
131 | return weightedMean(values: [
132 | invertDiff(value: saturation, targetValue: targetSaturation), saturationWeight,
133 | invertDiff(value: luma, targetValue: targetLuma), lumaWeight,
134 | CGFloat(population) / CGFloat(maxPopulation), populationWeight
135 | ])
136 | }
137 |
138 | /**
139 | Returns a value in the range 0-1. 1 is returned when value equals the targetValue and then decreases as the
140 | absolute difference between value and {@code targetValue} increases.
141 |
142 | :param: value the item’s value
143 | :param: targetValue the value which we desire
144 | */
145 | private static func invertDiff(value: CGFloat, targetValue: CGFloat) -> CGFloat {
146 | return 1.0 - abs(value - targetValue)
147 | }
148 |
149 | private static func weightedMean(values: [CGFloat]) -> CGFloat {
150 | var sum = CGFloat(0)
151 | var sumWeight = CGFloat(0)
152 |
153 | for i in stride(from: 0, to: values.count - 1, by: 2) {
154 | let value = values[i]
155 | let weight = values[i + 1]
156 |
157 | sum += (value * weight)
158 | sumWeight += weight
159 | }
160 |
161 | return sum / sumWeight
162 | }
163 |
164 | }
165 |
--------------------------------------------------------------------------------
/ios/Runner/ImagePalette/HSLColor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HSL.swift
3 | // ImagePalette
4 | //
5 | // Original created by Google/Android
6 | // Ported to Swift/iOS by Shaun Harrison
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | internal struct HSLColor {
13 | let hue: CGFloat
14 | let saturation: CGFloat
15 | let lightness: CGFloat
16 | let alpha: CGFloat
17 |
18 | init(hue: CGFloat, saturation: CGFloat, lightness: CGFloat, alpha: CGFloat) {
19 | self.hue = hue
20 | self.saturation = saturation
21 | self.lightness = lightness
22 | self.alpha = alpha
23 | }
24 |
25 | var color: UIColor {
26 | return self.rgb.color
27 | }
28 |
29 | var rgb: RGBColor {
30 | let c = (1.0 - abs(2 * self.lightness - 1.0)) * self.saturation
31 | let m = self.lightness - 0.5 * c
32 | let x = c * (1.0 - abs(((self.hue / 60.0).truncatingRemainder(dividingBy: 2.0)) - 1.0))
33 |
34 | let hueSegment = Int(self.hue / 60.0)
35 |
36 | var r = Int64(0)
37 | var g = Int64(0)
38 | var b = Int64(0)
39 |
40 | switch hueSegment {
41 | case 0:
42 | r = Int64(round(255 * (c + m)))
43 | g = Int64(round(255 * (x + m)))
44 | b = Int64(round(255 * m))
45 | case 1:
46 | r = Int64(round(255 * (x + m)))
47 | g = Int64(round(255 * (c + m)))
48 | b = Int64(round(255 * m))
49 | case 2:
50 | r = Int64(round(255 * m))
51 | g = Int64(round(255 * (c + m)))
52 | b = Int64(round(255 * (x + m)))
53 | case 3:
54 | r = Int64(round(255 * m))
55 | g = Int64(round(255 * (x + m)))
56 | b = Int64(round(255 * (c + m)))
57 | case 4:
58 | r = Int64(round(255 * (x + m)))
59 | g = Int64(round(255 * m))
60 | b = Int64(round(255 * (c + m)))
61 | case 5, 6:
62 | r = Int64(round(255 * (c + m)))
63 | g = Int64(round(255 * m))
64 | b = Int64(round(255 * (x + m)))
65 | default:
66 | break
67 | }
68 |
69 | r = max(0, min(255, r))
70 | g = max(0, min(255, g))
71 | b = max(0, min(255, b))
72 |
73 | return RGBColor(red: r, green: g, blue: b, alpha: Int64(round(self.alpha * 255)))
74 | }
75 |
76 | internal func colorWith(lightness: CGFloat) -> HSLColor {
77 | return HSLColor(hue: self.hue, saturation: self.saturation, lightness: self.lightness, alpha: self.alpha)
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/ios/Runner/ImagePalette/HexColor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HexColor.swift
3 | // ImagePalette
4 | //
5 | // Original created by Google/Android
6 | // Ported to Swift/iOS by Shaun Harrison
7 | //
8 |
9 | import UIKit
10 |
11 | private let MIN_ALPHA_SEARCH_MAX_ITERATIONS = Int64(10)
12 | private let MIN_ALPHA_SEARCH_PRECISION = Int64(10)
13 |
14 | internal struct HexColor {
15 | static let WHITE = HexColor.fromRGB(255, green: 255, blue: 255)
16 | static let BLACK = HexColor.fromRGB(0, green: 0, blue: 0)
17 |
18 | /**
19 | :return: The alpha component of a color int.
20 | */
21 | internal static func alpha(_ color: Int64) -> Int64 {
22 | return color >> 24
23 | }
24 |
25 | /**
26 | :return: The red component of a color int.
27 | */
28 | internal static func red(_ color: Int64) -> Int64 {
29 | return (color >> 16) & 0xFF
30 | }
31 |
32 | /**
33 | :return: The green component of a color int.
34 | */
35 | internal static func green(_ color: Int64) -> Int64 {
36 | return (color >> 8) & 0xFF
37 | }
38 |
39 | /**
40 | :return: The blue component of a color int.
41 | */
42 | internal static func blue(_ color: Int64) -> Int64 {
43 | return color & 0xFF
44 | }
45 |
46 | /**
47 | Return a color-int from red, green, blue components.
48 | The alpha component is implicity 255 (fully opaque).
49 | These component values should be [0..255], but there is no
50 | range check performed, so if they are out of range, the
51 | returned color is undefined.
52 |
53 | :param: red Red component [0..255] of the color
54 | :param: green Green component [0..255] of the color
55 | :param: blue Blue component [0..255] of the color
56 | */
57 | internal static func fromRGB(_ red: Int64, green: Int64, blue: Int64) -> Int64 {
58 | return (0xFF << 24) | (red << 16) | (green << 8) | blue
59 | }
60 |
61 | /**
62 | Return a color-int from alpha, red, green, blue components.
63 | These component values should be [0..255], but there is no
64 | range check performed, so if they are out of range, the
65 | returned color is undefined.
66 |
67 | :param: alpha Alpha component [0..255] of the color
68 | :param: red Red component [0..255] of the color
69 | :param: green Green component [0..255] of the color
70 | :param: blue Blue component [0..255] of the color
71 | */
72 | internal static func fromARGB(_ alpha: Int64, red: Int64, green: Int64, blue: Int64) -> Int64 {
73 | return (alpha << 24) | (red << 16) | (green << 8) | blue
74 | }
75 |
76 | internal static func toUIColor(_ color: Int64) -> UIColor {
77 | return UIColor(red: CGFloat(self.red(color)) / 255.0, green: CGFloat(self.green(color)) / 255.0, blue: CGFloat(self.blue(color)) / 255.0, alpha: CGFloat(self.alpha(color)) / 255.0)
78 | }
79 |
80 | internal static func toRGB(_ color: Int64) -> RGBColor {
81 | return RGBColor(red: self.red(color), green: self.green(color), blue: self.blue(color), alpha: self.alpha(color))
82 | }
83 |
84 | internal static func toHSL(_ color: Int64) -> HSLColor {
85 | return self.toRGB(color).hsl
86 | }
87 |
88 | /** Composite two potentially translucent colors over each other and returns the result. */
89 | internal static func compositeColors(_ foreground: Int64, background: Int64) -> Int64 {
90 | let alpha1 = CGFloat(self.alpha(foreground)) / 255.0
91 | let alpha2 = CGFloat(self.alpha(background)) / 255.0
92 |
93 | let a = (alpha1 + alpha2) * (1.0 - alpha1)
94 | let r = (CGFloat(self.red(foreground)) * alpha1) + (CGFloat(self.red(background)) * alpha2 * (1.0 - alpha1))
95 | let g = (CGFloat(self.green(foreground)) * alpha1) + (CGFloat(self.green(background)) * alpha2 * (1.0 - alpha1))
96 | let b = (CGFloat(self.blue(foreground)) * alpha1) + (CGFloat(self.blue(background)) * alpha2 * (1.0 - alpha1))
97 |
98 | return self.fromARGB(Int64(a), red: Int64(r), green: Int64(g), blue: Int64(b))
99 | }
100 |
101 | /**
102 | Formula defined here: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
103 |
104 | :return: The luminance of a color
105 | */
106 | internal static func calculateLuminance(color: Int64) -> CGFloat {
107 | var red = CGFloat(self.red(color)) / 255.0
108 | red = red < 0.03928 ? red / 12.92 : pow((red + 0.055) / 1.055, 2.4)
109 |
110 | var green = CGFloat(self.green(color)) / 255.0
111 | green = green < 0.03928 ? green / 12.92 : pow((green + 0.055) / 1.055, 2.4)
112 |
113 | var blue = CGFloat(self.blue(color)) / 255.0
114 | blue = blue < 0.03928 ? blue / 12.92 : pow((blue + 0.055) / 1.055, 2.4)
115 |
116 | return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue)
117 | }
118 |
119 | /**
120 | Returns the contrast ratio between foreground and background.
121 | background must be opaque.
122 |
123 | Formula defined http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
124 | */
125 | internal static func calculateContrast(foreground: Int64, background: Int64) -> CGFloat {
126 | var foreground = foreground
127 |
128 | assert(self.alpha(background) == 255, "background can not be translucent")
129 |
130 | if self.alpha(foreground) < 255 {
131 | // If the foreground is translucent, composite the foreground over the background
132 | foreground = self.compositeColors(foreground, background: background)
133 | }
134 |
135 | let luminance1 = calculateLuminance(color: foreground) + 0.05
136 | let luminance2 = calculateLuminance(color: background) + 0.05
137 |
138 | // Now return the lighter luminance divided by the darker luminance
139 | return max(luminance1, luminance2) / min(luminance1, luminance2)
140 | }
141 |
142 | /**
143 | Calculates the minimum alpha value which can be applied to foreground so that would
144 | have a contrast value of at least minContrastRatio when compared to
145 | background.
146 |
147 | :param: foreground the foreground color.
148 | :param: background the background color. Should be opaque.
149 | :param: minContrastRatio the minimum contrast ratio.
150 |
151 | :return: the alpha value in the range 0-255, or nil if no value could be calculated.
152 | */
153 | internal static func calculateMinimumAlpha(foreground: Int64, background: Int64, minContrastRatio: CGFloat) -> Int64? {
154 | assert(self.alpha(background) == 255, "background can not be translucent")
155 |
156 | // First lets check that a fully opaque foreground has sufficient contrast
157 | var testForeground = self.setAlphaComponent(color: foreground, alpha: 255)
158 | var testRatio: CGFloat = self.calculateContrast(foreground: testForeground, background: background)
159 | if testRatio < minContrastRatio {
160 | // Fully opaque foreground does not have sufficient contrast, return error
161 | return nil
162 | }
163 |
164 | // Binary search to find a value with the minimum value which provides sufficient contrast
165 | var numIterations = Int64(0)
166 | var minAlpha = Int64(0)
167 | var maxAlpha = Int64(255)
168 |
169 | while numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS && (maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION {
170 | let testAlpha = Int64((minAlpha + maxAlpha) / 2)
171 |
172 | testForeground = self.setAlphaComponent(color: foreground, alpha: testAlpha)
173 | testRatio = self.calculateContrast(foreground: testForeground, background: background)
174 |
175 | if testRatio < minContrastRatio {
176 | minAlpha = testAlpha
177 | } else {
178 | maxAlpha = testAlpha
179 | }
180 |
181 | numIterations += 1
182 | }
183 |
184 | // Conservatively return the max of the range of possible alphas, which is known to pass.
185 | return maxAlpha
186 | }
187 |
188 | /** Set the alpha component of color to be alpha. */
189 | internal static func setAlphaComponent(color: Int64, alpha: Int64) -> Int64 {
190 | assert(alpha >= 0 && alpha <= 255, "alpha must be between 0 and 255.")
191 | return (color & 0x00ffffff) | (alpha << 24)
192 | }
193 |
194 | }
195 |
--------------------------------------------------------------------------------
/ios/Runner/ImagePalette/Palette.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Palette.swift
3 | // ImagePalette
4 | //
5 | // Original created by Google/Android
6 | // Ported to Swift/iOS by Shaun Harrison
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | private func createQueue() -> DispatchQueue {
13 | return DispatchQueue(label: "palette.generator", qos: .background, attributes: .concurrent, target: DispatchQueue.global(qos: .background))
14 | }
15 |
16 | public struct Palette {
17 | static var internalQueue = createQueue()
18 |
19 | /** All of the swatches which make up the palette */
20 | public let swatches: [PaletteSwatch]
21 | private let generator: PaletteGenerator
22 |
23 | private init(swatches: [PaletteSwatch], generator: PaletteGenerator) {
24 | self.swatches = swatches
25 | self.generator = generator
26 | }
27 |
28 | /**
29 | Create a palette synchronously.
30 | */
31 | public init(configuration: PaletteConfiguration) {
32 | let (swatches, generator) = configuration.generate()
33 | self.init(swatches: swatches, generator: generator)
34 | }
35 |
36 | public static func generateWith(configuration: PaletteConfiguration, completion: @escaping (Palette) -> Void) {
37 | self.generateWith(configuration: configuration, queue: self.internalQueue, completion: completion)
38 | }
39 |
40 | public static func generateWith(configuration: PaletteConfiguration, queue: DispatchQueue, completion: @escaping (Palette) -> Void) {
41 | queue.async {
42 | let (swatches, generator) = configuration.generate()
43 | let palette = self.init(swatches: swatches, generator: generator)
44 |
45 | DispatchQueue.main.async {
46 | completion(palette)
47 | }
48 | }
49 | }
50 |
51 | /** The most vibrant swatch in the palette. */
52 | public var vibrantSwatch: PaletteSwatch? {
53 | return self.generator.vibrantSwatch
54 | }
55 |
56 | /** Light and vibrant swatch from the palette. */
57 | public var lightVibrantSwatch: PaletteSwatch? {
58 | return self.generator.lightVibrantSwatch
59 | }
60 |
61 | /** Dark and vibrant swatch from the palette. */
62 | public var darkVibrantSwatch: PaletteSwatch? {
63 | return self.generator.darkVibrantSwatch
64 | }
65 |
66 | /** Muted swatch from the palette. */
67 | public var mutedSwatch: PaletteSwatch? {
68 | return self.generator.mutedSwatch
69 | }
70 |
71 | /** Muted and light swatch from the palette. */
72 | public var lightMutedSwatch: PaletteSwatch? {
73 | return self.generator.lightMutedSwatch
74 | }
75 |
76 | /** Muted and dark swatch from the palette. */
77 | public var darkMutedSwatch: PaletteSwatch? {
78 | return self.generator.darkMutedSwatch
79 | }
80 |
81 | /**
82 | Returns the most vibrant color in the palette as an RGB packed int.
83 |
84 | :param: defaultColor value to return if the swatch isn't available
85 | */
86 | public func vibrantColor(defaultColor: UIColor) -> UIColor {
87 | return self.vibrantSwatch?.color ?? defaultColor
88 | }
89 |
90 | /**
91 | Returns a light and vibrant color from the palette as an RGB packed int.
92 |
93 | :param: defaultColor value to return if the swatch isn't available
94 | */
95 | public func lightVibrantColor(defaultColor: UIColor) -> UIColor {
96 | return self.lightVibrantSwatch?.color ?? defaultColor
97 | }
98 |
99 | /**
100 | Returns a dark and vibrant color from the palette as an RGB packed int.
101 |
102 | :param: defaultColor value to return if the swatch isn't available
103 | */
104 | public func darkVibrantColor(defaultColor: UIColor) -> UIColor {
105 | return self.darkVibrantSwatch?.color ?? defaultColor
106 | }
107 |
108 | /**
109 | Returns a muted color from the palette as an RGB packed int.
110 |
111 | :param: defaultColor value to return if the swatch isn't available
112 | */
113 | public func mutedColor(defaultColor: UIColor) -> UIColor {
114 | return self.mutedSwatch?.color ?? defaultColor
115 | }
116 |
117 | /**
118 | Returns a muted and light color from the palette as an RGB packed int.
119 |
120 | :param: defaultColor value to return if the swatch isn't available
121 | */
122 | public func lightMutedColor(defaultColor: UIColor) -> UIColor {
123 | return self.lightMutedSwatch?.color ?? defaultColor
124 | }
125 |
126 | /**
127 | Returns a muted and dark color from the palette as an RGB packed int.
128 |
129 | :param: defaultColor value to return if the swatch isn't available
130 | */
131 | public func darkMutedColor(defaultColor: UIColor) -> UIColor {
132 | return self.darkMutedSwatch?.color ?? defaultColor
133 | }
134 |
135 | }
136 |
--------------------------------------------------------------------------------
/ios/Runner/ImagePalette/PaletteConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PaletteConfiguration.swift
3 | // ImagePalette
4 | //
5 | // Original created by Google/Android
6 | // Ported to Swift/iOS by Shaun Harrison
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | public struct PaletteConfiguration {
13 | private static let DEFAULT_CALCULATE_NUMBER_COLORS = 16
14 | private static let DEFAULT_RESIZE_BITMAP_MAX_DIMENSION = CGFloat(192)
15 |
16 | private let bitmap: UIImage?
17 | private let swatches: [PaletteSwatch]?
18 |
19 | /**
20 | The maximum number of colors to use in the quantization step when using an image
21 | as the source.
22 |
23 | Good values for depend on the source image type. For landscapes, good values are in
24 | the range 10-16. For images which are largely made up of people’s faces then this
25 | value should be increased to ~24.
26 | */
27 | public var maxColors = DEFAULT_CALCULATE_NUMBER_COLORS
28 |
29 | /**
30 | Set the resize value when using an image as the source. If the bitmap’s largest
31 | dimension is greater than the value specified, then the image will be resized so
32 | that it’s largest dimension matches maxDimension. If the bitmap is smaller or
33 | equal, the original is used as-is.
34 |
35 | This value has a large effect on the processing time. The larger the resized image is,
36 | the greater time it will take to generate the palette. The smaller the image is, the
37 | more detail is lost in the resulting image and thus less precision for color selection.
38 | */
39 | public var resizeMaxDimension = DEFAULT_RESIZE_BITMAP_MAX_DIMENSION
40 |
41 | /** Generator to use when generating the palette. If nil, the default generator will be used. */
42 | public var generator: PaletteGenerator?
43 |
44 | /**
45 | Create configuration using a source image
46 | */
47 | public init(image: UIImage) {
48 | self.bitmap = image
49 | self.swatches = nil
50 | }
51 |
52 | /**
53 | Create configuration using an array of PaletteSwatch instances.
54 | Typically only used for testing.
55 | */
56 | public init(swatches: [PaletteSwatch]) {
57 | assert(swatches.count > 0, "Swatches array must contain at least one swatch")
58 |
59 | self.bitmap = nil
60 | self.swatches = swatches
61 | }
62 |
63 | internal func generate() -> ([PaletteSwatch], PaletteGenerator) {
64 | var swatches: [PaletteSwatch]
65 |
66 | if let image = self.bitmap {
67 | // We have a Image so we need to quantization to reduce the number of colors
68 |
69 | assert(self.resizeMaxDimension > 0, "Minimum dimension size for resizing should should be >= 1")
70 |
71 | // First we’ll scale down the bitmap so it’s largest dimension is as specified
72 | guard let scaledBitmap = image.scaleDown(to: self.resizeMaxDimension) else {
73 | fatalError("Unable to scale down image.")
74 | }
75 |
76 | // Now generate a quantizer from the Bitmap
77 | let quantizer = ColorCutQuantizer.from(image: scaledBitmap, maxColors: self.maxColors);
78 | swatches = quantizer.quantizedColors
79 | } else if let s = self.swatches {
80 | // We’re using the provided swatches
81 | swatches = s
82 | } else {
83 | fatalError("Invalid palette configuration.")
84 | }
85 |
86 | // If we haven't been provided with a generator, use the default
87 | let generator: PaletteGenerator
88 |
89 | if let g = self.generator {
90 | generator = g
91 | } else {
92 | generator = DefaultPaletteGenerator()
93 | }
94 |
95 | // Now call let the Generator do it’s thing
96 | generator.generate(swatches: swatches)
97 |
98 | return (swatches, generator)
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/ios/Runner/ImagePalette/PaletteGenerator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PaletteGenerator.swift
3 | // ImagePalette
4 | //
5 | // Original created by Google/Android
6 | // Ported to Swift/iOS by Shaun Harrison
7 | //
8 |
9 | /**
10 | Protocol for Palette which allows custom processing of the list of
11 | PaletteSwatch instances which represent an image.
12 |
13 | You should do as much processing as possible during the
14 | generate method call. The other methods in this class
15 | may be called multiple times to retrieve an appropriate PaletteSwatch.
16 | */
17 | public protocol PaletteGenerator {
18 |
19 | /**
20 | This method will be called with the PaletteSwatch that represent an image.
21 | You should process this list so that you have appropriate values when the other methods in
22 | class are called.
23 |
24 | This method will probably be called on a background thread.
25 | */
26 | func generate(swatches: [PaletteSwatch])
27 |
28 | /** Return the most vibrant PaletteSwatch */
29 | var vibrantSwatch: PaletteSwatch? { get }
30 |
31 | /** A light and vibrant PaletteSwatch */
32 | var lightVibrantSwatch: PaletteSwatch? { get }
33 |
34 | /** A dark and vibrant PaletteSwatch */
35 | var darkVibrantSwatch: PaletteSwatch? { get }
36 |
37 | /** A muted PaletteSwatch */
38 | var mutedSwatch: PaletteSwatch? { get }
39 |
40 | /** A muted and light PaletteSwatch */
41 | var lightMutedSwatch: PaletteSwatch? { get }
42 |
43 | /** A muted and dark PaletteSwatch */
44 | var darkMutedSwatch: PaletteSwatch? { get }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/ios/Runner/ImagePalette/PaletteSwatch.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PaletteSwatch.swift
3 | // ImagePalette
4 | //
5 | // Original created by Google/Android
6 | // Ported to Swift/iOS by Shaun Harrison
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | /** Represents a color swatch generated from an image’s palette. */
13 | open class PaletteSwatch {
14 | private static let MIN_CONTRAST_TITLE_TEXT = CGFloat(3.0)
15 | private static let MIN_CONTRAST_BODY_TEXT = CGFloat(4.5)
16 |
17 | fileprivate let rgb: RGBColor
18 | private let hex: Int64
19 |
20 | /** This swatch’s color */
21 | public let color: UIColor
22 |
23 | /** The number of pixels represented by this swatch */
24 | public let population: Int64
25 |
26 | private var generatedTextColors: Bool = false
27 | private var _titleTextColor: UIColor?
28 | private var _bodyTextColor: UIColor?
29 |
30 | fileprivate var _hsl: HSLColor?
31 |
32 | public convenience init(color: UIColor, population: Int64) {
33 | self.init(rgbColor: RGBColor(color: color), population: population)
34 | }
35 |
36 | internal convenience init(color: HSLColor, population: Int64) {
37 | self.init(rgbColor: color.rgb, population: population)
38 | }
39 |
40 | internal init(rgbColor: RGBColor, population: Int64) {
41 | self.rgb = rgbColor
42 | self.hex = rgbColor.hex
43 | self.color = rgbColor.color
44 | self.population = population
45 | }
46 |
47 | /**
48 | Return this swatch’s HSL values.
49 | */
50 | internal var hsl: HSLColor {
51 | if let hsl = self._hsl {
52 | return hsl
53 | }
54 |
55 | let hsl = self.rgb.hsl
56 | self._hsl = hsl
57 | return hsl
58 | }
59 |
60 | /**
61 | * An appropriate color to use for any 'title' text which is displayed over this
62 | * Swatch’s color. This color is guaranteed to have sufficient contrast.
63 | */
64 | open var titleTextColor: UIColor? {
65 | self.ensureTextColorsGenerated()
66 | return self._titleTextColor
67 | }
68 |
69 | /**
70 | * An appropriate color to use for any 'body' text which is displayed over this
71 | * Swatch’s color. This color is guaranteed to have sufficient contrast.
72 | */
73 | open var bodyTextColor: UIColor? {
74 | self.ensureTextColorsGenerated()
75 | return self._bodyTextColor
76 | }
77 |
78 | private func ensureTextColorsGenerated() {
79 | guard !self.generatedTextColors else {
80 | return
81 | }
82 |
83 | // First check white, as most colors will be dark
84 | let lightBodyAlpha = HexColor.calculateMinimumAlpha(foreground: HexColor.WHITE, background: hex, minContrastRatio: type(of: self).MIN_CONTRAST_BODY_TEXT)
85 | let lightTitleAlpha = HexColor.calculateMinimumAlpha(foreground: HexColor.WHITE, background: hex, minContrastRatio: type(of: self).MIN_CONTRAST_TITLE_TEXT)
86 |
87 | if let lightBodyAlpha = lightBodyAlpha, let lightTitleAlpha = lightTitleAlpha {
88 | // If we found valid light values, use them and return
89 | self._bodyTextColor = UIColor(white: 1.0, alpha: CGFloat(lightBodyAlpha) / 255.0)
90 | self._titleTextColor = UIColor(white: 1.0, alpha: CGFloat(lightTitleAlpha) / 255.0)
91 |
92 | self.generatedTextColors = true
93 | return
94 | }
95 |
96 | let darkBodyAlpha = HexColor.calculateMinimumAlpha(foreground: HexColor.BLACK, background: hex, minContrastRatio: type(of: self).MIN_CONTRAST_BODY_TEXT)
97 | let darkTitleAlpha = HexColor.calculateMinimumAlpha(foreground: HexColor.BLACK, background: hex, minContrastRatio: type(of: self).MIN_CONTRAST_TITLE_TEXT)
98 |
99 | if let darkBodyAlpha = darkBodyAlpha, let darkTitleAlpha = darkTitleAlpha {
100 | // If we found valid dark values, use them and return
101 | self._bodyTextColor = UIColor(white: 0.0, alpha: CGFloat(darkBodyAlpha) / 255.0)
102 | self._titleTextColor = UIColor(white: 0.0, alpha: CGFloat(darkTitleAlpha) / 255.0)
103 |
104 | self.generatedTextColors = true
105 | return
106 | }
107 |
108 | // If we reach here then we can not find title and body values which use the same
109 | // lightness, we need to use mismatched values
110 | if let lightBodyAlpha = lightBodyAlpha {
111 | self._bodyTextColor = UIColor(white: 1.0, alpha: CGFloat(lightBodyAlpha) / 255.0)
112 | } else if let darkBodyAlpha = darkBodyAlpha {
113 | self._bodyTextColor = UIColor(white: 0.0, alpha: CGFloat(darkBodyAlpha) / 255.0)
114 | }
115 |
116 | if let lightTitleAlpha = lightTitleAlpha {
117 | self._titleTextColor = UIColor(white: 1.0, alpha: CGFloat(lightTitleAlpha) / 255.0)
118 | } else if let darkTitleAlpha = darkTitleAlpha {
119 | self._titleTextColor = UIColor(white: 0.0, alpha: CGFloat(darkTitleAlpha) / 255.0)
120 | }
121 |
122 | self.generatedTextColors = true
123 | }
124 |
125 | }
126 |
127 | extension PaletteSwatch: CustomDebugStringConvertible {
128 |
129 | public var debugDescription: String {
130 | var description = "<\(type(of: self)) 0x\(self.hashValue)"
131 | description += "; color = \(self.color)"
132 | description += "; hsl = \(String(describing: self._hsl))"
133 | description += "; population = \(self.population)"
134 | description += "; titleTextColor = \(String(describing: self.titleTextColor))"
135 | description += "; bodyTextColor = \(String(describing: self.bodyTextColor))"
136 | return description + ">"
137 | }
138 |
139 | }
140 |
141 | extension PaletteSwatch: Equatable, Hashable {
142 |
143 | public var hashValue: Int {
144 | let maxInt = Int(Int32.max)
145 | return Int((31 * self.color.hashValue + Int(self.population)) % maxInt)
146 | }
147 |
148 | }
149 |
150 | public func ==(lhs: PaletteSwatch, rhs: PaletteSwatch) -> Bool {
151 | return lhs.population == rhs.population && lhs.rgb == rhs.rgb
152 | }
153 |
154 |
--------------------------------------------------------------------------------
/ios/Runner/ImagePalette/RGBColor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RGB.swift
3 | // ImagePalette
4 | //
5 | // Original created by Google/Android
6 | // Ported to Swift/iOS by Shaun Harrison
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | internal struct RGBColor: Hashable, Equatable {
13 | let red: Int64
14 | let green: Int64
15 | let blue: Int64
16 | let alpha: Int64
17 | let hashValue: Int
18 |
19 | init(red: Int64, green: Int64, blue: Int64, alpha: Int64) {
20 | self.red = red
21 | self.green = green
22 | self.blue = blue
23 | self.alpha = alpha
24 | let maxInt = Int64(Int32.max)
25 | self.hashValue = Int(((alpha << 24) | (red << 16) | (green << 8) | blue) % maxInt)
26 | }
27 |
28 | init(color: UIColor) {
29 | var red: CGFloat = 0.0
30 | var green: CGFloat = 0.0
31 | var blue: CGFloat = 0.0
32 | var alpha: CGFloat = 0.0
33 |
34 | color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
35 |
36 | self.init(red: Int64(round(red * 255.0)), green: Int64(round(green * 255.0)), blue: Int64(round(blue * 255.0)), alpha: Int64(round(alpha * 255.0)))
37 | }
38 |
39 | var color: UIColor {
40 | return UIColor(red: CGFloat(self.red) / 255.0, green: CGFloat(self.green) / 255.0, blue: CGFloat(self.blue) / 255.0, alpha: CGFloat(self.alpha) / 255.0)
41 | }
42 |
43 | var hex: Int64 {
44 | return HexColor.fromRGB(self.red, green: self.green, blue: self.blue)
45 | }
46 |
47 | var hsl: HSLColor {
48 | let rf = CGFloat(self.red) / 255.0
49 | let gf = CGFloat(self.green) / 255.0
50 | let bf = CGFloat(self.blue) / 255.0
51 |
52 | let maxValue = max(rf, max(gf, bf))
53 | let minValue = min(rf, min(gf, bf))
54 | let deltaMaxMin = maxValue - minValue
55 |
56 | var h: CGFloat
57 | var s: CGFloat
58 | let l = (maxValue + minValue) / 2.0
59 |
60 | if maxValue == minValue {
61 | // Monochromatic
62 | h = 0.0
63 | s = 0.0
64 | } else {
65 | if maxValue == rf {
66 | h = ((gf - bf) / deltaMaxMin).truncatingRemainder(dividingBy: 6.0)
67 | } else if maxValue == gf {
68 | h = ((bf - rf) / deltaMaxMin) + 2.0
69 | } else {
70 | h = ((rf - gf) / deltaMaxMin) + 4.0
71 | }
72 |
73 | s = deltaMaxMin / (1.0 - abs(2.0 * l - 1.0))
74 | }
75 |
76 | return HSLColor(hue: (h * 60.0).truncatingRemainder(dividingBy: 360.0), saturation: s, lightness: l, alpha: CGFloat(self.alpha) / 255.0)
77 | }
78 |
79 | }
80 |
81 | internal func ==(lhs: RGBColor, rhs: RGBColor) -> Bool {
82 | return lhs.hashValue == rhs.hashValue
83 | }
84 |
--------------------------------------------------------------------------------
/ios/Runner/ImagePalette/SwiftPriorityQueue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftPriorityQueue.swift
3 | // SwiftPriorityQueue
4 | //
5 | // Copyright (c) 2015-2017 David Kopec
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in all
15 | // copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | // SOFTWARE.
24 |
25 | // This code was inspired by Section 2.4 of Algorithms by Sedgewick & Wayne, 4th Edition
26 |
27 | /// A PriorityQueue takes objects to be pushed of any type that implements Comparable.
28 | /// It will pop the objects in the order that they would be sorted. A pop() or a push()
29 | /// can be accomplished in O(lg n) time. It can be specified whether the objects should
30 | /// be popped in ascending or descending order (Max Priority Queue or Min Priority Queue)
31 | /// at the time of initialization.
32 | public struct PriorityQueue {
33 |
34 | fileprivate var heap = [T]()
35 | private let ordered: (T, T) -> Bool
36 |
37 | public init(ascending: Bool = false, startingValues: [T] = []) {
38 | self.init(order: ascending ? { $0 > $1 } : { $0 < $1 }, startingValues: startingValues)
39 | }
40 |
41 | /// Creates a new PriorityQueue with the given ordering.
42 | ///
43 | /// - parameter order: A function that specifies whether its first argument should
44 | /// come after the second argument in the PriorityQueue.
45 | /// - parameter startingValues: An array of elements to initialize the PriorityQueue with.
46 | public init(order: @escaping (T, T) -> Bool, startingValues: [T] = []) {
47 | ordered = order
48 |
49 | // Based on "Heap construction" from Sedgewick p 323
50 | heap = startingValues
51 | var i = heap.count/2 - 1
52 | while i >= 0 {
53 | sink(i)
54 | i -= 1
55 | }
56 | }
57 |
58 | /// How many elements the Priority Queue stores
59 | public var count: Int { return heap.count }
60 |
61 | /// true if and only if the Priority Queue is empty
62 | public var isEmpty: Bool { return heap.isEmpty }
63 |
64 | /// Add a new element onto the Priority Queue. O(lg n)
65 | ///
66 | /// - parameter element: The element to be inserted into the Priority Queue.
67 | public mutating func push(_ element: T) {
68 | heap.append(element)
69 | swim(heap.count - 1)
70 | }
71 |
72 | /// Remove and return the element with the highest priority (or lowest if ascending). O(lg n)
73 | ///
74 | /// - returns: The element with the highest priority in the Priority Queue, or nil if the PriorityQueue is empty.
75 | public mutating func pop() -> T? {
76 |
77 | if heap.isEmpty { return nil }
78 | if heap.count == 1 { return heap.removeFirst() } // added for Swift 2 compatibility
79 | // so as not to call swap() with two instances of the same location
80 | heap.swapAt(0, heap.count - 1)
81 | let temp = heap.removeLast()
82 | sink(0)
83 |
84 | return temp
85 | }
86 |
87 |
88 | /// Removes the first occurence of a particular item. Finds it by value comparison using ==. O(n)
89 | /// Silently exits if no occurrence found.
90 | ///
91 | /// - parameter item: The item to remove the first occurrence of.
92 | public mutating func remove(_ item: T) {
93 | if let index = heap.index(of: item) {
94 | heap.swapAt(index, heap.count - 1)
95 | heap.removeLast()
96 | if index < heap.count { // if we removed the last item, nothing to swim
97 | swim(index)
98 | sink(index)
99 | }
100 | }
101 | }
102 |
103 | /// Removes all occurences of a particular item. Finds it by value comparison using ==. O(n)
104 | /// Silently exits if no occurrence found.
105 | ///
106 | /// - parameter item: The item to remove.
107 | public mutating func removeAll(_ item: T) {
108 | var lastCount = heap.count
109 | remove(item)
110 | while (heap.count < lastCount) {
111 | lastCount = heap.count
112 | remove(item)
113 | }
114 | }
115 |
116 | /// Get a look at the current highest priority item, without removing it. O(1)
117 | ///
118 | /// - returns: The element with the highest priority in the PriorityQueue, or nil if the PriorityQueue is empty.
119 | public func peek() -> T? {
120 | return heap.first
121 | }
122 |
123 | /// Eliminate all of the elements from the Priority Queue.
124 | public mutating func clear() {
125 | heap.removeAll(keepingCapacity: false)
126 | }
127 |
128 | // Based on example from Sedgewick p 316
129 | private mutating func sink(_ index: Int) {
130 | var index = index
131 | while 2 * index + 1 < heap.count {
132 |
133 | var j = 2 * index + 1
134 |
135 | if j < (heap.count - 1) && ordered(heap[j], heap[j + 1]) { j += 1 }
136 | if !ordered(heap[index], heap[j]) { break }
137 |
138 | heap.swapAt(index, j)
139 | index = j
140 | }
141 | }
142 |
143 | // Based on example from Sedgewick p 316
144 | private mutating func swim(_ index: Int) {
145 | var index = index
146 | while index > 0 && ordered(heap[(index - 1) / 2], heap[index]) {
147 | heap.swapAt((index - 1) / 2, index)
148 | index = (index - 1) / 2
149 | }
150 | }
151 | }
152 |
153 | // MARK: - GeneratorType
154 | extension PriorityQueue: IteratorProtocol {
155 |
156 | public typealias Element = T
157 | mutating public func next() -> Element? { return pop() }
158 | }
159 |
160 | // MARK: - SequenceType
161 | extension PriorityQueue: Sequence {
162 |
163 | public typealias Iterator = PriorityQueue
164 | public func makeIterator() -> Iterator { return self }
165 | }
166 |
167 | // MARK: - CollectionType
168 | extension PriorityQueue: Collection {
169 |
170 | public typealias Index = Int
171 |
172 | public var startIndex: Int { return heap.startIndex }
173 | public var endIndex: Int { return heap.endIndex }
174 |
175 | public subscript(i: Int) -> T { return heap[i] }
176 |
177 | public func index(after i: PriorityQueue.Index) -> PriorityQueue.Index {
178 | return heap.index(after: i)
179 | }
180 | }
181 |
182 | // MARK: - CustomStringConvertible, CustomDebugStringConvertible
183 | extension PriorityQueue: CustomStringConvertible, CustomDebugStringConvertible {
184 |
185 | public var description: String { return heap.description }
186 | public var debugDescription: String { return heap.debugDescription }
187 | }
188 |
--------------------------------------------------------------------------------
/ios/Runner/ImagePalette/UIImage+PaletteExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage+PaletteExtension.swift
3 | // ImagePalette
4 | //
5 | // Original created by Google/Android
6 | // Ported to Swift/iOS by Shaun Harrison
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension UIImage {
13 |
14 | /**
15 | Scale the image down so that it’s largest dimension is targetMaxDimension.
16 | If image is smaller than this, then it is returned.
17 | */
18 | internal func scaleDown(to targetMaxDimension: CGFloat) -> UIImage? {
19 | let size = self.size
20 | let maxDimensionInPoints = max(size.width, size.height)
21 | let maxDimensionInPixels = maxDimensionInPoints * self.scale
22 |
23 | if maxDimensionInPixels <= targetMaxDimension {
24 | // If the bitmap is small enough already, just return it
25 | return self
26 | }
27 |
28 | let scaleRatio = targetMaxDimension / maxDimensionInPoints
29 | let width = round(size.width * scaleRatio)
30 | let height = round(size.height * scaleRatio)
31 |
32 | UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), true, 1.0)
33 | self.draw(in: CGRect(x: 0.0, y: 0.0, width: width, height: height))
34 | let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
35 | UIGraphicsEndImageContext()
36 |
37 | return scaledImage
38 | }
39 |
40 | internal var pixels: [Int64] {
41 | let image = self.cgImage!
42 |
43 | let pixelsWide = image.width
44 | let pixelsHigh = image.height
45 |
46 | let bitmapBytesPerRow = (pixelsWide * 4)
47 | let bitmapByteCount = (bitmapBytesPerRow * pixelsHigh)
48 |
49 | let colorSpace = CGColorSpaceCreateDeviceRGB()
50 | let bitmapData = malloc(bitmapByteCount)
51 | defer { free(bitmapData) }
52 |
53 | guard let context = CGContext(data: bitmapData, width: pixelsWide, height: pixelsHigh, bitsPerComponent: 8, bytesPerRow: bitmapBytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) else {
54 | fatalError("Unable to create bitmap context")
55 | }
56 |
57 | guard let unconstrainedData = context.data else {
58 | fatalError("Unable to get bitmap data")
59 | }
60 |
61 | context.draw(image, in: CGRect(x: 0.0, y: 0.0, width: CGFloat(pixelsWide), height: CGFloat(pixelsHigh)))
62 |
63 | let data = unconstrainedData.bindMemory(to: UInt8.self, capacity: pixelsWide * pixelsHigh)
64 | var pixels = [Int64]()
65 |
66 | for x in 0 ..< pixelsWide {
67 | for y in 0 ..< pixelsHigh {
68 | let pixelInfo = ((pixelsWide * y) + x) * 4
69 |
70 | let alpha = Int64(data[pixelInfo])
71 | let red = Int64(data[pixelInfo + 1])
72 | let green = Int64(data[pixelInfo + 2])
73 | let blue = Int64(data[pixelInfo + 3])
74 |
75 | pixels.append(HexColor.fromARGB(alpha, red: red, green: green, blue: blue))
76 | }
77 | }
78 |
79 | return pixels
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | 豆瓣电影Flutter版
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | doubanmovie_flutter
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleSignature
22 | ????
23 | CFBundleVersion
24 | 1.1
25 | LSRequiresIPhoneOS
26 |
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 | UIViewControllerBasedStatusBarAppearance
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
--------------------------------------------------------------------------------
/ios/Runner/SwiftPalettePlugin.swift:
--------------------------------------------------------------------------------
1 | import Flutter
2 | import UIKit
3 |
4 | let PALETTE_CHANNEL = "channel:com.postmuseapp.designer/palette";
5 |
6 | public class SwiftPalettePlugin: NSObject, FlutterPlugin {
7 | public static func register(with registrar: FlutterPluginRegistrar) {
8 | let channel = FlutterMethodChannel(name: PALETTE_CHANNEL, binaryMessenger: registrar.messenger())
9 | let instance = SwiftPalettePlugin()
10 | registrar.addMethodCallDelegate(instance, channel: channel)
11 | }
12 |
13 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
14 | let arguments = call.arguments as! Dictionary
15 | let path = arguments["path"] as? String
16 | let url = arguments["url"] as? String
17 | if ("getPalette" == call.method){
18 | self.getPalette(path: path!, result:result)
19 | }else if("getPaletteWithUrl"==call.method){
20 | self.getPaletteWithUrl(url: url!, result: result)
21 | }
22 | }
23 |
24 | func getPalette(path:String, result:@escaping FlutterResult){
25 | getPalette(path: path, result: result, onPalette: {palette in
26 | result(self.convertPalette(palette: palette))
27 | })
28 | }
29 |
30 | func getPaletteWithUrl(url:String,result:@escaping FlutterResult){
31 | getPaletteWithUrl(url: url, result: result,onPalette: {palette in
32 | result(self.convertPalette(palette: palette))
33 | })
34 | }
35 |
36 | func getPalette(path:String, result:FlutterResult, onPalette:@escaping (Palette)->Void){
37 | do {
38 | let imageUrl = URL.init(fileURLWithPath: path)
39 | let imageData = try Data.init(contentsOf: imageUrl)
40 | let image = UIImage.init(data: imageData)!
41 | Palette.generateWith(configuration: PaletteConfiguration(image: image)) {palette in
42 | onPalette(palette)
43 | }
44 | } catch {
45 | print(error)
46 | result(error)
47 | }
48 | }
49 |
50 | func getPaletteWithUrl(url:String,result:FlutterResult,onPalette:@escaping(Palette)->Void){
51 | do{
52 | let imgUrl = URL.init(string: url)
53 | let uiImage = try UIImage.init(data: Data.init(contentsOf: imgUrl!))
54 | Palette.generateWith(configuration: PaletteConfiguration(image: uiImage!)) {palette in
55 | onPalette(palette)
56 | }
57 | }catch{
58 | print(error)
59 | result(error)
60 | }
61 |
62 | }
63 |
64 | func convertPalette(palette: Palette) -> Dictionary {
65 | var paletteMap = Dictionary()
66 | paletteMap["vibrant"] = convertSwatch(swatch:palette.vibrantSwatch)
67 | paletteMap["darkVibrant"] = convertSwatch(swatch:palette.darkVibrantSwatch)
68 | paletteMap["lightVibrant"] = convertSwatch(swatch:palette.lightVibrantSwatch)
69 | paletteMap["muted"] = convertSwatch(swatch:palette.mutedSwatch)
70 | paletteMap["darkMuted"] = convertSwatch(swatch:palette.darkMutedSwatch)
71 | paletteMap["lightMuted"] = convertSwatch(swatch:palette.lightMutedSwatch)
72 | paletteMap["swatches"] = palette.swatches.map{convertSwatch(swatch: $0)}
73 | return paletteMap;
74 | }
75 |
76 | func convertSwatch(swatch: PaletteSwatch?) -> Dictionary? {
77 | if (swatch != nil){
78 | let swatch = swatch!
79 | var swatchMap = Dictionary()
80 | swatchMap["color"] = colorToARGB(color: swatch.color)
81 | swatchMap["population"] = swatch.population
82 | swatchMap["titleTextColor"] = colorToARGB(color: swatch.titleTextColor)
83 | swatchMap["bodyTextColor"] = colorToARGB(color: swatch.bodyTextColor)
84 | swatchMap["swatchInfo"] = swatch.debugDescription
85 | return swatchMap;
86 | } else {
87 | return nil
88 | }
89 | }
90 |
91 | func colorToARGB(color: UIColor?) -> Int?{
92 | if (color == nil){
93 | return nil
94 | }
95 | var red: CGFloat = 0.0
96 | var green: CGFloat = 0.0
97 | var blue: CGFloat = 0.0
98 | var alpha: CGFloat = 0.0
99 | color!.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
100 |
101 | let r = Int(round(red * 255.0))
102 | let g = Int(round(green * 255.0))
103 | let b = Int(round(blue * 255.0))
104 | let a = Int(round(alpha * 255.0))
105 |
106 | return (a << 24) | (r << 16) | (g << 8) | b
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/lib/CustomView.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math' as Math;
2 |
3 | import 'package:flutter/material.dart';
4 |
5 | class ScoreView extends StatelessWidget {
6 | double score;
7 | Size size;
8 | double padding;
9 | double spacing;
10 |
11 | ScoreView(this.padding, this.spacing, this.size, this.score);
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return new CustomPaint(
16 | size: size,
17 | painter: new ScorePainter(score, padding, spacing),
18 | );
19 | }
20 | }
21 |
22 | class ScorePainter extends CustomPainter {
23 | double _score;
24 | double padding = 10.0; //左右两边间距
25 | double spacing = 2.0; // 星星之间间距
26 | Paint _backgroundPaint;
27 | Paint _foregroundPaint;
28 |
29 | ScorePainter(num score, double padding, num spacing) {
30 | this._score = score;
31 | this.padding = padding;
32 | this.spacing = spacing;
33 | init();
34 | }
35 |
36 | void init() {
37 | _backgroundPaint = new Paint();
38 | _backgroundPaint.isAntiAlias = true;
39 | _backgroundPaint.color = Colors.grey;
40 | _backgroundPaint.strokeWidth = 1.0;
41 | _backgroundPaint.style = PaintingStyle.fill;
42 |
43 | _foregroundPaint = new Paint();
44 | _foregroundPaint.isAntiAlias = true;
45 | _foregroundPaint.color = Colors.orange;
46 | _foregroundPaint.strokeWidth = 1.0;
47 | _foregroundPaint.style = PaintingStyle.fill;
48 | }
49 |
50 | @override
51 | void paint(Canvas canvas, Size size) {
52 | double outR = (size.width - 2 * padding - 4 * spacing) / 2 / 5;
53 | double inR = outR * sin(18) / sin(180 - 36 - 18);
54 |
55 | canvas.translate(padding + outR, outR);
56 |
57 | Path path;
58 | for (int i = 0; i < 5; i++) {
59 | if (_score > 2.0) {
60 | // 完整的星星
61 | path = getStarPath(outR, inR, 0.0, 0.0, 0.0);
62 | canvas.drawPath(path, _foregroundPaint);
63 | } else if (_score > 0.0) {
64 | // 不完整的星星
65 | path = getStarPath(outR, inR, 0.0, 0.0, 0.0);
66 | canvas.drawPath(path, _backgroundPaint);
67 |
68 | _foregroundPaint.blendMode = BlendMode.overlay;
69 | Rect rect = new Rect.fromLTWH(-outR, -outR, outR * _score, 2 * outR);
70 | canvas.drawRect(rect, _foregroundPaint);
71 | } else {
72 | // 灰色星星
73 | path = getStarPath(outR, inR, 0.0, 0.0, 0.0);
74 | canvas.drawPath(path, _backgroundPaint);
75 | }
76 | canvas.translate(spacing + outR * 2, 0.0);
77 | _score -= 2.0;
78 | }
79 | }
80 |
81 | @override
82 | bool shouldRepaint(CustomPainter oldDelegate) {
83 | return true;
84 | }
85 |
86 | getStarPath(double R, double r, double x, double y, double rot) {
87 | Path path = new Path();
88 |
89 | path.moveTo(Math.cos((54 + 72 * -1 - rot) / 180 * Math.pi) * r + x,
90 | -Math.sin((54 + 72 * -1 - rot) / 180 * Math.pi) * r + y);
91 |
92 | for (var i = 0; i < 5; i++) {
93 | path.lineTo(Math.cos((18 + 72 * i - rot) / 180 * Math.pi) * R + x,
94 | -Math.sin((18 + 72 * i - rot) / 180 * Math.pi) * R + y);
95 | path.lineTo(Math.cos((54 + 72 * i - rot) / 180 * Math.pi) * r + x,
96 | -Math.sin((54 + 72 * i - rot) / 180 * Math.pi) * r + y);
97 | }
98 | path.close();
99 | return path;
100 | }
101 |
102 | double cos(int num) {
103 | return Math.cos(num * Math.pi / 180);
104 | }
105 |
106 | double sin(int num) {
107 | return Math.sin(num * Math.pi / 180);
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/lib/NestScrollSample.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class Sample extends StatelessWidget {
4 | var _tabs = ["1", "2", "3"];
5 | @override
6 | Widget build(BuildContext context) {
7 | return new DefaultTabController(
8 | length: _tabs.length, // This is the number of tabs.
9 | child: new NestedScrollView(
10 | headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
11 | // These are the slivers that show up in the "outer" scroll view.
12 | return [
13 | new SliverOverlapAbsorber(
14 | // This widget takes the overlapping behavior of the SliverAppBar,
15 | // and redirects it to the SliverOverlapInjector below. If it is
16 | // missing, then it is possible for the nested "inner" scroll view
17 | // below to end up under the SliverAppBar even when the inner
18 | // scroll view thinks it has not been scrolled.
19 | // This is not necessary if the "headerSliverBuilder" only builds
20 | // widgets that do not overlap the next sliver.
21 | handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
22 | child: new SliverAppBar(
23 | title: const Text('Books'), // This is the title in the app bar.
24 | pinned: true,
25 | expandedHeight: 150.0,
26 | // The "forceElevated" property causes the SliverAppBar to show
27 | // a shadow. The "innerBoxIsScrolled" parameter is true when the
28 | // inner scroll view is scrolled beyond its "zero" point, i.e.
29 | // when it appears to be scrolled below the SliverAppBar.
30 | // Without this, there are cases where the shadow would appear
31 | // or not appear inappropriately, because the SliverAppBar is
32 | // not actually aware of the precise position of the inner
33 | // scroll views.
34 | forceElevated: innerBoxIsScrolled,
35 | bottom: new TabBar(
36 | // These are the widgets to put in each tab in the tab bar.
37 | tabs:
38 | _tabs.map((String name) => new Tab(text: name)).toList(),
39 | ),
40 | ),
41 | ),
42 | ];
43 | },
44 | body: new TabBarView(
45 | // These are the contents of the tab views, below the tabs.
46 | children: _tabs.map((String name) {
47 | return new SafeArea(
48 | top: false,
49 | bottom: false,
50 | child: new Builder(
51 | // This Builder is needed to provide a BuildContext that is "inside"
52 | // the NestedScrollView, so that sliverOverlapAbsorberHandleFor() can
53 | // find the NestedScrollView.
54 | builder: (BuildContext context) {
55 | return new CustomScrollView(
56 | // The "controller" and "primary" members should be left
57 | // unset, so that the NestedScrollView can control this
58 | // inner scroll view.
59 | // If the "controller" property is set, then this scroll
60 | // view will not be associated with the NestedScrollView.
61 | // The PageStorageKey should be unique to this ScrollView;
62 | // it allows the list to remember its scroll position when
63 | // the tab view is not on the screen.
64 | key: new PageStorageKey(name),
65 | slivers: [
66 | new SliverOverlapInjector(
67 | // This is the flip side of the SliverOverlapAbsorber above.
68 | handle: NestedScrollView
69 | .sliverOverlapAbsorberHandleFor(context),
70 | ),
71 | new SliverPadding(
72 | padding: const EdgeInsets.all(8.0),
73 | // In this example, the inner scroll view has
74 | // fixed-height list items, hence the use of
75 | // SliverFixedExtentList. However, one could use any
76 | // sliver widget here, e.g. SliverList or SliverGrid.
77 | sliver: new SliverFixedExtentList(
78 | // The items in this example are fixed to 48 pixels
79 | // high. This matches the Material Design spec for
80 | // ListTile widgets.
81 | itemExtent: 48.0,
82 | delegate: new SliverChildBuilderDelegate(
83 | (BuildContext context, int index) {
84 | // This builder is called for each child.
85 | // In this example, we just number each list item.
86 | return new ListTile(
87 | title: new Text('Item $index'),
88 | );
89 | },
90 | // The childCount of the SliverChildBuilderDelegate
91 | // specifies how many children this inner list
92 | // has. In this example, each tab has a list of
93 | // exactly 30 items, but this is arbitrary.
94 | childCount: 30,
95 | ),
96 | ),
97 | ),
98 | ],
99 | );
100 | },
101 | ),
102 | );
103 | }).toList(),
104 | ),
105 | ),
106 | );
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:doubanmovie_flutter/pages/explore_page.dart';
2 | import 'package:doubanmovie_flutter/pages/hotlist_page.dart';
3 | import 'package:doubanmovie_flutter/pages/more_page.dart';
4 | import 'package:flutter/cupertino.dart';
5 | import 'package:flutter/material.dart';
6 |
7 | void main() => runApp(new DoubanMovieClient());
8 |
9 | class DoubanMovieClient extends StatefulWidget {
10 | @override
11 | State createState() {
12 | return new ClientState();
13 | }
14 | }
15 |
16 | class ClientState extends State {
17 | int _tabIndex = 0;
18 | final tabTextStyleNormal = new TextStyle(color: const Color(0xff999999));
19 | final tabTextStyleSelected = new TextStyle(color: const Color(0xff4c4c4c));
20 |
21 | var tabImages;
22 | var _body;
23 | var appBarTitles = ['首页', '发现', '更多'];
24 |
25 | Image getTabImage(path) {
26 | return new Image.asset(path, width: 20.0, height: 20.0);
27 | }
28 |
29 | void initData() {
30 | if (tabImages == null) {
31 | tabImages = [
32 | [
33 | getTabImage('images/icon_hot_normal.png'),
34 | getTabImage('images/icon_hot_selected.png')
35 | ],
36 | [
37 | getTabImage('images/icon_explore_normal.png'),
38 | getTabImage('images/icon_explore_selected.png')
39 | ],
40 | [
41 | getTabImage('images/icon_more_normal.png'),
42 | getTabImage('images/icon_more_selected.png')
43 | ]
44 | ];
45 | }
46 | _body = new IndexedStack(
47 | children: [
48 | new HotListPage(),
49 | new ExplorePage(),
50 | new MorePage(),
51 | new HotListPage()
52 | ],
53 | index: _tabIndex,
54 | );
55 | }
56 |
57 | TextStyle getTabTextStyle(int curIndex) {
58 | if (curIndex == _tabIndex) {
59 | return tabTextStyleSelected;
60 | }
61 | return tabTextStyleNormal;
62 | }
63 |
64 | Image getTabIcon(int curIndex) {
65 | if (curIndex == _tabIndex) {
66 | return tabImages[curIndex][1];
67 | }
68 | return tabImages[curIndex][0];
69 | }
70 |
71 | Text getTabTitle(int curIndex) {
72 | return new Text(appBarTitles[curIndex], style: getTabTextStyle(curIndex));
73 | }
74 |
75 | @override
76 | Widget build(BuildContext context) {
77 | initData();
78 |
79 | final CupertinoTabBar botNavBar = new CupertinoTabBar(
80 | /*
81 | * 在底部导航栏中布置的交互项:迭代存储NavigationIconView类的列表
82 | * 返回此迭代的每个元素的底部导航栏项目
83 | * 创建包含此迭代的元素的列表
84 | */
85 | items: [
86 | new BottomNavigationBarItem(
87 | icon: getTabIcon(0), title: getTabTitle(0)),
88 | new BottomNavigationBarItem(
89 | icon: getTabIcon(1), title: getTabTitle(1)),
90 | new BottomNavigationBarItem(
91 | icon: getTabIcon(2), title: getTabTitle(2)),
92 | ],
93 | // 当前活动项的索引:存储底部导航栏的当前选择
94 | currentIndex: _tabIndex,
95 | // 当点击项目时调用的回调
96 | onTap: (int index) {
97 | setState(() {
98 | _tabIndex = index;
99 | });
100 | });
101 |
102 | return new MaterialApp(
103 | theme: new ThemeData(primaryColor: const Color(0xFF63CA6C)),
104 | home: new Scaffold(
105 | body: _body,
106 | bottomNavigationBar: botNavBar,
107 | ),
108 | );
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/lib/model/MovieActor.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 |
3 | import 'MovieImgs.dart';
4 |
5 | part 'MovieActor.g.dart';
6 |
7 | @JsonSerializable()
8 | class MovieActor extends Object with _$MovieActorSerializerMixin {
9 | String alt;
10 | MovieImgs avatars;
11 | String name;
12 | String id;
13 |
14 | MovieActor(this.alt, this.avatars, this.name, this.id);
15 |
16 | factory MovieActor.fromJson(Map json) =>
17 | _$MovieActorFromJson(json);
18 | }
19 |
--------------------------------------------------------------------------------
/lib/model/MovieActor.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'MovieActor.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | MovieActor _$MovieActorFromJson(Map json) {
10 | return new MovieActor(
11 | json['alt'] as String,
12 | json['avatars'] == null
13 | ? null
14 | : new MovieImgs.fromJson(json['avatars'] as Map),
15 | json['name'] as String,
16 | json['id'] as String);
17 | }
18 |
19 | abstract class _$MovieActorSerializerMixin {
20 | String get alt;
21 | MovieImgs get avatars;
22 | String get name;
23 | String get id;
24 | Map toJson() =>
25 | {'alt': alt, 'avatars': avatars, 'name': name, 'id': id};
26 | }
27 |
--------------------------------------------------------------------------------
/lib/model/MovieDetail.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 |
3 | import 'MovieActor.dart';
4 | import 'MovieImgs.dart';
5 | import 'MovieRate.dart';
6 |
7 | part 'MovieDetail.g.dart';
8 |
9 | @JsonSerializable()
10 | class MovieDetail extends Object with _$MovieDetailSerializerMixin {
11 | MovieRate rating;
12 | int reviews_count;
13 | int wish_count;
14 | String year;
15 | MovieImgs images;
16 | String alt;
17 | String id;
18 | String mobile_url;
19 | String title;
20 | String share_url;
21 | List genres;
22 | int collect_count;
23 | List casts;
24 | String original_title;
25 | String summary;
26 | List directors;
27 | int comments_count;
28 | int ratings_count;
29 |
30 | MovieDetail(
31 | this.rating,
32 | this.reviews_count,
33 | this.wish_count,
34 | this.year,
35 | this.images,
36 | this.alt,
37 | this.id,
38 | this.mobile_url,
39 | this.title,
40 | this.share_url,
41 | this.genres,
42 | this.collect_count,
43 | this.casts,
44 | this.original_title,
45 | this.summary,
46 | this.directors,
47 | this.comments_count,
48 | this.ratings_count);
49 |
50 | factory MovieDetail.fromJson(Map json) =>
51 | _$MovieDetailFromJson(json);
52 | }
53 |
--------------------------------------------------------------------------------
/lib/model/MovieDetail.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'MovieDetail.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | MovieDetail _$MovieDetailFromJson(Map json) {
10 | return new MovieDetail(
11 | json['rating'] == null
12 | ? null
13 | : new MovieRate.fromJson(json['rating'] as Map),
14 | json['reviews_count'] as int,
15 | json['wish_count'] as int,
16 | json['year'] as String,
17 | json['images'] == null
18 | ? null
19 | : new MovieImgs.fromJson(json['images'] as Map),
20 | json['alt'] as String,
21 | json['id'] as String,
22 | json['mobile_url'] as String,
23 | json['title'] as String,
24 | json['share_url'] as String,
25 | (json['genres'] as List)?.map((e) => e as String)?.toList(),
26 | json['collect_count'] as int,
27 | (json['casts'] as List)
28 | ?.map((e) => e == null
29 | ? null
30 | : new MovieActor.fromJson(e as Map))
31 | ?.toList(),
32 | json['original_title'] as String,
33 | json['summary'] as String,
34 | (json['directors'] as List)
35 | ?.map((e) => e == null
36 | ? null
37 | : new MovieActor.fromJson(e as Map))
38 | ?.toList(),
39 | json['comments_count'] as int,
40 | json['ratings_count'] as int);
41 | }
42 |
43 | abstract class _$MovieDetailSerializerMixin {
44 | MovieRate get rating;
45 | int get reviews_count;
46 | int get wish_count;
47 | String get year;
48 | MovieImgs get images;
49 | String get alt;
50 | String get id;
51 | String get mobile_url;
52 | String get title;
53 | String get share_url;
54 | List get genres;
55 | int get collect_count;
56 | List get casts;
57 | String get original_title;
58 | String get summary;
59 | List get directors;
60 | int get comments_count;
61 | int get ratings_count;
62 | Map toJson() => {
63 | 'rating': rating,
64 | 'reviews_count': reviews_count,
65 | 'wish_count': wish_count,
66 | 'year': year,
67 | 'images': images,
68 | 'alt': alt,
69 | 'id': id,
70 | 'mobile_url': mobile_url,
71 | 'title': title,
72 | 'share_url': share_url,
73 | 'genres': genres,
74 | 'collect_count': collect_count,
75 | 'casts': casts,
76 | 'original_title': original_title,
77 | 'summary': summary,
78 | 'directors': directors,
79 | 'comments_count': comments_count,
80 | 'ratings_count': ratings_count
81 | };
82 | }
83 |
--------------------------------------------------------------------------------
/lib/model/MovieImgs.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 |
3 | part 'MovieImgs.g.dart';
4 |
5 | @JsonSerializable()
6 | class MovieImgs extends Object with _$MovieImgsSerializerMixin {
7 | String small;
8 | String large;
9 | String medium;
10 |
11 | MovieImgs(this.small, this.large, this.medium);
12 |
13 | factory MovieImgs.fromJson(Map json) =>
14 | _$MovieImgsFromJson(json);
15 | }
16 |
--------------------------------------------------------------------------------
/lib/model/MovieImgs.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'MovieImgs.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | MovieImgs _$MovieImgsFromJson(Map json) {
10 | return new MovieImgs(json['small'] as String, json['large'] as String,
11 | json['medium'] as String);
12 | }
13 |
14 | abstract class _$MovieImgsSerializerMixin {
15 | String get small;
16 | String get large;
17 | String get medium;
18 | Map toJson() =>
19 | {'small': small, 'large': large, 'medium': medium};
20 | }
21 |
--------------------------------------------------------------------------------
/lib/model/MovieIntro.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 |
3 | import 'MovieActor.dart';
4 | import 'MovieImgs.dart';
5 | import 'MovieRate.dart';
6 |
7 | part 'MovieIntro.g.dart';
8 |
9 | ///电影概要信息
10 | @JsonSerializable()
11 | class MovieIntro extends Object with _$MovieIntroSerializerMixin {
12 | MovieRate rating;
13 | List genres;
14 | String title;
15 | List casts;
16 | int collect_count;
17 | String original_title;
18 | String subtype;
19 | List directors;
20 | String year;
21 | MovieImgs images;
22 | String alt;
23 | String id;
24 |
25 | MovieIntro(
26 | this.rating,
27 | this.genres,
28 | this.title,
29 | this.casts,
30 | this.collect_count,
31 | this.original_title,
32 | this.subtype,
33 | this.directors,
34 | this.year,
35 | this.images,
36 | this.alt,
37 | this.id);
38 |
39 | factory MovieIntro.fromJson(Map json) =>
40 | _$MovieIntroFromJson(json);
41 | }
42 |
--------------------------------------------------------------------------------
/lib/model/MovieIntro.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'MovieIntro.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | MovieIntro _$MovieIntroFromJson(Map json) {
10 | return new MovieIntro(
11 | json['rating'] == null
12 | ? null
13 | : new MovieRate.fromJson(json['rating'] as Map),
14 | (json['genres'] as List)?.map((e) => e as String)?.toList(),
15 | json['title'] as String,
16 | (json['casts'] as List)
17 | ?.map((e) => e == null
18 | ? null
19 | : new MovieActor.fromJson(e as Map))
20 | ?.toList(),
21 | json['collect_count'] as int,
22 | json['original_title'] as String,
23 | json['subtype'] as String,
24 | (json['directors'] as List)
25 | ?.map((e) => e == null
26 | ? null
27 | : new MovieActor.fromJson(e as Map))
28 | ?.toList(),
29 | json['year'] as String,
30 | json['images'] == null
31 | ? null
32 | : new MovieImgs.fromJson(json['images'] as Map),
33 | json['alt'] as String,
34 | json['id'] as String);
35 | }
36 |
37 | abstract class _$MovieIntroSerializerMixin {
38 | MovieRate get rating;
39 | List get genres;
40 | String get title;
41 | List get casts;
42 | int get collect_count;
43 | String get original_title;
44 | String get subtype;
45 | List get directors;
46 | String get year;
47 | MovieImgs get images;
48 | String get alt;
49 | String get id;
50 | Map toJson() => {
51 | 'rating': rating,
52 | 'genres': genres,
53 | 'title': title,
54 | 'casts': casts,
55 | 'collect_count': collect_count,
56 | 'original_title': original_title,
57 | 'subtype': subtype,
58 | 'directors': directors,
59 | 'year': year,
60 | 'images': images,
61 | 'alt': alt,
62 | 'id': id
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/lib/model/MovieIntroList.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 |
3 | import 'MovieIntro.dart';
4 |
5 | part 'MovieIntroList.g.dart';
6 |
7 | @JsonSerializable()
8 | class MovieIntroList extends Object with _$MovieIntroListSerializerMixin {
9 | List subjects;
10 |
11 | MovieIntroList(this.subjects);
12 |
13 | factory MovieIntroList.fromJson(Map json) =>
14 | _$MovieIntroListFromJson(json);
15 | }
16 |
--------------------------------------------------------------------------------
/lib/model/MovieIntroList.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'MovieIntroList.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | MovieIntroList _$MovieIntroListFromJson(Map json) {
10 | return new MovieIntroList((json['subjects'] as List)
11 | ?.map((e) =>
12 | e == null ? null : new MovieIntro.fromJson(e as Map))
13 | ?.toList());
14 | }
15 |
16 | abstract class _$MovieIntroListSerializerMixin {
17 | List get subjects;
18 | Map toJson() => {'subjects': subjects};
19 | }
20 |
--------------------------------------------------------------------------------
/lib/model/MovieRate.dart:
--------------------------------------------------------------------------------
1 | import 'package:json_annotation/json_annotation.dart';
2 |
3 | part 'MovieRate.g.dart';
4 |
5 | @JsonSerializable()
6 | class MovieRate extends Object with _$MovieRateSerializerMixin {
7 | double max;
8 | double average;
9 | String stars;
10 | double min;
11 |
12 | MovieRate(this.max, this.average, this.stars, this.min);
13 |
14 | factory MovieRate.fromJson(Map json) =>
15 | _$MovieRateFromJson(json);
16 | }
17 |
--------------------------------------------------------------------------------
/lib/model/MovieRate.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'MovieRate.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | MovieRate _$MovieRateFromJson(Map json) {
10 | return new MovieRate(
11 | (json['max'] as num)?.toDouble(),
12 | (json['average'] as num)?.toDouble(),
13 | json['stars'] as String,
14 | (json['min'] as num)?.toDouble());
15 | }
16 |
17 | abstract class _$MovieRateSerializerMixin {
18 | double get max;
19 | double get average;
20 | String get stars;
21 | double get min;
22 | Map toJson() => {
23 | 'max': max,
24 | 'average': average,
25 | 'stars': stars,
26 | 'min': min
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/lib/net/DateSource.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:html/dom.dart';
4 | import 'package:html/parser.dart' show parse;
5 | import 'package:http/http.dart' as http;
6 |
7 | void main() {
8 | new DataSource().getHotFilmCritics();
9 | }
10 |
11 | class DataSource {
12 | void a() {
13 | http
14 | .get('https://movie.douban.com/subject/26804147/photos?type=R')
15 | .then((http.Response response) {
16 | var document = parse(response.body.toString());
17 | String imgId = document
18 | .getElementsByClassName('poster-col3 clearfix')[0]
19 | .getElementsByTagName('li')[0]
20 | .attributes['data-id']
21 | .toString();
22 | print("https://img1.doubanio.com/view/photo/l/public/p" + imgId + ".jpg");
23 | });
24 | }
25 |
26 | void getHdCover() {
27 | Map hdImgMap;
28 | http
29 | .get('https://movie.douban.com/cinema/nowplaying/beijing/')
30 | .then((http.Response response) {
31 | var document = parse(response.body.toString());
32 | List items = document.getElementsByClassName('list-item');
33 | hdImgMap = new Map();
34 | for (var item in items) {
35 | var url =
36 | item.getElementsByTagName('img')[0].attributes['src'].toString();
37 | String movieId = item.attributes['data-subject'].toString();
38 |
39 | RegExp exp = new RegExp('public\/p.+\.jpg');
40 | String s = exp.firstMatch(url).group(0);
41 |
42 | String imgId = s.substring(8, 18);
43 | String imgUrl =
44 | "https://img1.doubanio.com/view/photo/l/public/p" + imgId + ".jpg";
45 | print(imgUrl);
46 | hdImgMap.addAll({movieId: imgUrl});
47 | }
48 | });
49 | }
50 |
51 | Future getNewList() async {
52 | List