Merge pull request #4 from andreidiaconu/flutter_foldable_support
Flutter foldable support update
|
@ -1,21 +1,46 @@
|
|||
# See https://www.dartlang.org/guides/libraries/private-files
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
|
||||
# Files and directories created by pub
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.packages
|
||||
build/
|
||||
# If you're building an application, you may want to check-in your pubspec.lock
|
||||
pubspec.lock
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
|
||||
# Directory created by dartdoc
|
||||
# If you don't generate documentation locally you can remove this line.
|
||||
doc/api/
|
||||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Avoid committing generated Javascript files:
|
||||
*.dart.js
|
||||
*.info.json # Produced by the --dump-info flag.
|
||||
*.js # When generated by dart2js. Don't specify *.js if your
|
||||
# project includes source files written in JavaScript.
|
||||
*.js_
|
||||
*.js.deps
|
||||
*.js.map
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
|
25
README.md
|
@ -1,26 +1,31 @@
|
|||
---
|
||||
page_type: sample
|
||||
name: Surface Duo - Flutter SDK samples
|
||||
name: Surface Duo - Flutter samples
|
||||
languages:
|
||||
- java
|
||||
- dart
|
||||
products:
|
||||
- surface-duo
|
||||
description: "Samples showing how to use the Surface Duo with for Flutter for Android app development."
|
||||
description: "Samples showing how to use Flutter for building apps for the Surface Duo."
|
||||
urlFragment: all
|
||||
---
|
||||
# Surface Duo Flutter samples
|
||||
|
||||
This repo contains Flutter samples that incorporate dual-screen enhancements for the Microsoft Surface Duo.
|
||||
This repo contains Flutter samples with enhancements for the Microsoft Surface Duo.
|
||||
|
||||
The [starter_sample](https://github.com/microsoft/surface-duo-sdk-samples-flutter/tree/master/starter_sample/) shows how to incorporate the Surface Duo SDK into Flutter so the device info is available in Dart. The [lightup_sample](https://github.com/microsoft/surface-duo-sdk-samples-flutter/tree/master/lightup_sample/) shows how to use the device info to create a layout that adapts to the dual-screen Surface Duo:
|
||||
The [design_patterns](https://github.com/microsoft/surface-duo-sdk-samples-flutter/tree/master/design_patterns/) project shows how to build the [dual-screen design patterns](https://docs.microsoft.com/en-us/dual-screen/introduction#dual-screen-app-patterns). It is one single application that allows navigating through the following screens:
|
||||
|
||||
![Flutter sample with a dual-screen layout](Screenshots/flutter-lightup-300.png)
|
||||
| Pattern | Folder | Dual-screen screenshot| Single screen screenshot|
|
||||
| :---------: | :---------: | ----------- | ----------- |
|
||||
| ![Extended Canvas design pattern](images/extended_canvas_icon.png)<br/>Extended Canvas | [extended_canvas](https://github.com/microsoft/surface-duo-sdk-samples-flutter/tree/master/design_patterns/lib/extended_canvas) | ![Flutter Extended Canvas sample in dual-screen mode](images/extended_canvas_dual.png) | ![Flutter Extended Canvas sample in single screen mode](images/extended_canvas_single.png) |
|
||||
| ![List Detail design pattern](images/list_detail_icon.png)<br/>List Detail | [list_detail](https://github.com/microsoft/surface-duo-sdk-samples-flutter/tree/master/design_patterns/lib/list_detail) | ![Flutter List Detail sample in dual-screen mode](images/list_detail_dual.png) | ![Flutter List Detail sample in single screen mode](images/list_detail_single.png) |
|
||||
| ![Two Page design pattern](images/two_page_icon.png)<br/>Two Page | [two_page](https://github.com/microsoft/surface-duo-sdk-samples-flutter/tree/master/design_patterns/lib/two_page) | ![Flutter Two Page sample in dual-screen mode](images/two_page_dual.png) | ![Flutter Two Page sample in single screen mode](images/two_page_single.png) |
|
||||
| ![Dual View design pattern](images/dual_view_icon.png)<br/>Dual View<br/>Notepad | [dual_view_notepad](https://github.com/microsoft/surface-duo-sdk-samples-flutter/tree/master/design_patterns/lib/dual_view_notepad) | ![Flutter Dual View Notepad sample in dual-screen mode](images/dual_view_notepad_dual.png) | ![Flutter Dual View Notepad sample in single screen mode](images/dual_view_notepad_single.png) |
|
||||
| ![Dual View design pattern](images/dual_view_icon.png)<br/>Dual View<br/>Restaurants | [dual_view_restaurants](https://github.com/microsoft/surface-duo-sdk-samples-flutter/tree/master/design_patterns/lib/dual_view_restaurants) | ![Flutter Dual View Restaurants sample in dual-screen mode](images/dual_view_restaurants_dual.png) | ![Flutter Dual View Restaurants sample in single screen mode](images/dual_view_restaurants_single.png) |
|
||||
| ![Companion Pane design pattern](images/companion_pane_icon.png)<br/>Companion Pane | [companion_pane](https://github.com/microsoft/surface-duo-sdk-samples-flutter/tree/master/design_patterns/lib/companion_pane) | ![Flutter Companion Pane sample in dual-screen mode](images/companion_pane_dual.png) | ![Flutter Companion Pane sample in single screen mode](images/companion_pane_single.png) |
|
||||
|
||||
Refer the to [dual-screen docs](https://docs.microsoft.com/dual-screen/) for more information and the [Surface Duo emulator download](https://docs.microsoft.com/dual-screen/android/emulator/).
|
||||
The [hinge_angle](https://github.com/microsoft/surface-duo-sdk-samples-flutter/tree/master/design_patterns/lib/hinge_angle) project shows how to use the hinge angle data provided by the [dual_screen](https://pub.dev/packages/dual_screen) flutter package.
|
||||
|
||||
## Adding dual-screen support to an existing app
|
||||
|
||||
In addition to the samples in this repo, the [Adding Surface Duo support to the Flokk application blog post](https://devblogs.microsoft.com/surface-duo/adding-microsoft-surface-duo-support-to-the-flokk-application/) is a good reference for adding dual-screen support.
|
||||
More information, including design resources can be found in the [dual-screen docs](https://docs.microsoft.com/dual-screen/). All the screenshots are generated using the [Surface Duo emulator](https://docs.microsoft.com/dual-screen/android/emulator/).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
|
@ -33,5 +34,13 @@
|
|||
# Web related
|
||||
lib/generated_plugin_registrant.dart
|
||||
|
||||
# Exceptions to above rules.
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
|
@ -4,7 +4,7 @@
|
|||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: f139b11009aeb8ed2a3a3aa8b0066e482709dde3
|
||||
channel: stable
|
||||
revision: 04d166ba19b42e43937011eda045a43975b6322f
|
||||
channel: foldable_support
|
||||
|
||||
project_type: app
|
|
@ -0,0 +1,11 @@
|
|||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||
key.properties
|
|
@ -26,24 +26,19 @@ apply plugin: 'kotlin-android'
|
|||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 31
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.example.duo_flutter_sample"
|
||||
applicationId "com.microsoft.flutterdualscreen.samples.dual_screen_samples"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
targetSdkVersion 30
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -53,7 +48,6 @@ android {
|
|||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
flutter {
|
||||
|
@ -62,8 +56,4 @@ flutter {
|
|||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
||||
compileOnly files('libs/com.microsoft.device.display.displaymask.jar')
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.duo_flutter_sample">
|
||||
package="com.microsoft.flutterdualscreen.samples.dual_screen_samples">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
|
@ -0,0 +1,41 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.microsoft.flutterdualscreen.samples.dual_screen_samples">
|
||||
<application
|
||||
android:label="dual_screen_samples"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<!-- Displays an Android View that continues showing the launch screen
|
||||
Drawable until Flutter paints its first frame, then this splash
|
||||
screen fades out. A splash screen is useful to avoid any visual
|
||||
gap between the end of Android's launch screen and the painting of
|
||||
Flutter's first frame. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.SplashScreenDrawable"
|
||||
android:resource="@drawable/launch_background"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
</manifest>
|
|
@ -0,0 +1,6 @@
|
|||
package com.microsoft.flutterdualscreen.samples.dual_screen_samples
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
До Ширина: | Высота: | Размер: 544 B После Ширина: | Высота: | Размер: 544 B |
До Ширина: | Высота: | Размер: 442 B После Ширина: | Высота: | Размер: 442 B |
До Ширина: | Высота: | Размер: 721 B После Ширина: | Высота: | Размер: 721 B |
До Ширина: | Высота: | Размер: 1.0 KiB После Ширина: | Высота: | Размер: 1.0 KiB |
До Ширина: | Высота: | Размер: 1.4 KiB После Ширина: | Высота: | Размер: 1.4 KiB |
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
|
@ -1,5 +1,5 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.duo_flutter_sample">
|
||||
package="com.microsoft.flutterdualscreen.samples.dual_screen_samples">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
|
@ -1,12 +1,12 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.5.10'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.2'
|
||||
classpath 'com.android.tools.build:gradle:4.1.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
@ -21,8 +21,6 @@ allprojects {
|
|||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.enableR8=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
|
@ -1,6 +1,6 @@
|
|||
#Mon May 11 22:01:59 CDT 2020
|
||||
#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-5.6.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
|
@ -0,0 +1,11 @@
|
|||
include ':app'
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
После Ширина: | Высота: | Размер: 2.9 MiB |
После Ширина: | Высота: | Размер: 60 KiB |
После Ширина: | Высота: | Размер: 64 KiB |
После Ширина: | Высота: | Размер: 763 KiB |
После Ширина: | Высота: | Размер: 69 KiB |
После Ширина: | Высота: | Размер: 53 KiB |
После Ширина: | Высота: | Размер: 70 KiB |
После Ширина: | Высота: | Размер: 71 KiB |
После Ширина: | Высота: | Размер: 69 KiB |
После Ширина: | Высота: | Размер: 54 KiB |
После Ширина: | Высота: | Размер: 924 KiB |
После Ширина: | Высота: | Размер: 814 KiB |
После Ширина: | Высота: | Размер: 534 KiB |
После Ширина: | Высота: | Размер: 314 KiB |
После Ширина: | Высота: | Размер: 866 KiB |
После Ширина: | Высота: | Размер: 564 KiB |
После Ширина: | Высота: | Размер: 416 KiB |
После Ширина: | Высота: | Размер: 892 KiB |
После Ширина: | Высота: | Размер: 589 KiB |
После Ширина: | Высота: | Размер: 105 KiB |
После Ширина: | Высота: | Размер: 568 KiB |
После Ширина: | Высота: | Размер: 545 KiB |
После Ширина: | Высота: | Размер: 352 KiB |
|
@ -18,6 +18,7 @@ Flutter/App.framework
|
|||
Flutter/Flutter.framework
|
||||
Flutter/Flutter.podspec
|
||||
Flutter/Generated.xcconfig
|
||||
Flutter/ephemeral/
|
||||
Flutter/app.flx
|
||||
Flutter/app.zip
|
||||
Flutter/flutter_assets/
|
|
@ -3,7 +3,7 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
|
@ -0,0 +1,2 @@
|
|||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
|
@ -0,0 +1,2 @@
|
|||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
|
@ -0,0 +1,41 @@
|
|||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '9.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
project 'Runner', {
|
||||
'Debug' => :debug,
|
||||
'Profile' => :release,
|
||||
'Release' => :release,
|
||||
}
|
||||
|
||||
def flutter_root
|
||||
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
|
||||
unless File.exist?(generated_xcode_build_settings_path)
|
||||
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
|
||||
end
|
||||
|
||||
File.foreach(generated_xcode_build_settings_path) do |line|
|
||||
matches = line.match(/FLUTTER_ROOT\=(.*)/)
|
||||
return matches[1].strip if matches
|
||||
end
|
||||
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
|
||||
end
|
||||
|
||||
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
|
||||
|
||||
flutter_ios_podfile_setup
|
||||
|
||||
target 'Runner' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
end
|
||||
end
|
|
@ -9,11 +9,7 @@
|
|||
/* 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, ); }; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
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, ); }; };
|
||||
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 */; };
|
||||
|
@ -26,8 +22,6 @@
|
|||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
|
||||
9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -38,13 +32,11 @@
|
|||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
|
@ -57,8 +49,6 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
|
||||
3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -68,9 +58,7 @@
|
|||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B80C3931E831B6300D905FE /* App.framework */,
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEBA1CF902C7004384FC /* Flutter.framework */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
|
@ -102,7 +90,6 @@
|
|||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
97C146F11CF9000F007C117D /* Supporting Files */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
|
@ -111,13 +98,6 @@
|
|||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F11CF9000F007C117D /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
@ -148,7 +128,7 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1020;
|
||||
ORGANIZATIONNAME = "The Chromium Authors";
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
|
@ -157,7 +137,7 @@
|
|||
};
|
||||
};
|
||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
|
@ -201,7 +181,7 @@
|
|||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
|
@ -253,7 +233,6 @@
|
|||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
|
@ -293,7 +272,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
@ -310,17 +289,9 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
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.duoFlutterSample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.flutterdualscreen.samples.dualScreenSamples;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
@ -330,7 +301,6 @@
|
|||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
|
@ -376,7 +346,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
|
@ -386,7 +356,6 @@
|
|||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
|
@ -426,7 +395,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
|
@ -444,17 +413,9 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
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.duoFlutterSample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.flutterdualscreen.samples.dualScreenSamples;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
|
@ -471,17 +432,9 @@
|
|||
CLANG_ENABLE_MODULES = YES;
|
||||
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.duoFlutterSample;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.flutterdualscreen.samples.dualScreenSamples;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
|
@ -2,6 +2,6 @@
|
|||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
До Ширина: | Высота: | Размер: 11 KiB После Ширина: | Высота: | Размер: 11 KiB |
До Ширина: | Высота: | Размер: 564 B После Ширина: | Высота: | Размер: 564 B |
До Ширина: | Высота: | Размер: 1.3 KiB После Ширина: | Высота: | Размер: 1.3 KiB |
До Ширина: | Высота: | Размер: 1.6 KiB После Ширина: | Высота: | Размер: 1.6 KiB |
До Ширина: | Высота: | Размер: 1.0 KiB После Ширина: | Высота: | Размер: 1.0 KiB |
До Ширина: | Высота: | Размер: 1.7 KiB После Ширина: | Высота: | Размер: 1.7 KiB |
До Ширина: | Высота: | Размер: 1.9 KiB После Ширина: | Высота: | Размер: 1.9 KiB |
До Ширина: | Высота: | Размер: 1.3 KiB После Ширина: | Высота: | Размер: 1.3 KiB |
До Ширина: | Высота: | Размер: 1.9 KiB После Ширина: | Высота: | Размер: 1.9 KiB |
До Ширина: | Высота: | Размер: 2.6 KiB После Ширина: | Высота: | Размер: 2.6 KiB |
До Ширина: | Высота: | Размер: 2.6 KiB После Ширина: | Высота: | Размер: 2.6 KiB |
До Ширина: | Высота: | Размер: 3.7 KiB После Ширина: | Высота: | Размер: 3.7 KiB |
До Ширина: | Высота: | Размер: 1.8 KiB После Ширина: | Высота: | Размер: 1.8 KiB |
До Ширина: | Высота: | Размер: 3.2 KiB После Ширина: | Высота: | Размер: 3.2 KiB |
До Ширина: | Высота: | Размер: 3.5 KiB После Ширина: | Высота: | Размер: 3.5 KiB |
До Ширина: | Высота: | Размер: 68 B После Ширина: | Высота: | Размер: 68 B |
До Ширина: | Высота: | Размер: 68 B После Ширина: | Высота: | Размер: 68 B |
До Ширина: | Высота: | Размер: 68 B После Ширина: | Высота: | Размер: 68 B |
|
@ -11,7 +11,7 @@
|
|||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>duo_flutter_sample</string>
|
||||
<string>dual_screen_samples</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
|
@ -0,0 +1 @@
|
|||
#import "GeneratedPluginRegistrant.h"
|
|
@ -0,0 +1,189 @@
|
|||
import 'package:dual_screen_samples/companion_pane/data.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CompanionPane extends StatelessWidget {
|
||||
const CompanionPane({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: ThemeData.dark(),
|
||||
child: Scaffold(
|
||||
// backgroundColor: Colors.grey[900],
|
||||
appBar: AppBar(
|
||||
title: Text('Companion Pane'),
|
||||
),
|
||||
body: TwoPane(
|
||||
pane1: PreviewPane(),
|
||||
pane2: ToolsPane(),
|
||||
paneProportion: 0.7,
|
||||
direction: Axis.vertical,
|
||||
padding: EdgeInsets.only(
|
||||
top: kToolbarHeight + MediaQuery.of(context).padding.top),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PreviewPane extends StatelessWidget {
|
||||
const PreviewPane({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Image.asset(
|
||||
'images/companion_pane_image.png',
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolsPane extends StatelessWidget {
|
||||
const ToolsPane({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
if (constraints.maxHeight > 250 && constraints.maxWidth > 400) {
|
||||
return LargeToolsPane();
|
||||
} else {
|
||||
return SmallToolsPane();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class LargeToolsPane extends StatelessWidget {
|
||||
const LargeToolsPane({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: constraints.maxHeight,
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
children: [
|
||||
Spacer(flex: 10),
|
||||
...tools
|
||||
.expand((e) => [
|
||||
ExpandedToolTile(tool: e),
|
||||
Spacer(flex: 1),
|
||||
])
|
||||
.toList(),
|
||||
Spacer(flex: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class SmallToolsPane extends StatelessWidget {
|
||||
const SmallToolsPane({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Spacer(flex: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: ToolSlider(),
|
||||
),
|
||||
Spacer(flex: 1),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children:
|
||||
tools.map((e) => ToolTile(tool: e, onTap: () {})).toList(),
|
||||
),
|
||||
),
|
||||
Spacer(flex: 2),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExpandedToolTile extends StatelessWidget {
|
||||
final Tool tool;
|
||||
|
||||
const ExpandedToolTile({Key? key, required this.tool}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
ToolTile(tool: tool),
|
||||
SizedBox(width: 10),
|
||||
Expanded(child: ToolSlider()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolTile extends StatelessWidget {
|
||||
final Tool tool;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ToolTile({
|
||||
Key? key,
|
||||
required this.tool,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 82,
|
||||
height: 82,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(tool.icon, size: 34),
|
||||
SizedBox(height: 10),
|
||||
Text(tool.name, textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToolSlider extends StatefulWidget {
|
||||
const ToolSlider({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ToolSliderState createState() => _ToolSliderState();
|
||||
}
|
||||
|
||||
class _ToolSliderState extends State<ToolSlider> {
|
||||
double value = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Slider(
|
||||
value: value,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
value = v;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class Tool {
|
||||
final IconData icon;
|
||||
final String name;
|
||||
|
||||
Tool(this.icon, this.name);
|
||||
}
|
||||
|
||||
List<Tool> tools = [
|
||||
Tool(
|
||||
Icons.auto_fix_high,
|
||||
'Auto Fix',
|
||||
),
|
||||
Tool(
|
||||
Icons.wb_sunny,
|
||||
'Brightness',
|
||||
),
|
||||
Tool(
|
||||
Icons.brightness_medium,
|
||||
'Contrast',
|
||||
),
|
||||
Tool(
|
||||
Icons.vignette,
|
||||
'Vignette',
|
||||
),
|
||||
Tool(
|
||||
Icons.rotate_left,
|
||||
'Rotate',
|
||||
),
|
||||
Tool(
|
||||
Icons.crop,
|
||||
'Crop',
|
||||
),
|
||||
];
|
|
@ -0,0 +1,32 @@
|
|||
const String initialMarkdownData = '''
|
||||
# Large header (h1)
|
||||
###### Small header (h6)
|
||||
|
||||
Text can be *italic* or **bold** or *even **both** at the same time*. It can also contain [links](https://pub.dev/packages/dual_screen).
|
||||
|
||||
* Lists can be
|
||||
* unordered
|
||||
* Item
|
||||
* Item
|
||||
* ordered
|
||||
1. Item
|
||||
1. Item
|
||||
|
||||
This sample can be boiled down to using `TwoPane`:
|
||||
|
||||
```
|
||||
TwoPane(
|
||||
pane1: TextField(
|
||||
onChanged: (text) {
|
||||
setState(() {
|
||||
this.data = text;
|
||||
});
|
||||
},
|
||||
),
|
||||
pane2: Markdown(data: data),
|
||||
panePriority: panePriority,
|
||||
)
|
||||
```
|
||||
|
||||
> Markdown is a lightweight markup language with plain-text-formatting syntax, created in 2004 by John Gruber with Aaron Swartz.
|
||||
''';
|
|
@ -0,0 +1,170 @@
|
|||
import 'package:dual_screen_samples/dual_view_notepad/data.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class DualViewNotepad extends StatefulWidget {
|
||||
const DualViewNotepad({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_DualViewNotepadState createState() => _DualViewNotepadState();
|
||||
}
|
||||
|
||||
class _DualViewNotepadState extends State<DualViewNotepad> {
|
||||
String data = initialMarkdownData;
|
||||
bool editing = true;
|
||||
TextEditingController textController =
|
||||
TextEditingController(text: initialMarkdownData);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool singleScreen = MediaQuery.of(context).hinge == null;
|
||||
var panePriority = TwoPanePriority.both;
|
||||
if (singleScreen) {
|
||||
panePriority = editing ? TwoPanePriority.pane1 : TwoPanePriority.pane2;
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Dual View Notepad'),
|
||||
actions: [
|
||||
if (singleScreen)
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(primary: Colors.white),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
editing = !editing;
|
||||
});
|
||||
},
|
||||
child: Text(editing ? 'View' : 'Edit'),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: TwoPane(
|
||||
pane1: DraftSavedMessage(
|
||||
child: PaneDecorations(
|
||||
header: Text('Editor'),
|
||||
contentColor: Colors.white,
|
||||
headerColor: Colors.blue[200]!,
|
||||
child: TextField(
|
||||
controller: textController,
|
||||
maxLines: 999,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.all(16),
|
||||
),
|
||||
style: GoogleFonts.robotoMono(),
|
||||
onChanged: (text) {
|
||||
setState(() {
|
||||
this.data = text;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
pane2: PaneDecorations(
|
||||
header: Text('Preview'),
|
||||
contentColor: Colors.transparent,
|
||||
headerColor: Colors.green[200]!,
|
||||
child: Markdown(data: data),
|
||||
),
|
||||
panePriority: panePriority,
|
||||
padding: EdgeInsets.only(
|
||||
top: kToolbarHeight + MediaQuery.of(context).padding.top),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DraftSavedMessage extends StatelessWidget {
|
||||
const DraftSavedMessage({
|
||||
Key? key,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
child,
|
||||
Positioned(
|
||||
bottom: 32,
|
||||
left: 32,
|
||||
right: 32,
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
elevation: 4,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text('Your drafts have been saved!')),
|
||||
Text(
|
||||
'Close',
|
||||
style: TextStyle(color: Colors.blue),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PaneDecorations extends StatelessWidget {
|
||||
const PaneDecorations({
|
||||
Key? key,
|
||||
required this.contentColor,
|
||||
required this.headerColor,
|
||||
required this.header,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final Color contentColor;
|
||||
final Color headerColor;
|
||||
final Text header;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey[600]!, width: 1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: contentColor,
|
||||
),
|
||||
margin: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(6),
|
||||
topRight: Radius.circular(6),
|
||||
),
|
||||
color: headerColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(fontSize: 16, color: Colors.black),
|
||||
child: header,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 1,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
Expanded(
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
class Restaurant {
|
||||
final String name, description, picture, type;
|
||||
final double rating;
|
||||
final int voteCount, priceRange;
|
||||
final LatLong latLong;
|
||||
|
||||
const Restaurant({
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.picture,
|
||||
required this.type,
|
||||
required this.priceRange,
|
||||
required this.rating,
|
||||
required this.voteCount,
|
||||
required this.latLong,
|
||||
});
|
||||
}
|
||||
|
||||
class LatLong {
|
||||
final double lat, long;
|
||||
|
||||
const LatLong(this.lat, this.long);
|
||||
}
|
||||
|
||||
const List<Restaurant> restaurants_repo = [
|
||||
const Restaurant(
|
||||
name: 'Pestle Rock',
|
||||
description:
|
||||
'Wine bar with upscale small plates in a lofty modern space with a central wine tower & staircase.',
|
||||
picture: 'images/dual_view_restaurants/pestle_rock_image.png',
|
||||
type: 'Thai',
|
||||
priceRange: 3,
|
||||
rating: 4.4,
|
||||
voteCount: 2303304,
|
||||
latLong: LatLong(0.380, 0.356),
|
||||
),
|
||||
const Restaurant(
|
||||
name: 'Sam\'s Pizza',
|
||||
description:
|
||||
'Take-out/delivery chain offering classic & specialty pizzas, wings & breadsticks, plus desserts.',
|
||||
picture: 'images/dual_view_restaurants/sams_pizza_image.png',
|
||||
type: 'American',
|
||||
priceRange: 2,
|
||||
rating: 4.9,
|
||||
voteCount: 1343,
|
||||
latLong: LatLong(-0.280, -0.56),
|
||||
),
|
||||
const Restaurant(
|
||||
name: 'Sizzle and Crunch',
|
||||
description:
|
||||
'Eatery with a wood-fired oven turning out European & NW dishes in a white-&-blue cottage-like room.',
|
||||
picture: 'images/dual_view_restaurants/sizzle_crunch_image.png',
|
||||
type: 'Thai',
|
||||
priceRange: 2,
|
||||
rating: 3.9,
|
||||
voteCount: 966,
|
||||
latLong: LatLong(0.180, -0.216),
|
||||
),
|
||||
const Restaurant(
|
||||
name: 'Cantinetta',
|
||||
description:
|
||||
'Gourmet Neapolitan pies served in a lofty space with casual, industrial-chic decor.',
|
||||
picture: 'images/dual_view_restaurants/cantinetta_image.png',
|
||||
type: 'Italian',
|
||||
priceRange: 4,
|
||||
rating: 4.6,
|
||||
voteCount: 1322,
|
||||
latLong: LatLong(-0.80, 0.356),
|
||||
),
|
||||
const Restaurant(
|
||||
name: 'Araya\'s Place',
|
||||
description:
|
||||
'Araya\'s Place is the 1st vegan-Thai restaurant in the northwest while supporting local farms.',
|
||||
picture: 'images/dual_view_restaurants/arayas_place_image.png',
|
||||
type: 'Thai',
|
||||
priceRange: 2,
|
||||
rating: 4.6,
|
||||
voteCount: 1322,
|
||||
latLong: LatLong(0.90, 0.210),
|
||||
),
|
||||
const Restaurant(
|
||||
name: 'Kimchi Bistro',
|
||||
description:
|
||||
'Small, no frills Korean restaurant with an extensive menu served in simple digs inside a mall.',
|
||||
picture: 'images/dual_view_restaurants/kimchi_bistro_image.png',
|
||||
type: 'Korean',
|
||||
priceRange: 4,
|
||||
rating: 3.6,
|
||||
voteCount: 4565,
|
||||
latLong: LatLong(-0.230, -0.396),
|
||||
),
|
||||
const Restaurant(
|
||||
name: 'Topolopompo Restaurant',
|
||||
description:
|
||||
'Compact locale with counter service dishing up classic Mediterranean eats such as hummus & falafel.',
|
||||
picture: 'images/dual_view_restaurants/topolopompo_image.png',
|
||||
type: 'FineDine',
|
||||
priceRange: 3,
|
||||
rating: 4.5,
|
||||
voteCount: 6001,
|
||||
latLong: LatLong(0.294, -0.226),
|
||||
),
|
||||
const Restaurant(
|
||||
name: 'Morsel',
|
||||
description:
|
||||
'Homey cafe with sofas, board games & quiet corners for gourmet coffee & craft biscuit sandwiches.',
|
||||
picture: 'images/dual_view_restaurants/morsel_image.png',
|
||||
type: 'Breakfast',
|
||||
priceRange: 3,
|
||||
rating: 4.7,
|
||||
voteCount: 787,
|
||||
latLong: LatLong(-0.180, 0.331),
|
||||
)
|
||||
];
|
|
@ -0,0 +1,221 @@
|
|||
import 'package:dual_screen_samples/dual_view_restaurants/data.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'mock_widgets.dart';
|
||||
|
||||
class DualViewRestaurants extends StatefulWidget {
|
||||
const DualViewRestaurants({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_DualViewRestaurantsState createState() => _DualViewRestaurantsState();
|
||||
}
|
||||
|
||||
class _DualViewRestaurantsState extends State<DualViewRestaurants> {
|
||||
final List<Restaurant> restaurants = restaurants_repo;
|
||||
int? selectedRestaurant;
|
||||
bool showList = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool singleScreen = MediaQuery.of(context).hinge == null;
|
||||
var panePriority = TwoPanePriority.both;
|
||||
if (singleScreen) {
|
||||
panePriority = showList ? TwoPanePriority.pane1 : TwoPanePriority.pane2;
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Dual View Restaurants'),
|
||||
actions: [
|
||||
if (singleScreen)
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(primary: Colors.white),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showList = !showList;
|
||||
});
|
||||
},
|
||||
child: Text(showList ? 'Map' : 'List'),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: TwoPane(
|
||||
pane1: ListPane(
|
||||
restaurants: restaurants,
|
||||
selectedRestaurant: selectedRestaurant,
|
||||
singleScreen: singleScreen,
|
||||
onRestaurantTap: (index) {
|
||||
setState(() {
|
||||
this.selectedRestaurant = index;
|
||||
});
|
||||
if (index != null) {
|
||||
openRestaurant(context, restaurants[index]);
|
||||
}
|
||||
},
|
||||
),
|
||||
pane2: MapPane(
|
||||
restaurants: restaurants,
|
||||
selectedRestaurant: selectedRestaurant,
|
||||
onPinTap: (index) {
|
||||
setState(() {
|
||||
this.selectedRestaurant = index;
|
||||
});
|
||||
},
|
||||
onPopupTap: (index) => openRestaurant(context, restaurants[index]),
|
||||
singleScreen: singleScreen,
|
||||
),
|
||||
panePriority: panePriority,
|
||||
padding: EdgeInsets.only(top: kToolbarHeight + MediaQuery.of(context).padding.top),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void openRestaurant(BuildContext context, Restaurant restaurant) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return RestaurantScreen(restaurant: restaurant);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a list of restaurants.
|
||||
///
|
||||
/// Use [singleScreen] to let the widget know what if this is used alongside
|
||||
/// a [MapPane] or not.
|
||||
/// - If it is, then the selected pin is highlighted.
|
||||
/// - If it is not, no highlighting is needed because selection is not
|
||||
/// possible and does not make sense.
|
||||
class ListPane extends StatefulWidget {
|
||||
final List<Restaurant> restaurants;
|
||||
final int? selectedRestaurant;
|
||||
final ValueChanged<int?> onRestaurantTap;
|
||||
final bool singleScreen;
|
||||
|
||||
const ListPane({
|
||||
Key? key,
|
||||
required this.restaurants,
|
||||
required this.selectedRestaurant,
|
||||
required this.onRestaurantTap,
|
||||
required this.singleScreen,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ListPaneState createState() => _ListPaneState();
|
||||
}
|
||||
|
||||
class _ListPaneState extends State<ListPane> {
|
||||
late ScrollController scrollController;
|
||||
static const itemHeight = 125.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController = ScrollController(
|
||||
initialScrollOffset: (widget.selectedRestaurant ?? 0.0) * itemHeight);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ListPane oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selectedRestaurant != null &&
|
||||
oldWidget.selectedRestaurant != widget.selectedRestaurant) {
|
||||
scrollController.animateTo(
|
||||
widget.selectedRestaurant! * itemHeight,
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.separated(
|
||||
controller: scrollController,
|
||||
itemBuilder: (ctx, index) => SizedBox(
|
||||
height: itemHeight,
|
||||
child: RestaurantListItem(
|
||||
restaurant: widget.restaurants[index],
|
||||
selected: !widget.singleScreen && index == widget.selectedRestaurant,
|
||||
onTap: () {
|
||||
widget.onRestaurantTap(index);
|
||||
},
|
||||
),
|
||||
),
|
||||
itemCount: widget.restaurants.length,
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
Divider(height: 0.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a map of restaurants.
|
||||
///
|
||||
/// Use [singleScreen] to let the widget know what if this is used alongside
|
||||
/// a [ListPane] or not.
|
||||
/// - If it is, then we rely on the list to show details about
|
||||
/// the pin we selected.
|
||||
/// - If it is not, then this widget needs a way to how details about the
|
||||
/// selected pin and it does this by showing a small popup at the bottom.
|
||||
class MapPane extends StatelessWidget {
|
||||
final List<Restaurant> restaurants;
|
||||
final int? selectedRestaurant;
|
||||
final ValueChanged<int?> onPinTap;
|
||||
final ValueChanged<int> onPopupTap;
|
||||
final bool singleScreen;
|
||||
|
||||
const MapPane({
|
||||
Key? key,
|
||||
required this.restaurants,
|
||||
required this.selectedRestaurant,
|
||||
required this.onPinTap,
|
||||
required this.onPopupTap,
|
||||
required this.singleScreen,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Restaurant? selectedMarker =
|
||||
selectedRestaurant == null ? null : restaurants[selectedRestaurant!];
|
||||
List<Restaurant> normalMarkers =
|
||||
restaurants.where((element) => element != selectedMarker).toList();
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: FakeMap(
|
||||
markers: normalMarkers.map((e) => e.latLong).toList(),
|
||||
selectedMarker: selectedMarker?.latLong,
|
||||
onMarkerSelected: (index) {
|
||||
if (index == null) {
|
||||
onPinTap(null);
|
||||
} else {
|
||||
Restaurant newSelection = normalMarkers[index];
|
||||
onPinTap(restaurants.indexOf(newSelection));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
if (singleScreen && selectedMarker != null)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: SizedBox(
|
||||
height: 130,
|
||||
child: Material(
|
||||
elevation: 3,
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
child: RestaurantListItem(
|
||||
restaurant: selectedMarker,
|
||||
onTap: () => onPopupTap(selectedRestaurant!)),
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,407 @@
|
|||
import 'package:dual_screen_samples/dual_view_restaurants/data.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Shows a summary of a restaurant.
|
||||
///
|
||||
/// This is used to render items in the Restaurant List pane but is also used
|
||||
/// to show details about the selected pin on the Restaurant Map pane in
|
||||
/// single-screen mode.
|
||||
class RestaurantListItem extends StatelessWidget {
|
||||
final Restaurant restaurant;
|
||||
final bool selected;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const RestaurantListItem({
|
||||
Key? key,
|
||||
required this.restaurant,
|
||||
this.selected = false,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final price = List.generate(restaurant.priceRange, (index) => '\$').join();
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
restaurant.picture,
|
||||
width: 140,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
restaurant.name,
|
||||
style: Theme.of(context).textTheme.subtitle1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
restaurant.rating.toStringAsFixed(1),
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
SizedBox(width: 6),
|
||||
Rating(rating: restaurant.rating),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'(${restaurant.voteCount})',
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Text(
|
||||
'${restaurant.type} • $price',
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
SizedBox(height: 14),
|
||||
Text(
|
||||
'✓ Open now',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.caption!
|
||||
.copyWith(color: Colors.lightGreen[800]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Expanded(child: Container()),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (selected)
|
||||
Icon(
|
||||
Icons.location_pin,
|
||||
color: Colors.red[800]!,
|
||||
size: 32,
|
||||
),
|
||||
Text(
|
||||
'${restaurant.rating.ceil()} min away',
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulates a map plugin.
|
||||
///
|
||||
/// A real map implementation would require API keys or other configuration and
|
||||
/// this sample needs to be simple and require no configuration from developers.
|
||||
/// Implementation details are not relevant to the sample.
|
||||
class FakeMap extends StatefulWidget {
|
||||
final List<LatLong> markers;
|
||||
final LatLong? selectedMarker;
|
||||
final ValueChanged<int?> onMarkerSelected;
|
||||
|
||||
const FakeMap({
|
||||
Key? key,
|
||||
this.markers = const [],
|
||||
this.selectedMarker,
|
||||
required this.onMarkerSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FakeMapState createState() => _FakeMapState();
|
||||
}
|
||||
|
||||
class _FakeMapState extends State<FakeMap> {
|
||||
TransformationController transformationController =
|
||||
new TransformationController();
|
||||
double scale = 1.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
transformationController.addListener(() {
|
||||
setState(() {
|
||||
Matrix4 v = transformationController.value;
|
||||
scale = v.entry(0, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InteractiveViewer(
|
||||
transformationController: transformationController,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
widget.onMarkerSelected.call(null);
|
||||
},
|
||||
child: Image.asset(
|
||||
'images/dual_view_restaurants/city_map.png',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
...widget.markers.map(
|
||||
(e) => Align(
|
||||
alignment: Alignment(e.lat, e.long),
|
||||
child: Transform.scale(
|
||||
scale: 1 / scale,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
widget.onMarkerSelected.call(widget.markers.indexOf(e));
|
||||
},
|
||||
child: Container(
|
||||
decoration: ShapeDecoration(
|
||||
shape: CircleBorder(), color: Colors.white),
|
||||
padding: EdgeInsets.all(6),
|
||||
child: Icon(
|
||||
Icons.restaurant,
|
||||
color: Colors.deepOrange,
|
||||
size: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.selectedMarker != null)
|
||||
Align(
|
||||
alignment: Alignment(
|
||||
widget.selectedMarker!.lat, widget.selectedMarker!.long),
|
||||
child: Transform.scale(
|
||||
scale: 1 / scale,
|
||||
child: Icon(
|
||||
Icons.location_pin,
|
||||
color: Colors.red[800]!,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A separate screen for one restaurant.
|
||||
///
|
||||
/// In this screen the user can order food or start directions or otherwise
|
||||
/// interact with the restaurant.
|
||||
class RestaurantScreen extends StatelessWidget {
|
||||
final Restaurant restaurant;
|
||||
|
||||
const RestaurantScreen({
|
||||
Key? key,
|
||||
required this.restaurant,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(restaurant.name)),
|
||||
body: TwoPane(
|
||||
pane1: RestaurantDetails(restaurant: restaurant),
|
||||
pane2: RestaurantDetailsSecondScreen(restaurant: restaurant),
|
||||
panePriority: MediaQuery.of(context).hinge == null
|
||||
? TwoPanePriority.pane1
|
||||
: TwoPanePriority.both,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RestaurantDetails extends StatelessWidget {
|
||||
const RestaurantDetails({
|
||||
Key? key,
|
||||
required this.restaurant,
|
||||
}) : super(key: key);
|
||||
|
||||
final Restaurant restaurant;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final price = List.generate(restaurant.priceRange, (index) => '\$').join();
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Image.asset(
|
||||
restaurant.picture,
|
||||
height: 140,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: GenericBox(height: 134, width: 134),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: GenericBox(height: 134),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
restaurant.rating.toStringAsFixed(1),
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
SizedBox(width: 6),
|
||||
Rating(rating: restaurant.rating),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'(${restaurant.voteCount})',
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
Text(
|
||||
'✓ Open now',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.caption!
|
||||
.copyWith(color: Colors.lightGreen[800]),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Text(
|
||||
'${restaurant.type} • $price',
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
restaurant.description,
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
GenericBox(height: 14, width: double.infinity),
|
||||
SizedBox(height: 12),
|
||||
GenericBox(height: 14, width: double.infinity),
|
||||
SizedBox(height: 12),
|
||||
GenericBox(height: 14, width: double.infinity),
|
||||
SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GenericBox(height: 100, width: double.infinity)),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: GenericBox(height: 100, width: double.infinity)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
GenericBox(height: 14, width: double.infinity),
|
||||
SizedBox(height: 12),
|
||||
GenericBox(height: 150, width: double.infinity),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RestaurantDetailsSecondScreen extends StatelessWidget {
|
||||
const RestaurantDetailsSecondScreen({
|
||||
Key? key,
|
||||
required this.restaurant,
|
||||
}) : super(key: key);
|
||||
|
||||
final Restaurant restaurant;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GenericBox(height: 150, width: double.infinity),
|
||||
SizedBox(height: 12),
|
||||
GenericBox(height: 14, width: double.infinity),
|
||||
SizedBox(height: 12),
|
||||
GenericBox(height: 14, width: double.infinity),
|
||||
SizedBox(height: 12),
|
||||
GenericBox(height: 14, width: double.infinity),
|
||||
SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GenericBox(height: 100, width: double.infinity)),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: GenericBox(height: 100, width: double.infinity)),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
GenericBox(height: 14, width: double.infinity),
|
||||
SizedBox(height: 12),
|
||||
GenericBox(height: 150, width: double.infinity),
|
||||
SizedBox(height: 12),
|
||||
GenericBox(height: 14, width: double.infinity),
|
||||
SizedBox(height: 12),
|
||||
GenericBox(height: 14, width: double.infinity),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Grey box used to fill the screen with temporary content.
|
||||
class GenericBox extends StatelessWidget {
|
||||
final double? width, height;
|
||||
|
||||
const GenericBox({Key? key, this.width, this.height}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey, borderRadius: BorderRadius.circular(16)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a 5-star rating.
|
||||
class Rating extends StatelessWidget {
|
||||
final double rating;
|
||||
static const int maxRating = 5;
|
||||
|
||||
const Rating({Key? key, required this.rating}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShaderMask(
|
||||
blendMode: BlendMode.srcATop,
|
||||
shaderCallback: (bounds) => LinearGradient(
|
||||
colors: [Colors.yellow[700]!, Colors.yellow[700]!, Colors.grey],
|
||||
stops: [0.0, rating / maxRating, rating / maxRating])
|
||||
.createShader(bounds),
|
||||
child: Row(
|
||||
children:
|
||||
List.generate(maxRating, (index) => Icon(Icons.star, size: 14)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import 'package:dual_screen_samples/dual_view_restaurants/data.dart';
|
||||
import 'package:dual_screen_samples/dual_view_restaurants/dual_view_restaurants.dart';
|
||||
import 'package:dual_screen_samples/dual_view_restaurants/mock_widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ExtendedCanvas extends StatefulWidget {
|
||||
const ExtendedCanvas({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ExtendedCanvasState createState() => _ExtendedCanvasState();
|
||||
}
|
||||
|
||||
class _ExtendedCanvasState extends State<ExtendedCanvas> {
|
||||
final List<Restaurant> restaurants = restaurants_repo;
|
||||
int? selectedRestaurant;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('Extended Canvas')),
|
||||
body: MapPane(
|
||||
restaurants: restaurants,
|
||||
selectedRestaurant: selectedRestaurant,
|
||||
onPinTap: (index) {
|
||||
openRestaurant(context, index);
|
||||
},
|
||||
onPopupTap: (index) {},
|
||||
singleScreen: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void openRestaurant(BuildContext context, int? index) async {
|
||||
setState(() {
|
||||
selectedRestaurant = index;
|
||||
});
|
||||
if (index != null) {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return RestaurantDetails(restaurant: restaurants[index]);
|
||||
},
|
||||
anchorPoint: _roughLocationOnScreen(context, index),
|
||||
);
|
||||
setState(() {
|
||||
selectedRestaurant = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Restaurant latLong vary from -1 to 1 and we map that to coordinates on the
|
||||
/// screen.
|
||||
Offset _roughLocationOnScreen(BuildContext context, int index) {
|
||||
if ((MediaQuery.of(context).hinge?.bounds.size.aspectRatio ?? 0) > 1) {
|
||||
// When the hinge separates the screens top-bottom, we always use the
|
||||
// bottom screen.
|
||||
return Offset(0.0, 1000);
|
||||
}
|
||||
final restaurantLocation = restaurants[index].latLong;
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
return Offset((restaurantLocation.lat + 1) * screenSize.width / 2,
|
||||
(restaurantLocation.long + 1) * screenSize.height / 2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:dual_screen/dual_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
class HingeAngle extends StatelessWidget {
|
||||
const HingeAngle({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Hinge Angle'),
|
||||
),
|
||||
body: OrientationBuilder(
|
||||
builder: (context, orientation) => TwoPane(
|
||||
panePriority: TwoPanePriority.both,
|
||||
direction: orientation == Orientation.landscape
|
||||
? Axis.horizontal
|
||||
: Axis.vertical,
|
||||
paneProportion: orientation == Orientation.landscape ? 0.6 : 0.4,
|
||||
pane1: Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
alignment: Alignment.center,
|
||||
child: Table(
|
||||
border: TableBorder.all(color: Colors.black),
|
||||
columnWidths: {
|
||||
0: FractionColumnWidth(0.3),
|
||||
1: FractionColumnWidth(0.11),
|
||||
},
|
||||
children: [
|
||||
row(
|
||||
Text('Variable'),
|
||||
Text('Value'),
|
||||
Text('Explanation'),
|
||||
),
|
||||
row(
|
||||
Text('MediaQuery\nposture'),
|
||||
Text(format(MediaQuery.of(context).displayFeatures)),
|
||||
Text(
|
||||
'The Posture is enough for most UX you want to build. This is reported only if the app is spanned.'),
|
||||
),
|
||||
row(
|
||||
Text('DualScreenInfo.\nhasHingeAngleSensor'),
|
||||
FutureBuilder<bool>(
|
||||
future: DualScreenInfo.hasHingeAngleSensor,
|
||||
builder: (context, hasHingeAngleSensor) {
|
||||
return Text(
|
||||
hasHingeAngleSensor.data?.toString() ?? 'N/A');
|
||||
},
|
||||
),
|
||||
Text(
|
||||
'Both foldable and dual screen devices have hinges. Use this property to know if the device has a hinge angle sensor.'),
|
||||
),
|
||||
row(
|
||||
Text('DualScreenInfo.\nhingeAngleEvents'),
|
||||
StreamBuilder<double>(
|
||||
stream: DualScreenInfo.hingeAngleEvents,
|
||||
builder: (context, hingeAngle) {
|
||||
return Text(
|
||||
hingeAngle.data?.toStringAsFixed(2) ?? 'N/A*');
|
||||
},
|
||||
),
|
||||
Text(
|
||||
'Stream<double> with the latest hinge angle. The angle is reported even when the app is not spanned.'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
pane2: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: StreamBuilder<double>(
|
||||
stream: DualScreenInfo.hingeAngleEvents,
|
||||
builder: (context, hingeAngle) {
|
||||
if (hingeAngle.data == null) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'* This sample shows you what the value of the hinge angle is. This requires a foldable or dual screen device or emulator. The emulator has an "Extended Controls" window where you can change the angle.'));
|
||||
} else {
|
||||
return AngleIndicator(angle: hingeAngle.data!);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(
|
||||
top: kToolbarHeight + MediaQuery.of(context).padding.top),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TableRow row(Widget a, b, c) {
|
||||
return TableRow(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8),
|
||||
child: a,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8),
|
||||
child: b,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8),
|
||||
child: c,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
String format(List<ui.DisplayFeature> displayFeatures) {
|
||||
if (displayFeatures.isEmpty) {
|
||||
return 'N/A';
|
||||
} else {
|
||||
return displayFeatures.map((e) => formatPosture(e.state)).join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
String formatPosture(ui.DisplayFeatureState displayFeatureState) {
|
||||
switch (displayFeatureState) {
|
||||
case DisplayFeatureState.postureFlat:
|
||||
return 'Flat';
|
||||
case DisplayFeatureState.postureHalfOpened:
|
||||
return 'HalfOpened';
|
||||
case DisplayFeatureState.unknown:
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AngleIndicator extends StatelessWidget {
|
||||
final double angle;
|
||||
|
||||
const AngleIndicator({Key? key, required this.angle}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: CustomPaint(
|
||||
painter: AnglePainter(
|
||||
angle: angle,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 70.0),
|
||||
child: Text(
|
||||
'${angle.toStringAsFixed(2)}°',
|
||||
style: TextStyle(fontSize: 48),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnglePainter extends CustomPainter {
|
||||
final double angle;
|
||||
final Color color;
|
||||
|
||||
AnglePainter({required this.angle, this.color = Colors.black});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
var paint1 = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
final radians = angle * (pi / 180);
|
||||
canvas.drawArc(
|
||||
Offset(0, 0) & size,
|
||||
-pi / 2 - radians / 2, //radians
|
||||
radians, //radians
|
||||
true,
|
||||
paint1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(AnglePainter oldDelegate) =>
|
||||
oldDelegate.angle != angle || oldDelegate.color != color;
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class ListDetail extends StatefulWidget {
|
||||
const ListDetail({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ListDetailState createState() => _ListDetailState();
|
||||
}
|
||||
|
||||
class _ListDetailState extends State<ListDetail> {
|
||||
final List<String> images = List.generate(
|
||||
12, (index) => 'images/list_detail/list_details_image_${index + 1}.png');
|
||||
int? selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool singleScreen = MediaQuery.of(context).hinge?.bounds?.top != 0.0;
|
||||
return Theme(
|
||||
data: ThemeData.dark(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('List Detail'),
|
||||
),
|
||||
body: TwoPane(
|
||||
pane1: ListPane(
|
||||
images: images,
|
||||
selected: selected,
|
||||
onImageTap: (index) {
|
||||
setState(() {
|
||||
this.selected = index;
|
||||
});
|
||||
if (singleScreen && index != null) {
|
||||
Navigator.of(context).push(
|
||||
SingleScreenExclusiveRoute(
|
||||
builder: (context) => DetailsScreen(image: images[index]),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
singleScreen: singleScreen,
|
||||
),
|
||||
pane2:
|
||||
DetailsPane(image: selected == null ? null : images[selected!]),
|
||||
panePriority:
|
||||
singleScreen ? TwoPanePriority.pane1 : TwoPanePriority.both,
|
||||
padding: EdgeInsets.only(
|
||||
top: kToolbarHeight + MediaQuery.of(context).padding.top),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ListPane extends StatelessWidget {
|
||||
final List<String> images;
|
||||
final int? selected;
|
||||
final ValueChanged<int?> onImageTap;
|
||||
final bool singleScreen;
|
||||
|
||||
const ListPane({
|
||||
Key? key,
|
||||
required this.images,
|
||||
required this.selected,
|
||||
required this.onImageTap,
|
||||
required this.singleScreen,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.all(16),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return Container(
|
||||
foregroundDecoration: index != selected || singleScreen
|
||||
? null
|
||||
: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).accentColor,
|
||||
width: 4,
|
||||
style: BorderStyle.solid,
|
||||
)),
|
||||
child: Ink.image(
|
||||
image: AssetImage(images[index]),
|
||||
fit: BoxFit.cover,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
onImageTap(index);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
return Image.asset(images[index]);
|
||||
},
|
||||
itemCount: images.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DetailsPane extends StatelessWidget {
|
||||
final String? image;
|
||||
|
||||
const DetailsPane({Key? key, required this.image}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return image == null
|
||||
? Center(child: Text('Pick an image from the grid.'))
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Image.asset(image!),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32.0),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: ListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: Icon(Icons.camera, size: 30),
|
||||
),
|
||||
title: Text('Camera'),
|
||||
subtitle: Text('f/2.0 2.5mm ISO 520'),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10.0),
|
||||
child: Icon(Icons.camera_alt, size: 30),
|
||||
),
|
||||
title: Text('Device'),
|
||||
subtitle: Text('Surface Duo'),
|
||||
),
|
||||
)
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A separate screen that presents the [DetailsPane] inside a [Scaffold].
|
||||
class DetailsScreen extends StatelessWidget {
|
||||
final String image;
|
||||
|
||||
const DetailsScreen({Key? key, required this.image}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: ThemeData.dark(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: Text('Image Details')),
|
||||
body: DetailsPane(image: image),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Route that auto-removes itself if the app spanned horizontally.
|
||||
class SingleScreenExclusiveRoute<T> extends MaterialPageRoute<T> {
|
||||
SingleScreenExclusiveRoute({
|
||||
required WidgetBuilder builder,
|
||||
}) : super(builder: builder);
|
||||
|
||||
@override
|
||||
Widget buildContent(BuildContext context) {
|
||||
if (MediaQuery.of(context).hinge?.bounds?.top == 0.0) {
|
||||
navigator?.removeRoute(this);
|
||||
}
|
||||
return super.buildContent(context);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
import 'package:dual_screen_samples/companion_pane/companion_pane.dart';
|
||||
import 'package:dual_screen_samples/dual_view_notepad/dual_view_notepad.dart';
|
||||
import 'package:dual_screen_samples/dual_view_restaurants/dual_view_restaurants.dart';
|
||||
import 'package:dual_screen_samples/extended_canvas/extended_canvas.dart';
|
||||
import 'package:dual_screen_samples/hinge_angle/hinge_angle.dart';
|
||||
import 'package:dual_screen_samples/list_detail/list_detail.dart';
|
||||
import 'package:dual_screen_samples/two_page/two_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
void main() {
|
||||
runApp(MyApp());
|
||||
}
|
||||
|
||||
List<SampleMeta> sampleCatalogue = [
|
||||
SampleMeta(
|
||||
'Extended Canvas',
|
||||
'View locations on a map extended across both screens.',
|
||||
'https://github.com/microsoft/flutter-dualscreen-samples/blob/main/lib/extended_canvas/extended_canvas.dart',
|
||||
'/extended-canvas',
|
||||
(context) => ExtendedCanvas(),
|
||||
),
|
||||
SampleMeta(
|
||||
'List Detail',
|
||||
'List of images on one screen and image details on the other screen.',
|
||||
'https://github.com/microsoft/flutter-dualscreen-samples/blob/main/lib/list_detail/list_detail.dart',
|
||||
'/list-detail',
|
||||
(context) => ListDetail(),
|
||||
),
|
||||
SampleMeta(
|
||||
'Two Page',
|
||||
'A book-like reading experience. You can see two pages simultaneously.',
|
||||
'https://github.com/microsoft/flutter-dualscreen-samples/blob/main/lib/two_page/two_page.dart',
|
||||
'/two-page',
|
||||
(context) => TwoPage(),
|
||||
),
|
||||
SampleMeta(
|
||||
'Dual View Notepad',
|
||||
'Edit markdown on one screen and preview results on the other.',
|
||||
'https://github.com/microsoft/flutter-dualscreen-samples/blob/main/lib/dual_view_notepad/dual_view_notepad.dart',
|
||||
'/dual-view-notepad',
|
||||
(context) => DualViewNotepad(),
|
||||
),
|
||||
SampleMeta(
|
||||
'Dual View Restaurants',
|
||||
'A list of restaurants on one screen and a map with pins on the other.',
|
||||
'https://github.com/microsoft/flutter-dualscreen-samples/blob/main/lib/dual_view_restaurants/dual_view_restaurants.dart',
|
||||
'/dual-view-restaurants',
|
||||
(context) => DualViewRestaurants(),
|
||||
),
|
||||
SampleMeta(
|
||||
'Companion Pane',
|
||||
'Image editor with a preview on one screen and the filters on the other.',
|
||||
'https://github.com/microsoft/flutter-dualscreen-samples/blob/main/lib/companion_pane/companion_pane.dart',
|
||||
'/companion-pane',
|
||||
(context) => CompanionPane(),
|
||||
),
|
||||
SampleMeta(
|
||||
'Hinge Angle',
|
||||
'Interact with the hinge hardware to see the angle change the UI.',
|
||||
'https://github.com/microsoft/flutter-dualscreen-samples/blob/main/lib/hinge_angle/hinge_angle.dart',
|
||||
'/hinge-angle',
|
||||
(context) => HingeAngle(),
|
||||
),
|
||||
];
|
||||
|
||||
class SampleMeta {
|
||||
final String title, subtitle, link, route;
|
||||
final WidgetBuilder builder;
|
||||
|
||||
SampleMeta(this.title, this.subtitle, this.link, this.route, this.builder);
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Dual Screen Samples',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
home: SamplesList(),
|
||||
routes: Map.fromEntries(sampleCatalogue.map(
|
||||
(sample) => MapEntry(sample.route, sample.builder),
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SamplesList extends StatelessWidget {
|
||||
const SamplesList({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Samples list'),
|
||||
),
|
||||
body: TwoPane(
|
||||
pane1: ListView.builder(
|
||||
itemCount: sampleCatalogue.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return ListTile(
|
||||
title: Text(sampleCatalogue[index].title),
|
||||
subtitle: Text(sampleCatalogue[index].subtitle),
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(sampleCatalogue[index].route);
|
||||
},
|
||||
trailing: PopupMenuButton(
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text("View Code")),
|
||||
SizedBox(width: 16),
|
||||
Icon(Icons.open_in_new),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (_) {
|
||||
launch(sampleCatalogue[index].link);
|
||||
},
|
||||
child: Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
padding: EdgeInsets.symmetric(vertical: 4.0),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(Icons.more_vert),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
pane2: Container(),
|
||||
panePriority: MediaQuery.of(context).hinge == null
|
||||
? TwoPanePriority.pane1
|
||||
: TwoPanePriority.both,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class Page1 extends StatelessWidget {
|
||||
const Page1({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final landscape = constraints.maxWidth > constraints.maxHeight;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'The Roman Colosseum',
|
||||
style: GoogleFonts.notoSerif(
|
||||
textStyle: TextStyle(
|
||||
fontSize: landscape ? 24 : 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: landscape ? 15 : 40),
|
||||
Center(
|
||||
child: Image.asset('images/two_page_rome_image.png',
|
||||
height: landscape ? 180 : 240),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Center(
|
||||
child: Text(
|
||||
'Roman Colosseum, ancient history',
|
||||
style: GoogleFonts.notoSans(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: landscape ? 10 : 40),
|
||||
Text(
|
||||
'Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source.',
|
||||
style: GoogleFonts.notoSerif(
|
||||
textStyle: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
Center(
|
||||
child: Text(
|
||||
'Page 1 of 4',
|
||||
style: GoogleFonts.notoSans(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Page2 extends StatelessWidget {
|
||||
const Page2({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final landscape = constraints.maxWidth > constraints.maxHeight;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Why do we use it?',
|
||||
style: GoogleFonts.notoSerif(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using \'Content here, content here\', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for \'lorem ipsum\' will uncover many web sites still in their infancy',
|
||||
style: GoogleFonts.notoSerif(
|
||||
textStyle: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
'Where can I get some?',
|
||||
style: GoogleFonts.notoSerif(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
'There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don\'t look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn\'t anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable.',
|
||||
style: GoogleFonts.notoSerif(
|
||||
textStyle: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
Center(
|
||||
child: Text(
|
||||
'Page 2 of 4',
|
||||
style: GoogleFonts.notoSans(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Page3 extends StatelessWidget {
|
||||
const Page3({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final landscape = constraints.maxWidth > constraints.maxHeight;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Origins and Discovery',
|
||||
style: GoogleFonts.notoSerif(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
'''
|
||||
Until recently, the prevailing view assumed lorem ipsum was born as a nonsense text. "It\'s not Latin, though it looks like it, and it actually says nothing," Before & After magazine answered a curious reader, "Its \'words\' loosely approximate the frequency with which letters occur in English, which is why at a glance it looks pretty real."
|
||||
|
||||
As Cicero would put it, "Um, not so fast."
|
||||
|
||||
The placeholder text, beginning with the line "Lorem ipsum dolor sit amet, consectetur adipiscing elit", looks like Latin because in its youth, centuries ago, it was Latin.
|
||||
|
||||
Richard McClintock, a Latin scholar from Hampden-Sydney College, is credited with discovering the source behind the ubiquitous filler text. In seeing a sample of lorem ipsum, his interest was piqued by consectetur—a genuine, albeit rare, Latin word. Consulting a Latin dictionary led McClintock to a passage from De Finibus Bonorum et Malorum ("On the Extremes of Good and Evil"), a first-century B.C. text from the Roman philosopher Cicero.''',
|
||||
style: GoogleFonts.notoSerif(
|
||||
textStyle: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
Center(
|
||||
child: Text(
|
||||
'Page 3 of 4',
|
||||
style: GoogleFonts.notoSans(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Page4 extends StatelessWidget {
|
||||
const Page4({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final landscape = constraints.maxWidth > constraints.maxHeight;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Here is the classic lorem ipsum passage followed by Boparai\'s odd, yet mesmerizing version:',
|
||||
style: GoogleFonts.notoSerif(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam hendrerit nisi sed sollicitudin pellentesque. Nunc posuere purus rhoncus pulvinar aliquam. Ut aliquet tristique nisl vitae volutpat. Nulla aliquet porttitor venenatis. Donec a dui et dui fringilla consectetur id nec massa. Aliquam erat volutpat. Sed ut dui ut lacus dictum fermentum vel tincidunt neque. Sed sed lacinia lectus. Duis sit amet sodales felis. Duis nunc eros, mattis at dui ac, convallis semper risus. In adipiscing ultrices tellus, in suscipit massa vehicula eu.',
|
||||
style: GoogleFonts.notoSerif(
|
||||
textStyle: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
'Boparai\'s version:',
|
||||
style: GoogleFonts.notoSerif(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
'Rrow itself, let it be sorrow; let him love it; let him pursue it, ishing for its acquisitiendum. Because he will ab hold, uniess but through concer, and also of those who resist. Now a pure snore disturbeded sum dust. He ejjnoyes, in order that somewon, also with a severe one, unless of life. May a cusstums offficer somewon nothing of a poison-filled. Until, from a twho, twho chaffinch may also pursue it, not even a lump. But as twho, as a tank; a proverb, yeast; or else they tinscribe nor. Yet yet dewlap bed. Twho may be, let him love fellows of a polecat. Now amour, the, twhose being, drunk, yet twhitch and, an enclosed valley’s always a laugh.',
|
||||
style: GoogleFonts.notoSerif(
|
||||
textStyle: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
Center(
|
||||
child: Text(
|
||||
'Page 4 of 4',
|
||||
style: GoogleFonts.notoSans(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import 'package:dual_screen_samples/two_page/data.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class TwoPage extends StatefulWidget {
|
||||
const TwoPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_TwoPageState createState() => _TwoPageState();
|
||||
}
|
||||
|
||||
class _TwoPageState extends State<TwoPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SystemChrome.setEnabledSystemUIOverlays([]);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Default values for single screen mode
|
||||
Axis axis = Axis.horizontal;
|
||||
double viewPortFraction = 1.0;
|
||||
Size pageSize = MediaQuery.of(context).size;
|
||||
Size lastPageSize = MediaQuery.of(context).size;
|
||||
EdgeInsets pagePadding = EdgeInsets.zero;
|
||||
EdgeInsets lastPagePadding = EdgeInsets.zero;
|
||||
final isDualScreen = MediaQuery.of(context).hinge != null;
|
||||
if (isDualScreen) {
|
||||
final Size hingeSize = MediaQuery.of(context).hinge!.bounds.size;
|
||||
axis = hingeSize.aspectRatio > 1.0 ? Axis.vertical : Axis.horizontal;
|
||||
if (axis == Axis.horizontal) {
|
||||
// Dual-screen with screens left-and-right
|
||||
pageSize = Size(MediaQuery.of(context).hinge!.bounds.right,
|
||||
MediaQuery.of(context).size.height);
|
||||
pagePadding = EdgeInsets.only(right: hingeSize.width);
|
||||
lastPageSize = Size(pageSize.width - hingeSize.width, pageSize.height);
|
||||
viewPortFraction = pageSize.width / MediaQuery.of(context).size.width;
|
||||
} else {
|
||||
// Dual-screen with screens top-and-bottom
|
||||
pageSize = Size(MediaQuery.of(context).size.width,
|
||||
MediaQuery.of(context).hinge!.bounds.bottom);
|
||||
pagePadding = EdgeInsets.only(bottom: hingeSize.height);
|
||||
lastPageSize = Size(pageSize.width, pageSize.height - hingeSize.height);
|
||||
lastPagePadding = EdgeInsets.only();
|
||||
viewPortFraction =
|
||||
pageSize.height / (MediaQuery.of(context).size.height);
|
||||
}
|
||||
}
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
ListView(
|
||||
scrollDirection: axis,
|
||||
physics: PageScrollPhysics(),
|
||||
controller: PageController(viewportFraction: viewPortFraction),
|
||||
children: [
|
||||
Container(
|
||||
width: pageSize.width,
|
||||
height: pageSize.height,
|
||||
padding: pagePadding,
|
||||
child: Page1(),
|
||||
),
|
||||
Container(
|
||||
width: pageSize.width,
|
||||
height: pageSize.height,
|
||||
padding: pagePadding,
|
||||
child: Page2(),
|
||||
),
|
||||
Container(
|
||||
width: pageSize.width,
|
||||
height: pageSize.height,
|
||||
padding: pagePadding,
|
||||
child: Page3(),
|
||||
),
|
||||
Container(
|
||||
width: lastPageSize.width,
|
||||
height: lastPageSize.height,
|
||||
padding: lastPagePadding,
|
||||
child: Page4(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: 12,
|
||||
left: 12,
|
||||
child: FloatingActionButton(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
mini: true,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Icon(Icons.arrow_back),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,469 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ansicolor
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.2"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
charcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: charcode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
console_log_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: console_log_handler
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.6"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
dual_screen:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../dual_screen"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.0+3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
flutter_map:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_map
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.0"
|
||||
flutter_markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.13.3"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.17.0"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.3"
|
||||
latlong:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: latlong
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.1"
|
||||
lists:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lists
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.6"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.11.4"
|
||||
markdown:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: markdown
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.11"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
mgrs_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mgrs_dart
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
path_provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
path_provider_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pedantic
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
positioned_tap_detector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: positioned_tap_detector
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
proj4dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: proj4dart
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
quiver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: quiver
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
sky_engine:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
path: "/Users/andreidiaconu/work/flutter/engine/src/out/host_debug_unopt/gen/dart-pkg/sky_engine"
|
||||
relative: false
|
||||
source: path
|
||||
version: "0.0.99"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
transparent_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: transparent_image
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: tuple
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
unicode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: unicode
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.4"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
validate:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: validate
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
wkt_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wkt_parser
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.7"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
sdks:
|
||||
dart: ">=2.12.0 <3.0.0"
|
||||
flutter: ">=2.0.0"
|
|
@ -0,0 +1,27 @@
|
|||
name: dual_screen_samples
|
||||
description: Showcases dual screen design patterns in Flutter
|
||||
publish_to: 'none'
|
||||
version: 1.0.0+1
|
||||
|
||||
environment:
|
||||
sdk: ">=2.12.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
url_launcher: ^6.0.3
|
||||
google_fonts: ^2.0.0
|
||||
flutter_markdown: ^0.6.2
|
||||
flutter_map: ^0.12.0
|
||||
dual_screen: ^1.0.0+3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
assets:
|
||||
- images/
|
||||
- images/list_detail/
|
||||
- images/dual_view_restaurants/
|