Merge pull request #4 from andreidiaconu/flutter_foldable_support

Flutter foldable support update
This commit is contained in:
Andrei Diaconu 2021-12-07 22:51:09 +02:00 коммит произвёл GitHub
Родитель 548e5aa103 37b640e250
Коммит 7f4ab9f2e7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
199 изменённых файлов: 3689 добавлений и 2057 удалений

57
.gitignore поставляемый
Просмотреть файл

@ -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

Просмотреть файл

@ -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

11
design_patterns/android/.gitignore поставляемый Normal file
Просмотреть файл

@ -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"

Двоичные данные
design_patterns/images/companion_pane_image.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.9 MiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 60 KiB

Двоичные данные
design_patterns/images/dual_view_restaurants/cantinetta_image.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 64 KiB

Двоичные данные
design_patterns/images/dual_view_restaurants/city_map.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 763 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 69 KiB

Двоичные данные
design_patterns/images/dual_view_restaurants/morsel_image.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 53 KiB

Двоичные данные
design_patterns/images/dual_view_restaurants/pestle_rock_image.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 70 KiB

Двоичные данные
design_patterns/images/dual_view_restaurants/sams_pizza_image.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 71 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 69 KiB

Двоичные данные
design_patterns/images/dual_view_restaurants/topolopompo_image.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 54 KiB

Двоичные данные
design_patterns/images/list_detail/list_details_image_1.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 924 KiB

Двоичные данные
design_patterns/images/list_detail/list_details_image_10.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 814 KiB

Двоичные данные
design_patterns/images/list_detail/list_details_image_11.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 534 KiB

Двоичные данные
design_patterns/images/list_detail/list_details_image_12.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 314 KiB

Двоичные данные
design_patterns/images/list_detail/list_details_image_2.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 866 KiB

Двоичные данные
design_patterns/images/list_detail/list_details_image_3.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 564 KiB

Двоичные данные
design_patterns/images/list_detail/list_details_image_4.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 416 KiB

Двоичные данные
design_patterns/images/list_detail/list_details_image_5.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 892 KiB

Двоичные данные
design_patterns/images/list_detail/list_details_image_6.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 589 KiB

Двоичные данные
design_patterns/images/list_detail/list_details_image_7.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 105 KiB

Двоичные данные
design_patterns/images/list_detail/list_details_image_8.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 568 KiB

Двоичные данные
design_patterns/images/list_detail/list_details_image_9.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 545 KiB

Двоичные данные
design_patterns/images/two_page_rome_image.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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.&quot;
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 consectetura 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 valleys 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/

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше