diff --git a/platform_design/.gitignore b/platform_design/.gitignore new file mode 100644 index 00000000000..07488ba61ac --- /dev/null +++ b/platform_design/.gitignore @@ -0,0 +1,70 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/platform_design/.metadata b/platform_design/.metadata new file mode 100644 index 00000000000..405f45dafa3 --- /dev/null +++ b/platform_design/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 8bea3fb2ebadc3933b6b213483d2d4379ac53a5c + channel: master + +project_type: app diff --git a/platform_design/README.md b/platform_design/README.md new file mode 100644 index 00000000000..441edea73c9 --- /dev/null +++ b/platform_design/README.md @@ -0,0 +1,104 @@ +# Platform Design + +Instead of transliterating widgets one by one between Cupertino and Material, +Android and iOS apps often follow different information architecture patterns +that require some design decisions. + +This sample project shows a Flutter app that maximizes application code reuse +while adhering to different design patterns on Android and iOS. On +Android, it uses Material's [lateral navigation](https://material.io/design/navigation/understanding-navigation.html#types-of-navigation) +based on a drawer and on iOS, it adheres to Apple Human Interface Guideline's +[flat navigation](https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/navigation/) +by using a bottom tab bar. + +Visually, the app presents platform-agnostic content surrounded by +platform-specific 'chrome'. + +# Preview + +![App's platform toggling preview](adaptive-overview.gif) + +See https://youtu.be/svhbbFZg1IA for a longer non-gif format. + +# Features + +## Home + +Defines the top level navigation structure of the app and shows the contents +of the songs tab on launch. + +### Android + +* Uses the drawer paradigm on the root page. + +### iOS + +* Uses bottom tab bars with parallel navigation stacks. + +## Songs feed tab + +Shows platform-agnostic cards that is tappable and that performs a hero +transition on top of the platform native page transitions. + +Both platforms also show a button in their app/nav bar to toggle the platform. + +### Android + +* Android uses a static pull-to-refresh pattern with an additional refresh +button in the app bar. +* The song details page must be popped in order to change tabs on Android. + +### iOS + +* The iOS songs tab uses a scrollable iOS 11 large title style navigation bar. +* iOS uses an overscrolling pull-to-refresh pattern. +* On iOS, parallel tabs are always accessible and the songs tab's navigation +stack is preserved when changing tabs. + +## News Tab + +Shows platform-agnostic news boxes. + +### Android + +* The news tab always appears on top of the songs tab when summoned from the +drawer. + +### iOS + +* The news tab appears instead of the songs tab on iOS when switching tabs from +the tab bar. + +## Profile Tab + +Shows a number of user preferences. + +### Android + +* The profile tab appears on top of the songs tab on Android. +* Has tappable preference cards which shows a multiple-choice dialog on Android. +* The log out button shows a 2 button dialog on Android. + +### iOS + +* The profile tab appears instead of the songs tab on iOS. +* Has tappable preference cards which shows a picker on iOS. +* The log out button shows a 3 choice action sheet on iOS. + +## Settings Tab + +Shows a number of app settings via Material switches which auto adapt to the +platform. + +### Android + +* The settings is directly available in the drawer on Android since a Material +Design drawer can fit many tabs. + +### iOS + +* The settings is accessible from a button inside the profile tab's nav bar on +iOS. This is a common pattern since there are conventionally more items in the +drawer than there are tabs. +* On iOS, the settings page is shown as a full screen dialog instead of a tab +in the tab scaffold. diff --git a/platform_design/adaptive-overview.gif b/platform_design/adaptive-overview.gif new file mode 100644 index 00000000000..83251984b1e Binary files /dev/null and b/platform_design/adaptive-overview.gif differ diff --git a/platform_design/android/app/build.gradle b/platform_design/android/app/build.gradle new file mode 100644 index 00000000000..b07e1e62960 --- /dev/null +++ b/platform_design/android/app/build.gradle @@ -0,0 +1,61 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 28 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.platform_design" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} diff --git a/platform_design/android/app/src/debug/AndroidManifest.xml b/platform_design/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000000..dcbbd9596e0 --- /dev/null +++ b/platform_design/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/platform_design/android/app/src/main/AndroidManifest.xml b/platform_design/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..a45b5113719 --- /dev/null +++ b/platform_design/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/platform_design/android/app/src/main/java/com/example/platform_design/MainActivity.java b/platform_design/android/app/src/main/java/com/example/platform_design/MainActivity.java new file mode 100644 index 00000000000..c7b74607272 --- /dev/null +++ b/platform_design/android/app/src/main/java/com/example/platform_design/MainActivity.java @@ -0,0 +1,13 @@ +package com.example.platform_design; + +import android.os.Bundle; +import io.flutter.app.FlutterActivity; +import io.flutter.plugins.GeneratedPluginRegistrant; + +public class MainActivity extends FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + GeneratedPluginRegistrant.registerWith(this); + } +} diff --git a/platform_design/android/app/src/main/res/drawable/launch_background.xml b/platform_design/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000000..304732f8842 --- /dev/null +++ b/platform_design/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/platform_design/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/platform_design/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000000..db77bb4b7b0 Binary files /dev/null and b/platform_design/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/platform_design/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/platform_design/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000000..17987b79bb8 Binary files /dev/null and b/platform_design/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/platform_design/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/platform_design/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000000..09d4391482b Binary files /dev/null and b/platform_design/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/platform_design/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/platform_design/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000000..d5f1c8d34e7 Binary files /dev/null and b/platform_design/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/platform_design/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/platform_design/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000000..4d6372eebdb Binary files /dev/null and b/platform_design/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/platform_design/android/app/src/main/res/values/styles.xml b/platform_design/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000000..00fa4417cfb --- /dev/null +++ b/platform_design/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/platform_design/android/app/src/profile/AndroidManifest.xml b/platform_design/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000000..dcbbd9596e0 --- /dev/null +++ b/platform_design/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/platform_design/android/build.gradle b/platform_design/android/build.gradle new file mode 100644 index 00000000000..bb8a303898c --- /dev/null +++ b/platform_design/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.2.1' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/platform_design/android/gradle.properties b/platform_design/android/gradle.properties new file mode 100644 index 00000000000..8bd86f68051 --- /dev/null +++ b/platform_design/android/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx1536M diff --git a/platform_design/android/gradle/wrapper/gradle-wrapper.properties b/platform_design/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..2819f022f1f --- /dev/null +++ b/platform_design/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/platform_design/android/settings.gradle b/platform_design/android/settings.gradle new file mode 100644 index 00000000000..5a2f14fb18f --- /dev/null +++ b/platform_design/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/platform_design/ios/Flutter/AppFrameworkInfo.plist b/platform_design/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000000..9367d483e44 --- /dev/null +++ b/platform_design/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/platform_design/ios/Flutter/Debug.xcconfig b/platform_design/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000000..592ceee85b8 --- /dev/null +++ b/platform_design/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/platform_design/ios/Flutter/Release.xcconfig b/platform_design/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000000..592ceee85b8 --- /dev/null +++ b/platform_design/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/platform_design/ios/Runner.xcodeproj/project.pbxproj b/platform_design/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..bfefb2bc826 --- /dev/null +++ b/platform_design/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,506 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B80C3931E831B6300D905FE /* App.framework */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEBA1CF902C7004384FC /* Flutter.framework */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0910; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = S8QB4VV633; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.platformDesign; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.platformDesign; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.platformDesign; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/platform_design/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/platform_design/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000000..1d526a16ed0 --- /dev/null +++ b/platform_design/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/platform_design/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/platform_design/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000000..786d6aad545 --- /dev/null +++ b/platform_design/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform_design/ios/Runner.xcworkspace/contents.xcworkspacedata b/platform_design/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000000..1d526a16ed0 --- /dev/null +++ b/platform_design/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/platform_design/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/platform_design/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000000..18d981003d6 --- /dev/null +++ b/platform_design/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/platform_design/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/platform_design/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000000..949b6789820 --- /dev/null +++ b/platform_design/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + BuildSystemType + Original + + diff --git a/platform_design/ios/Runner/AppDelegate.h b/platform_design/ios/Runner/AppDelegate.h new file mode 100644 index 00000000000..36e21bbf9cf --- /dev/null +++ b/platform_design/ios/Runner/AppDelegate.h @@ -0,0 +1,6 @@ +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/platform_design/ios/Runner/AppDelegate.m b/platform_design/ios/Runner/AppDelegate.m new file mode 100644 index 00000000000..59a72e90be1 --- /dev/null +++ b/platform_design/ios/Runner/AppDelegate.m @@ -0,0 +1,13 @@ +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..d36b1fab2d9 --- /dev/null +++ b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000000..3d43d11e66f Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000000..28c6bf03016 Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000000..2ccbfd967d9 Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000000..f091b6b0bca Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000000..4cde12118dd Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000000..d0ef06e7edb Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000000..dcdc2306c28 Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000000..2ccbfd967d9 Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000000..c8f9ed8f5ce Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000000..a6d6b8609df Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000000..a6d6b8609df Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000000..75b2d164a5a Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000000..c4df70d39da Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000000..6a84f41e14e Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000000..d0e1f585360 Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000000..0bedcf2fd46 --- /dev/null +++ b/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000000..9da19eacad3 Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000000..9da19eacad3 Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000000..9da19eacad3 Binary files /dev/null and b/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000000..89c2725b70f --- /dev/null +++ b/platform_design/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +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. \ No newline at end of file diff --git a/platform_design/ios/Runner/Base.lproj/LaunchScreen.storyboard b/platform_design/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000000..185aac4585a --- /dev/null +++ b/platform_design/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform_design/ios/Runner/Base.lproj/Main.storyboard b/platform_design/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000000..f3c28516fb3 --- /dev/null +++ b/platform_design/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform_design/ios/Runner/Info.plist b/platform_design/ios/Runner/Info.plist new file mode 100644 index 00000000000..9cca781c643 --- /dev/null +++ b/platform_design/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + platform_design + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/platform_design/ios/Runner/main.m b/platform_design/ios/Runner/main.m new file mode 100644 index 00000000000..dff6597e451 --- /dev/null +++ b/platform_design/ios/Runner/main.m @@ -0,0 +1,9 @@ +#import +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/platform_design/lib/main.dart b/platform_design/lib/main.dart new file mode 100644 index 00000000000..5752cacf51b --- /dev/null +++ b/platform_design/lib/main.dart @@ -0,0 +1,186 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +import 'songs_tab.dart'; +import 'news_tab.dart'; +import 'profile_tab.dart'; +import 'settings_tab.dart'; +import 'widgets.dart'; + +void main() => runApp(MyAdaptingApp()); + +class MyAdaptingApp extends StatelessWidget { + @override + Widget build(context) { + // Change this value to better see animations. + timeDilation = 1; + // Either Material or Cupertino widgets work in either Material or Cupertino + // Apps. + return MaterialApp( + title: 'Adaptive Music App', + theme: ThemeData( + // Use the green theme for Material widgets. + primarySwatch: Colors.green, + ), + builder: (context, child) { + return CupertinoTheme( + // Instead of letting Cupertino widgets auto-adapt to the Material + // theme (which is green), this app will use a different theme + // for Cupertino (which is blue by default). + data: CupertinoThemeData(), + child: Material(child: child), + ); + }, + home: PlatformAdaptingHomePage(), + ); + } +} + +// Shows a different type of scaffold depending on the platform. +// +// This file has the most amount of non-sharable code since it behaves the most +// differently between the platforms. +// +// These differences are also subjective and have more than one 'right' answer +// depending on the app and content. +class PlatformAdaptingHomePage extends StatefulWidget { + @override + _PlatformAdaptingHomePageState createState() => _PlatformAdaptingHomePageState(); +} + +class _PlatformAdaptingHomePageState extends State { + // This app keeps a global key for the songs tab because it owns a bunch of + // data. Since changing platform reparents those tabs into different + // scaffolds, keeping a global key to it lets this app keep that tab's data as + // the platform toggles. + // + // This isn't needed for apps that doesn't toggle platforms while running. + final songsTabKey = GlobalKey(); + + // In Material, this app uses the hamburger menu paradigm and flatly lists + // all 4 possible tabs. This drawer is injected into the songs tab which is + // actually building the scaffold around the drawer. + Widget _buildAndroidHomePage(context) { + return SongsTab( + key: songsTabKey, + androidDrawer: _AndroidDrawer(), + ); + } + + // On iOS, the app uses a bottom tab paradigm. Here, each tab view sits inside + // a tab in the tab scaffold. The tab scaffold also positions the tab bar + // in a row at the bottom. + // + // An important thing to note is that while a Material Drawer can display a + // large number of items, a tab bar cannot. To illustrate one way of adjusting + // for this, the app folds its fourth tab (the settings page) into the + // third tab. This is a common pattern on iOS. + Widget _buildIosHomePage(context) { + return CupertinoTabScaffold( + tabBar: CupertinoTabBar( + items: [ + BottomNavigationBarItem(title: Text(SongsTab.title), icon: SongsTab.iosIcon), + BottomNavigationBarItem(title: Text(NewsTab.title), icon: NewsTab.iosIcon), + BottomNavigationBarItem(title: Text(ProfileTab.title), icon: ProfileTab.iosIcon), + ], + ), + tabBuilder: (context, index) { + switch (index) { + case 0: + return CupertinoTabView( + defaultTitle: SongsTab.title, + builder: (context) => SongsTab(key: songsTabKey), + ); + case 1: + return CupertinoTabView( + defaultTitle: NewsTab.title, + builder: (context) => NewsTab(), + ); + case 2: + return CupertinoTabView( + defaultTitle: ProfileTab.title, + builder: (context) => ProfileTab(), + ); + default: + assert(false, 'Unexpected tab'); + return null; + } + }, + ); + } + + @override + Widget build(context) { + return PlatformWidget( + androidBuilder: _buildAndroidHomePage, + iosBuilder: _buildIosHomePage, + ); + } +} + +class _AndroidDrawer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Drawer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + DrawerHeader( + decoration: BoxDecoration(color: Colors.green), + child: Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Icon( + Icons.account_circle, + color: Colors.green.shade800, + size: 96, + ), + ), + ), + ListTile( + leading: SongsTab.androidIcon, + title: Text(SongsTab.title), + onTap: () { + Navigator.pop(context); + }, + ), + ListTile( + leading: NewsTab.androidIcon, + title: Text(NewsTab.title), + onTap: () { + Navigator.pop(context); + Navigator.push(context, MaterialPageRoute( + builder: (context) => NewsTab() + )); + }, + ), + ListTile( + leading: ProfileTab.androidIcon, + title: Text(ProfileTab.title), + onTap: () { + Navigator.pop(context); + Navigator.push(context, MaterialPageRoute( + builder: (context) => ProfileTab() + )); + }, + ), + // Long drawer contents are often segmented. + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Divider(), + ), + ListTile( + leading: SettingsTab.androidIcon, + title: Text(SettingsTab.title), + onTap: () { + Navigator.pop(context); + Navigator.push(context, MaterialPageRoute( + builder: (context) => SettingsTab() + )); + }, + ), + ], + ), + ); + } +} diff --git a/platform_design/lib/news_tab.dart b/platform_design/lib/news_tab.dart new file mode 100644 index 00000000000..f567de84e9c --- /dev/null +++ b/platform_design/lib/news_tab.dart @@ -0,0 +1,126 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_lorem/flutter_lorem.dart'; + +import 'utils.dart'; +import 'widgets.dart'; + +class NewsTab extends StatefulWidget { + static const title = 'News'; + static const androidIcon = Icon(Icons.library_books); + static const iosIcon = Icon(CupertinoIcons.news); + + @override + _NewsTabState createState() => _NewsTabState(); +} + +class _NewsTabState extends State { + static const _itemsLength = 20; + + List colors; + List titles; + List contents; + + @override + void initState() { + colors = getRandomColors(_itemsLength); + titles = List.generate(_itemsLength, (index) => generateRandomHeadline()); + contents = List.generate(_itemsLength, (index) => lorem(paragraphs: 1, words: 24)); + super.initState(); + } + + Widget _listBuilder(context, index) { + if (index >= _itemsLength) + return null; + + return SafeArea( + top: false, + bottom: false, + child: Card( + elevation: 1.5, + margin: EdgeInsets.fromLTRB(6, 12, 6, 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + child: InkWell( + // Make it splash on Android. It would happen automatically if this + // was a real card but this is just a demo. Skip the splash on iOS. + onTap: defaultTargetPlatform == TargetPlatform.iOS ? null : () {}, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + backgroundColor: colors[index], + child: Text( + titles[index].substring(0, 1), + style: TextStyle(color: Colors.white), + ), + ), + Padding(padding: EdgeInsets.only(left: 16)), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + titles[index], + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ), + ), + Padding(padding: EdgeInsets.only(top: 8)), + Text( + contents[index], + ), + ], + ), + ), + ], + ), + ), + ) + ), + ); + } + + // =========================================================================== + // Non-shared code below because this tab uses different scaffolds. + // =========================================================================== + + Widget _buildAndroid(context) { + return Scaffold( + appBar: AppBar( + title: Text(NewsTab.title), + ), + body: Container( + color: Colors.grey[100], + child: ListView.builder( + itemBuilder: _listBuilder, + ), + ), + ); + } + + Widget _buildIos(context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar(), + child: Container( + color: Colors.grey[100], + child: ListView.builder( + itemBuilder: _listBuilder, + ), + ), + ); + } + + @override + Widget build(context) { + return PlatformWidget( + androidBuilder: _buildAndroid, + iosBuilder: _buildIos, + ); + } +} diff --git a/platform_design/lib/profile_tab.dart b/platform_design/lib/profile_tab.dart new file mode 100644 index 00000000000..1b414ad3398 --- /dev/null +++ b/platform_design/lib/profile_tab.dart @@ -0,0 +1,243 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'settings_tab.dart'; +import 'widgets.dart'; + +class ProfileTab extends StatelessWidget { + static const title = 'Profile'; + static const androidIcon = Icon(Icons.person); + static const iosIcon = Icon(CupertinoIcons.profile_circled); + + Widget _buildBody(context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(8), + child: Center( + child: Text('😼', style: TextStyle( + fontSize: 80, + decoration: TextDecoration.none, + )), + ), + ), + PreferenceCard( + header: 'MY INTENSITY PREFERENCE', + content: 'πŸ”₯', + preferenceChoices: [ + 'Super heavy', + 'Dial it to 11', + "Head bangin'", + '1000W', + 'My neighbor hates me', + ], + ), + PreferenceCard( + header: 'CURRENT MOOD', + content: 'πŸ€˜πŸΎπŸš€', + preferenceChoices: [ + 'Over the moon', + 'Basking in sunlight', + 'Hello fellow Martians', + 'Into the darkness', + ], + ), + Expanded( + child: Container(), + ), + LogOutButton(), + ], + ), + ), + ); + } + + // =========================================================================== + // Non-shared code below because on iOS, the settings tab is nested inside of + // the profile tab as a button in the nav bar. + // =========================================================================== + + Widget _buildAndroid(context) { + return Scaffold( + appBar: AppBar( + title: Text(title), + ), + body: _buildBody(context), + ); + } + + Widget _buildIos(context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + trailing: CupertinoButton( + padding: EdgeInsets.zero, + child: SettingsTab.iosIcon, + onPressed: () { + // This pushes the settings page as a full page modal dialog on top + // of the tab bar and everything. + Navigator.of(context, rootNavigator: true).push( + CupertinoPageRoute( + title: SettingsTab.title, + fullscreenDialog: true, + builder: (context) => SettingsTab(), + ), + ); + }, + ), + ), + child: _buildBody(context), + ); + } + + @override + Widget build(context) { + return PlatformWidget( + androidBuilder: _buildAndroid, + iosBuilder: _buildIos, + ); + } +} + +class PreferenceCard extends StatelessWidget { + const PreferenceCard({ this.header, this.content, this.preferenceChoices }); + + final String header; + final String content; + final List preferenceChoices; + + @override + Widget build(context) { + return PressableCard( + color: Colors.green, + flattenAnimation: AlwaysStoppedAnimation(0), + child: Stack( + children: [ + Container( + height: 120, + width: 250, + child: Padding( + padding: EdgeInsets.only(top: 40), + child: Center( + child: Text( + content, + style: TextStyle(fontSize: 48), + ), + ), + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + color: Colors.black12, + height: 40, + padding: EdgeInsets.only(left: 12), + alignment: Alignment.centerLeft, + child: Text( + header, + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + onPressed: () { + showChoices(context, preferenceChoices); + }, + ); + } +} + +class LogOutButton extends StatelessWidget { + static const _logoutMessage = Text('You may check out any time you like, but you can never leave'); + + // =========================================================================== + // Non-shared code below because this tab shows different interfaces. On + // Android, it's showing an alert dialog with 2 buttons and on iOS, + // it's showing an action sheet with 3 choices. + // + // This is a design choice and you may want to do something different in your + // app. + // =========================================================================== + + Widget _buildAndroid(context) { + return RaisedButton( + child: Text('LOG OUT', style: TextStyle(color: Colors.red)), + onPressed: () { + // You should do something with the result of the dialog prompt in a + // real app but this is just a demo. + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text('Log out?'), + content: _logoutMessage, + actions: [ + FlatButton( + child: const Text('Go back'), + onPressed: () => Navigator.pop(context) , + ), + FlatButton( + child: const Text('Cancel'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } + ); + }, + ); + } + + Widget _buildIos(context) { + return CupertinoButton( + color: CupertinoColors.destructiveRed, + child: Text('Log out'), + onPressed: () { + // You should do something with the result of the action sheet prompt + // in a real app but this is just a demo. + showCupertinoModalPopup( + context: context, + builder: (context) { + return CupertinoActionSheet( + title: Text('Log out?'), + message: _logoutMessage, + actions: [ + CupertinoActionSheetAction( + child: const Text('Reprogram the night man'), + isDestructiveAction: true, + onPressed: () => Navigator.pop(context), + ), + CupertinoActionSheetAction( + child: const Text('Go back'), + onPressed: () => Navigator.pop(context), + ), + ], + cancelButton: CupertinoActionSheetAction( + child: const Text('Cancel'), + isDefaultAction: true, + onPressed: () => Navigator.pop(context), + ), + ); + }, + ); + }, + ); + } + + @override + Widget build(context) { + return PlatformWidget( + androidBuilder: _buildAndroid, + iosBuilder: _buildIos, + ); + } +} diff --git a/platform_design/lib/settings_tab.dart b/platform_design/lib/settings_tab.dart new file mode 100644 index 00000000000..643e9d727cb --- /dev/null +++ b/platform_design/lib/settings_tab.dart @@ -0,0 +1,104 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'widgets.dart'; + +class SettingsTab extends StatefulWidget { + static const title = 'Settings'; + static const androidIcon = Icon(Icons.settings); + static const iosIcon = Icon(CupertinoIcons.gear); + + @override + _SettingsTabState createState() => _SettingsTabState(); +} + +class _SettingsTabState extends State { + var switch1 = false; var switch2 = true; var switch3 = true; var switch4 = true; + var switch5 = true; var switch6 = false; var switch7 = true; + + Widget _buildList() { + return ListView( + children: [ + Padding(padding: EdgeInsets.only(top: 24)), + ListTile( + title: Text('Send me marketing emails'), + // The Material switch has a platform adaptive constructor. + trailing: Switch.adaptive( + value: switch1, + onChanged: (value) => setState(() => switch1 = value), + ), + ), + ListTile( + title: Text('Enable notifications'), + trailing: Switch.adaptive( + value: switch2, + onChanged: (value) => setState(() => switch2 = value), + ), + ), + ListTile( + title: Text('Remind me to rate this app'), + trailing: Switch.adaptive( + value: switch3, + onChanged: (value) => setState(() => switch3 = value), + ), + ), + ListTile( + title: Text('Background song refresh'), + trailing: Switch.adaptive( + value: switch4, + onChanged: (value) => setState(() => switch4 = value), + ), + ), + ListTile( + title: Text('Recommend me songs based on my location'), + trailing: Switch.adaptive( + value: switch5, + onChanged: (value) => setState(() => switch5 = value), + ), + ), + ListTile( + title: Text('Auto-transition playback to cast devices'), + trailing: Switch.adaptive( + value: switch6, + onChanged: (value) => setState(() => switch6 = value), + ), + ), + ListTile( + title: Text('Find friends from my contact list'), + trailing: Switch.adaptive( + value: switch7, + onChanged: (value) => setState(() => switch7 = value), + ), + ), + ], + ); + } + + // =========================================================================== + // Non-shared code below because this tab uses different scaffolds. + // =========================================================================== + + Widget _buildAndroid(context) { + return Scaffold( + appBar: AppBar( + title: Text(SettingsTab.title), + ), + body: _buildList(), + ); + } + + Widget _buildIos(context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar(), + child: _buildList(), + ); + } + + @override + Widget build(context) { + return PlatformWidget( + androidBuilder: _buildAndroid, + iosBuilder: _buildIos, + ); + } +} diff --git a/platform_design/lib/song_detail_tab.dart b/platform_design/lib/song_detail_tab.dart new file mode 100644 index 00000000000..97c21799102 --- /dev/null +++ b/platform_design/lib/song_detail_tab.dart @@ -0,0 +1,99 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import 'widgets.dart'; + +// Page shown when a card in the songs tab is tapped. +// +// On Android, this page sits at the top of your app. On iOS, this page is on +// top of the songs tab's content but is below the tab bar itself. +class SongDetailTab extends StatelessWidget { + const SongDetailTab({ this.id, this.song, this.color }); + + final int id; + final String song; + final Color color; + + Widget _buildBody() { + return SafeArea( + bottom: false, + left: false, + right: false, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Hero( + tag: id, + child: HeroAnimatingSongCard( + song: song, + color: color, + heroAnimation: AlwaysStoppedAnimation(1), + ), + // This app uses a flightShuttleBuilder to specify the exact widget + // to build while the hero transition is mid-flight. + // + // It could either be specified here or in SongsTab. + flightShuttleBuilder: (context, animation, flightDirection, fromHeroContext, toHeroContext) { + return HeroAnimatingSongCard( + song: song, + color: color, + heroAnimation: animation, + ); + }, + ), + Divider( + height: 0, + color: Colors.grey, + ), + Expanded( + child: ListView.builder( + itemCount: 10, + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.only(left: 15, top: 16, bottom: 8), + child: Text('You might also like:', style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + )), + ); + } + // Just a bunch of boxes that simulates loading song choices. + return SongPlaceholderTile(); + }, + ), + ), + ], + ), + ); + } + + // =========================================================================== + // Non-shared code below because we're using different scaffolds. + // =========================================================================== + + Widget _buildAndroid(context) { + return Scaffold( + appBar: AppBar(title: Text(song)), + body: _buildBody(), + ); + } + + Widget _buildIos(context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(song), + previousPageTitle: 'Songs', + ), + child: _buildBody(), + ); + } + + @override + Widget build(context) { + return PlatformWidget( + androidBuilder: _buildAndroid, + iosBuilder: _buildIos, + ); + } +} diff --git a/platform_design/lib/songs_tab.dart b/platform_design/lib/songs_tab.dart new file mode 100644 index 00000000000..835cfaec915 --- /dev/null +++ b/platform_design/lib/songs_tab.dart @@ -0,0 +1,168 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'song_detail_tab.dart'; +import 'utils.dart'; +import 'widgets.dart'; + +class SongsTab extends StatefulWidget { + static const title = 'Songs'; + static const androidIcon = Icon(Icons.music_note); + static const iosIcon = Icon(CupertinoIcons.music_note); + + const SongsTab({ Key key, this.androidDrawer }) : super(key: key); + + final Widget androidDrawer; + + @override + _SongsTabState createState() => _SongsTabState(); +} + +class _SongsTabState extends State { + static const _itemsLength = 50; + + final _androidRefreshKey = GlobalKey(); + + List colors; + List songNames; + + @override + void initState() { + _setData(); + super.initState(); + } + + void _setData() { + colors = getRandomColors(_itemsLength); + songNames = getRandomNames(_itemsLength); + } + + Future _refreshData() { + return Future.delayed( + // This is just an arbitrary delay that simulates some network activity. + const Duration(seconds: 2), + () => setState(() => _setData()), + ); + } + + Widget _listBuilder(context, index) { + if (index >= _itemsLength) + return null; + + // Show a slightly different color palette. Show poppy-ier colors on iOS + // due to lighter contrasting bars and tone it down on Android. + final color = defaultTargetPlatform == TargetPlatform.iOS + ? colors[index] + : colors[index].shade400; + + return SafeArea( + top: false, + bottom: false, + child: Hero( + tag: index, + child: HeroAnimatingSongCard( + song: songNames[index], + color: color, + heroAnimation: AlwaysStoppedAnimation(0), + onPressed: () => Navigator.of(context).push(MaterialPageRoute( + builder: (context) => SongDetailTab( + id: index, + song: songNames[index], + color: color, + ), + )), + ), + ), + ); + } + + void _togglePlatform() { + TargetPlatform _getOppositePlatform() { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return TargetPlatform.android; + } else { + return TargetPlatform.iOS; + } + } + + debugDefaultTargetPlatformOverride = _getOppositePlatform(); + // This rebuilds the application. This should obviously never be + // done in a real app but it's done here since this app + // unrealistically toggles the current platform for demonstration + // purposes. + WidgetsBinding.instance.reassembleApplication(); + } + + // =========================================================================== + // Non-shared code below because: + // - Android and iOS have different scaffolds + // - There are differenc items in the app bar / nav bar + // - Android has a hamburger drawer, iOS has bottom tabs + // - The iOS nav bar is scrollable, Android is not + // - Pull-to-refresh works differently, and Android has a button to trigger it too + // + // And these are all design time choices that doesn't have a single 'right' + // answer. + // =========================================================================== + Widget _buildAndroid(context) { + return Scaffold( + appBar: AppBar( + title: Text(SongsTab.title), + actions: [ + IconButton( + icon: Icon(Icons.refresh), + onPressed: () async => await _androidRefreshKey.currentState.show(), + ), + IconButton( + icon: Icon(Icons.shuffle), + onPressed: _togglePlatform, + ), + ], + ), + drawer: widget.androidDrawer, + body: RefreshIndicator( + key: _androidRefreshKey, + onRefresh: _refreshData, + child: ListView.builder( + padding: EdgeInsets.symmetric(vertical: 12), + itemBuilder: _listBuilder, + ), + ), + ); + } + + Widget _buildIos(context) { + return CustomScrollView( + slivers: [ + CupertinoSliverNavigationBar( + trailing: CupertinoButton( + padding: EdgeInsets.zero, + child: Icon(CupertinoIcons.shuffle), + onPressed: _togglePlatform, + ), + ), + CupertinoSliverRefreshControl( + onRefresh: _refreshData, + ), + SliverSafeArea( + top: false, + sliver: SliverPadding( + padding: EdgeInsets.symmetric(vertical: 12), + sliver: SliverList( + delegate: SliverChildBuilderDelegate(_listBuilder), + ), + ), + ), + ], + ); + } + + @override + Widget build(context) { + return PlatformWidget( + androidBuilder: _buildAndroid, + iosBuilder: _buildIos, + ); + } +} diff --git a/platform_design/lib/utils.dart b/platform_design/lib/utils.dart new file mode 100644 index 00000000000..3413e77db3e --- /dev/null +++ b/platform_design/lib/utils.dart @@ -0,0 +1,92 @@ +import 'dart:math'; + +import 'package:english_words/english_words.dart'; +// This reimplements generateWordPair because english_words's +// implementation has some performance issues. +// https://github.com/filiph/english_words/issues/9 +// ignore: implementation_imports +import 'package:english_words/src/words/unsafe.dart'; +import 'package:flutter/material.dart'; + +// This file has a number of platform-agnostic non-Widget utility functions. + +const _myListOfRandomColors = [ + Colors.red, Colors.blue, Colors.teal, Colors.yellow, Colors.amber, + Colors.deepOrange, Colors.green, Colors.indigo, Colors.lime, Colors.pink, + Colors.orange, +]; + +final _random = Random(); + +Iterable wordPairIterator = generateWordPair(); +Iterable generateWordPair() sync* { + bool filterWord(word) => unsafe.contains(word); + String pickRandom(List list) => list[_random.nextInt(list.length)]; + + String prefix; + while (true) { + if (_random.nextBool()) { + prefix = pickRandom(adjectives); + } else { + prefix = pickRandom(nouns); + } + final suffix = pickRandom(nouns); + + if (filterWord(prefix) || filterWord(suffix)) + continue; + + final wordPair = WordPair(prefix, suffix); + yield wordPair; + } +} + +String generateRandomHeadline() { + final artist = capitalizePair(wordPairIterator.first); + + switch (_random.nextInt(9)) { + case 0: + return '$artist says ${nouns[_random.nextInt(nouns.length)]}'; + case 1: + return '$artist arrested due to ${wordPairIterator.first.join(' ')}'; + case 2: + return '$artist releases ${capitalizePair(wordPairIterator.first)}'; + case 3: + return '$artist talks about his ${nouns[_random.nextInt(nouns.length)]}'; + case 4: + return '$artist talks about her ${nouns[_random.nextInt(nouns.length)]}'; + case 5: + return '$artist talks about their ${nouns[_random.nextInt(nouns.length)]}'; + case 6: + return '$artist says their music is inspired by ${wordPairIterator.first.join(' ')}'; + case 7: + return '$artist says the world needs more ${nouns[_random.nextInt(nouns.length)]}'; + case 7: + return '$artist calls their band ${adjectives[_random.nextInt(adjectives.length)]}'; + case 8: + return '$artist finally ready to talk about ${nouns[_random.nextInt(nouns.length)]}'; + } + + assert(false, 'Failed to generate news headline'); + return null; +} + +List getRandomColors(int amount) { + return List.generate(amount, (int index) { + return _myListOfRandomColors[_random.nextInt(_myListOfRandomColors.length)]; + }); +} + +List getRandomNames(int amount) { + return wordPairIterator + .take(amount) + .map((pair) => capitalizePair(pair)) + .toList(); +} + +String capitalize(String word) { + return '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}'; +} + +String capitalizePair(WordPair pair) { + return '${capitalize(pair.first)} ${capitalize(pair.second)}'; +} diff --git a/platform_design/lib/widgets.dart b/platform_design/lib/widgets.dart new file mode 100644 index 00000000000..fadca8b2040 --- /dev/null +++ b/platform_design/lib/widgets.dart @@ -0,0 +1,334 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +/// A simple widget that builds different things on different platforms. +class PlatformWidget extends StatelessWidget { + const PlatformWidget({ + Key key, + @required this.androidBuilder, + @required this.iosBuilder, + }) : assert(androidBuilder != null), + assert(iosBuilder != null), + super(key: key); + + final WidgetBuilder androidBuilder; + final WidgetBuilder iosBuilder; + + @override + Widget build(context) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return androidBuilder(context); + case TargetPlatform.iOS: + return iosBuilder(context); + default: + assert(false, 'Unexpected platform $defaultTargetPlatform'); + return null; + } + } +} + +/// A platform-agnostic card with a high elevation that reacts when tapped. +/// +/// This is an example of a custom widget that an app developer might create for +/// use on both iOS and Android as part of their brand's unique design. +class PressableCard extends StatefulWidget { + const PressableCard({ + this.onPressed, + this.color, + this.flattenAnimation, + this.child, + }); + + final VoidCallback onPressed; + final Color color; + final Animation flattenAnimation; + final Widget child; + + @override + State createState() => new _PressableCardState(); +} + +class _PressableCardState extends State with SingleTickerProviderStateMixin { + bool pressed = false; + AnimationController controller; + Animation elevationAnimation; + + @override + void initState() { + controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 40), + ); + elevationAnimation = controller.drive(CurveTween(curve: Curves.easeInOutCubic)); + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + double get flatten => 1 - widget.flattenAnimation.value; + + @override + Widget build(context) { + return Listener( + onPointerDown: (details) { if (widget.onPressed != null) { controller.forward(); } }, + onPointerUp: (details) { controller.reverse(); }, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (widget.onPressed != null) { + widget.onPressed(); + } + }, + // This widget both internally drives an animation when pressed and + // responds to an external animation to flatten the card when in a + // hero animation. You likely want to modularize them more in your own + // app. + child: AnimatedBuilder( + animation: Listenable.merge([elevationAnimation, widget.flattenAnimation]), + child: widget.child, + builder: (context, child) { + return Transform.scale( + // This is just a sample. You likely want to keep the math cleaner + // in your own app. + scale: 1 - elevationAnimation.value * 0.03, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16, horizontal: 16) * flatten, + child: PhysicalModel( + elevation: ((1 - elevationAnimation.value) * 10 + 10) * flatten, + borderRadius: BorderRadius.circular(12 * flatten), + clipBehavior: Clip.antiAlias, + color: widget.color, + child: child, + ), + ), + ); + }, + ), + ), + ); + } +} + +/// A platform-agnostic card representing a song which can be in a card state, +/// a flat state or anything in between. +/// +/// When it's in a card state, it's pressable. +/// +/// This is an example of a custom widget that an app developer might create for +/// use on both iOS and Android as part of their brand's unique design. +class HeroAnimatingSongCard extends StatelessWidget { + HeroAnimatingSongCard({ this.song, this.color, this.heroAnimation, this.onPressed }); + + final String song; + final Color color; + final Animation heroAnimation; + final VoidCallback onPressed; + + double get playButtonSize => 50 + 50 * heroAnimation.value; + + @override + Widget build(context) { + // This is an inefficient usage of AnimatedBuilder since it's rebuilding + // the entire subtree instead of passing in a non-changing child and + // building a transition widget in between. + // + // Left simple in this demo because this card doesn't have any real inner + // content so this just rebuilds everything while animating. + return AnimatedBuilder( + animation: heroAnimation, + builder: (context, child) { + return PressableCard( + onPressed: heroAnimation.value == 0 ? onPressed : null, + color: color, + flattenAnimation: heroAnimation, + child: SizedBox( + height: 250, + child: Stack( + alignment: Alignment.center, + children: [ + // The song title banner slides off in the hero animation. + Positioned( + bottom: - 80 * heroAnimation.value, + left: 0, + right: 0, + child: Container( + height: 80, + color: Colors.black12, + alignment: Alignment.centerLeft, + padding: EdgeInsets.symmetric(horizontal: 12), + child: Text( + song, + style: TextStyle( + fontSize: 21, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + // The play button grows in the hero animation. + Padding( + padding: EdgeInsets.only(bottom: 45) * (1 - heroAnimation.value), + child: Container( + height: playButtonSize, + width: playButtonSize, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black12, + ), + alignment: Alignment.center, + child: Icon(Icons.play_arrow, size: playButtonSize, color: Colors.black38), + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +/// A loading song tile's silhouette. +/// +/// This is an example of a custom widget that an app developer might create for +/// use on both iOS and Android as part of their brand's unique design. +class SongPlaceholderTile extends StatelessWidget { + @override + Widget build(BuildContext context) { + return SizedBox( + height: 95, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Row( + children: [ + Container( + color: Colors.grey[400], + width: 130, + ), + Padding( + padding: EdgeInsets.only(left: 12), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 9, + margin: EdgeInsets.only(right: 60), + color: Colors.grey[300], + ), + Container( + height: 9, + margin: EdgeInsets.only(right: 20, top: 8), + color: Colors.grey[300], + ), + Container( + height: 9, + margin: EdgeInsets.only(right: 40, top: 8), + color: Colors.grey[300], + ), + Container( + height: 9, + margin: EdgeInsets.only(right: 80, top: 8), + color: Colors.grey[300], + ), + Container( + height: 9, + margin: EdgeInsets.only(right: 50, top: 8), + color: Colors.grey[300], + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +// =========================================================================== +// Non-shared code below because different interfaces are shown to prompt +// for a multiple-choice answer. +// +// This is a design choice and you may want to do something different in your +// app. +// =========================================================================== +/// This uses a platform-appropriate mechanism to show users multiple choices. +/// +/// On Android, it uses a dialog with radio buttons. On iOS, it uses a picker. +void showChoices(BuildContext context, List choices) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + showDialog( + context: context, + builder: (context) { + int selectedRadio = 1; + return AlertDialog( + contentPadding: EdgeInsets.only(top: 12), + content: StatefulBuilder( + builder: (context, setState) { + return Column( + mainAxisSize: MainAxisSize.min, + children: List.generate(choices.length, (index) { + return RadioListTile( + title: Text(choices[index]), + value: index, + groupValue: selectedRadio, + onChanged: (value) { + setState(() => selectedRadio = value); + }, + ); + }), + ); + }, + ), + actions: [ + FlatButton( + child: Text('OK'), + onPressed: () => Navigator.of(context).pop(), + ), + FlatButton( + child: Text('CANCEL'), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ); + }, + ); + return; + case TargetPlatform.iOS: + showCupertinoModalPopup( + context: context, + builder: (context) { + return SizedBox( + height: 250, + child: CupertinoPicker( + useMagnifier: true, + magnification: 1.1, + itemExtent: 40, + scrollController: FixedExtentScrollController(initialItem: 1), + children: List.generate(choices.length, (index) { + return Center(child: Text( + choices[index], + style: TextStyle( + fontSize: 21, + ), + )); + }), + onSelectedItemChanged: (value) {}, + ), + ); + } + ); + return; + default: + assert(false, 'Unexpected platform $defaultTargetPlatform'); + } +} diff --git a/platform_design/pubspec.yaml b/platform_design/pubspec.yaml new file mode 100644 index 00000000000..4bde7cf8dac --- /dev/null +++ b/platform_design/pubspec.yaml @@ -0,0 +1,22 @@ +name: platform_design +description: A project showcasing a Flutter app following different platform IA conventions. +version: 1.0.0+1 + +environment: + sdk: ">=2.1.0 <3.0.0" + flutter: ">=1.5.2" + +dependencies: + english_words: ^3.1.5 + flutter_lorem: ^1.1.0 + flutter: + sdk: flutter + + cupertino_icons: ^0.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/platform_design/test/widget_test.dart b/platform_design/test/widget_test.dart new file mode 100644 index 00000000000..9b29041d5b1 --- /dev/null +++ b/platform_design/test/widget_test.dart @@ -0,0 +1,30 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:platform_design/main.dart'; + +void main() { + testWidgets('Can change platform correctly', (tester) async { + await tester.pumpWidget(MyAdaptingApp()); + + // The test should be able to find the drawer button. + expect(find.byIcon(Icons.menu), findsOneWidget); + // There should be a refresh button. + expect(find.byIcon(Icons.refresh), findsOneWidget); + + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + await tester.pumpWidget(MyAdaptingApp()); + + // There should now be a large title style nav bar. + expect(find.byType(CupertinoSliverNavigationBar), findsOneWidget); + // There's a tab button for the first tab. + expect(find.byIcon(CupertinoIcons.music_note), findsOneWidget); + // The hamburger button isn't there anymore. + expect(find.byIcon(Icons.menu), findsNothing); + + // Since this is a static, undo the change made in the test. + debugDefaultTargetPlatformOverride = null; + }); +} diff --git a/travis_script.sh b/travis_script.sh index 4c619cf7c5f..98665f0b724 100755 --- a/travis_script.sh +++ b/travis_script.sh @@ -7,6 +7,7 @@ declare -a PROJECT_NAMES=( "veggieseasons" \ "place_tracker" \ "platform_view_swift" \ + "platform_design" ) for PROJECT_NAME in "${PROJECT_NAMES[@]}"