Skip to content

Commit 23b1459

Browse files
joyl1216conceptdev
andauthored
A simple sample for Jetpack Compose in Surface Duo (#31)
* Add Ktlint support * Fix codestyle issue * Use LayoutChangeCallback instead of manual checking * Move to Compose-dev17 * Add ComposeSample to the CI pipeline * Add README * Update .gitignore file * Update ComposeSample dependencies * Update kotlin and code cleanup * Remove test files * Add debug tag * Fix ktlint issues * Update compose and AGP * Remove proguard-rules * Remove buildtypes * urlFragment should be all lowercase * Update image assets * Update layout * Add ComposeSample into new pipeline file * Fix the rebase issue * Delete workspace.xml Co-authored-by: Craig Dunn <[email protected]>
1 parent 3880c69 commit 23b1459

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1271
-2
lines changed

.github/workflows/app-samples-CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222

2323
strategy:
2424
matrix:
25-
projects: [SourceEditor, PhotoEditor, Widget, TwoNote] # add ComposeSample once merged
25+
projects: [SourceEditor, PhotoEditor, Widget, TwoNote, ComposeSample]
2626
fail-fast: false
2727

2828
steps:

ComposeSample/.gitignore

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
### Android template
2+
# Built application files
3+
*.apk
4+
*.ap_
5+
*.aab
6+
7+
# Files for the ART/Dalvik VM
8+
*.dex
9+
10+
# Java class files
11+
*.class
12+
13+
# Generated files
14+
bin/
15+
gen/
16+
out/
17+
release/
18+
19+
# Gradle files
20+
.gradle/
21+
build/
22+
23+
# Local configuration file (sdk path, etc)
24+
local.properties
25+
26+
# Proguard folder generated by Eclipse
27+
proguard/
28+
29+
# Log Files
30+
*.log
31+
32+
# Android Studio Navigation editor temp files
33+
.navigation/
34+
35+
# Android Studio captures folder
36+
captures/
37+
38+
# IntelliJ
39+
*.iml
40+
.idea/
41+
42+
# Keystore files
43+
# Uncomment the following lines if you do not want to check your keystore files in.
44+
#*.jks
45+
#*.keystore

ComposeSample/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
page_type: sample
3+
name: "Surface Duo - ComposeSample"
4+
description: "A sample showing how to use Jetpack Compose to build an app on the Surface Duo."
5+
languages:
6+
- kotlin
7+
products:
8+
- surface-duo
9+
urlFragment: compose-sample
10+
---
11+
12+
# ComposeSample
13+
14+
This sample is built with Jetpack Compose, the new UI framework in Android.
15+
16+
Here are the requirements for the sample.
17+
18+
- Jetpack Compose version: `0.1.0-dev17`
19+
20+
- Kotlin version: `1.4.0-rc`
21+
22+
- Gradle plugin version: `4.2.0-alpha07`
23+
24+
- Android Studio version: `4.2 Canary 7`
25+
26+
## Getting Started
27+
28+
To learn how to load apps on the Surface Duo emulator, see the [documentation](https://docs.microsoft.com/dual-screen/android), and follow [the blog](https://devblogs.microsoft.com/surface-duo).
29+
30+
31+
## Features
32+
33+
The sample uses [List-Detail](https://docs.microsoft.com/dual-screen/introduction#companion-pane) app pattern to show a list of image thumbnails in the single screen. When the app is spanned into two screens, it shows the full image in the other screen. To select the image item from the list will show the full image accordingly.
34+
35+
![Screenshot](screenshots/Screenshot.png)
36+
37+
## Contributing
38+
39+
This project welcomes contributions and suggestions. Most contributions require you to agree to a
40+
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
41+
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
42+
43+
When you submit a pull request, a CLA bot will automatically determine whether you need to provide
44+
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
45+
provided by the bot. You will only need to do this once across all repos using our CLA.
46+
47+
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
48+
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
49+
contact [[email protected]](mailto:[email protected]) with any additional questions or comments.
50+
51+
## License
52+
53+
Copyright (c) Microsoft Corporation.
54+
55+
MIT License
56+
57+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
58+
59+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
60+
61+
THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

ComposeSample/app/build.gradle

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
*
3+
* * Copyright (c) Microsoft Corporation. All rights reserved.
4+
* * Licensed under the MIT License.
5+
*
6+
*/
7+
8+
apply plugin: 'com.android.application'
9+
apply plugin: 'kotlin-android'
10+
apply plugin: 'kotlin-android-extensions'
11+
12+
android {
13+
compileSdkVersion rootProject.ext.compileSdkVersion
14+
buildToolsVersion rootProject.ext.buildToolsVersion
15+
16+
defaultConfig {
17+
applicationId "com.microsoft.device.display.samples.composesample"
18+
minSdkVersion 21
19+
targetSdkVersion 30
20+
versionCode 1
21+
versionName "1.0"
22+
23+
testInstrumentationRunner config.testInstrumentationRunner
24+
}
25+
26+
compileOptions {
27+
sourceCompatibility JavaVersion.VERSION_1_8
28+
targetCompatibility JavaVersion.VERSION_1_8
29+
}
30+
31+
kotlinOptions {
32+
jvmTarget = '1.8'
33+
}
34+
35+
buildFeatures {
36+
compose true
37+
}
38+
39+
composeOptions {
40+
kotlinCompilerVersion "$kotlinVersion"
41+
kotlinCompilerExtensionVersion "$composeVersion"
42+
}
43+
}
44+
45+
dependencies {
46+
implementation kotlinDependencies.kotlinStdlib
47+
implementation androidxDependencies.ktxCore
48+
implementation androidxDependencies.appCompat
49+
implementation androidxDependencies.window
50+
implementation androidxDependencies.compose
51+
implementation androidxDependencies.composeRuntime
52+
implementation androidxDependencies.composeMaterial
53+
implementation androidxDependencies.composeUITooling
54+
55+
implementation googleDependencies.material
56+
57+
testImplementation testDependencies.junit
58+
androidTestImplementation instrumentationTestDependencies.junit
59+
androidTestImplementation instrumentationTestDependencies.espressoCore
60+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="com.microsoft.device.display.samples.composesample">
4+
5+
<application
6+
android:allowBackup="true"
7+
android:icon="@mipmap/ic_launcher"
8+
android:label="@string/app_name"
9+
android:roundIcon="@mipmap/ic_launcher_round"
10+
android:supportsRtl="true"
11+
android:theme="@style/Theme.ComposeSample">
12+
<activity
13+
android:name=".MainActivity"
14+
android:label="@string/app_name"
15+
android:theme="@style/Theme.ComposeSample">
16+
<intent-filter>
17+
<action android:name="android.intent.action.MAIN" />
18+
19+
<category android:name="android.intent.category.LAUNCHER" />
20+
</intent-filter>
21+
</activity>
22+
</application>
23+
24+
</manifest>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
*
3+
* * Copyright (c) Microsoft Corporation. All rights reserved.
4+
* * Licensed under the MIT License.
5+
*
6+
*/
7+
8+
package com.microsoft.device.display.samples.composesample
9+
10+
import android.util.Log
11+
import androidx.compose.foundation.Image
12+
import androidx.compose.foundation.Text
13+
import androidx.compose.foundation.layout.Arrangement
14+
import androidx.compose.foundation.layout.Column
15+
import androidx.compose.foundation.layout.Row
16+
import androidx.compose.foundation.layout.Spacer
17+
import androidx.compose.foundation.layout.fillMaxHeight
18+
import androidx.compose.foundation.layout.fillMaxWidth
19+
import androidx.compose.foundation.layout.padding
20+
import androidx.compose.foundation.layout.preferredHeight
21+
import androidx.compose.foundation.layout.preferredWidth
22+
import androidx.compose.foundation.layout.wrapContentSize
23+
import androidx.compose.foundation.lazy.LazyColumnForIndexed
24+
import androidx.compose.foundation.selection.selectable
25+
import androidx.compose.material.Divider
26+
import androidx.compose.runtime.Composable
27+
import androidx.compose.runtime.livedata.observeAsState
28+
import androidx.compose.ui.Alignment
29+
import androidx.compose.ui.Modifier
30+
import androidx.compose.ui.graphics.Color
31+
import androidx.compose.ui.res.imageResource
32+
import androidx.compose.ui.text.font.FontWeight
33+
import androidx.compose.ui.unit.dp
34+
import androidx.compose.ui.unit.sp
35+
import androidx.ui.tooling.preview.Preview
36+
import com.microsoft.device.display.samples.composesample.models.DataProvider
37+
import com.microsoft.device.display.samples.composesample.models.ImageModel
38+
import com.microsoft.device.display.samples.composesample.viewModels.AppStateViewModel
39+
40+
private lateinit var appStateViewModel: AppStateViewModel
41+
private val DEBUG_TAG = "ComposeSample"
42+
43+
@Preview
44+
@Composable
45+
fun HomePreview() {
46+
val models = DataProvider.imageModels
47+
ShowList(models = models)
48+
}
49+
50+
@Composable
51+
fun Home(viewModel: AppStateViewModel) {
52+
appStateViewModel = viewModel
53+
SetupUI()
54+
}
55+
56+
@Composable
57+
fun SetupUI() {
58+
val models = DataProvider.imageModels
59+
val isScreenSpannedLiveData = appStateViewModel.getIsScreenSpannedLiveData()
60+
val isScreenSpanned = isScreenSpannedLiveData.observeAsState(initial = false).value
61+
62+
Log.i(DEBUG_TAG, "SetupUI isScreenSpanned: $isScreenSpanned")
63+
64+
if (isScreenSpanned) {
65+
ShowDetailWithList(models)
66+
} else {
67+
ShowList(models)
68+
}
69+
}
70+
71+
@Composable
72+
private fun ShowList(models: List<ImageModel>) {
73+
ShowListColumn(models, Modifier.fillMaxHeight() then Modifier.fillMaxWidth())
74+
}
75+
76+
@Composable
77+
private fun ShowListColumn(models: List<ImageModel>, modifier: Modifier) {
78+
val imageSelectionLiveData = appStateViewModel.getImageSelectionLiveData()
79+
val selectedIndex = imageSelectionLiveData.observeAsState(initial = 0).value
80+
81+
// ScrollableColumn(modifier) {
82+
// models.forEachIndexed { index, model ->
83+
LazyColumnForIndexed(
84+
items = models,
85+
modifier = modifier
86+
) { index, item ->
87+
Row(
88+
modifier = Modifier.selectable(
89+
selected = (index == selectedIndex),
90+
onClick = {
91+
appStateViewModel.setImageSelectionLiveData(index)
92+
}
93+
) then Modifier.fillMaxWidth(),
94+
verticalGravity = Alignment.CenterVertically
95+
) {
96+
Image(asset = imageResource(item.image), modifier = Modifier.preferredHeight(100.dp).preferredWidth(150.dp))
97+
Spacer(Modifier.preferredWidth(16.dp))
98+
Column(modifier = Modifier.fillMaxHeight() then Modifier.padding(16.dp)) {
99+
Text(item.id, modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center), fontSize = 20.sp, fontWeight = FontWeight.Bold)
100+
Text(item.title, modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center))
101+
}
102+
}
103+
Divider(color = Color.LightGray)
104+
}
105+
}
106+
107+
@Composable
108+
fun ShowDetailWithList(models: List<ImageModel>) {
109+
val imageSelectionLiveData = appStateViewModel.getImageSelectionLiveData()
110+
val selectedIndex = imageSelectionLiveData.observeAsState(initial = 0).value
111+
val selectedImageModel = models[selectedIndex]
112+
113+
Row(
114+
modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center)
115+
then Modifier.fillMaxWidth().wrapContentSize(Alignment.Center)
116+
) {
117+
ShowListColumn(
118+
models, Modifier.fillMaxHeight().wrapContentSize(Alignment.Center).weight(1f)
119+
)
120+
Column(
121+
modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center).weight(1f),
122+
horizontalGravity = Alignment.CenterHorizontally,
123+
verticalArrangement = Arrangement.spacedBy(space = 40.dp)
124+
) {
125+
Text(text = selectedImageModel.id, fontSize = 60.sp)
126+
Image(asset = imageResource(selectedImageModel.image))
127+
}
128+
}
129+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
*
3+
* * Copyright (c) Microsoft Corporation. All rights reserved.
4+
* * Licensed under the MIT License.
5+
*
6+
*/
7+
8+
package com.microsoft.device.display.samples.composesample
9+
10+
import android.os.Bundle
11+
import android.os.Handler
12+
import android.os.Looper
13+
import androidx.appcompat.app.AppCompatActivity
14+
import androidx.compose.ui.platform.setContent
15+
import androidx.core.util.Consumer
16+
import androidx.lifecycle.ViewModelProvider
17+
import androidx.window.WindowLayoutInfo
18+
import androidx.window.WindowManager
19+
import com.microsoft.device.display.samples.composesample.ui.ComposeSampleTheme
20+
import com.microsoft.device.display.samples.composesample.viewModels.AppStateViewModel
21+
import java.util.concurrent.Executor
22+
23+
class MainActivity : AppCompatActivity() {
24+
private lateinit var windowManager: WindowManager
25+
private lateinit var appStateViewModel: AppStateViewModel
26+
27+
private val handler = Handler(Looper.getMainLooper())
28+
private val mainThreadExecutor = Executor { r: Runnable -> handler.post(r) }
29+
private val layoutStateChangeCallback = LayoutStateChangeCallback()
30+
31+
override fun onCreate(savedInstanceState: Bundle?) {
32+
windowManager = WindowManager(this, null)
33+
appStateViewModel = ViewModelProvider(this).get(AppStateViewModel::class.java)
34+
35+
super.onCreate(savedInstanceState)
36+
37+
setContent {
38+
ComposeSampleTheme {
39+
Home(appStateViewModel)
40+
}
41+
}
42+
}
43+
44+
override fun onAttachedToWindow() {
45+
super.onAttachedToWindow()
46+
windowManager.registerLayoutChangeCallback(mainThreadExecutor, layoutStateChangeCallback)
47+
}
48+
49+
override fun onDetachedFromWindow() {
50+
super.onDetachedFromWindow()
51+
windowManager.unregisterLayoutChangeCallback(layoutStateChangeCallback)
52+
}
53+
54+
inner class LayoutStateChangeCallback : Consumer<WindowLayoutInfo> {
55+
override fun accept(newLayoutInfo: WindowLayoutInfo) {
56+
val isScreenSpanned = newLayoutInfo.displayFeatures.size > 0
57+
appStateViewModel.setIsScreenSpannedLiveData(isScreenSpanned)
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)