Skip to content

Commit

Permalink
A simple sample for Jetpack Compose in Surface Duo (#31)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
joyl1216 and conceptdev authored Sep 1, 2020
1 parent 3880c69 commit 23b1459
Show file tree
Hide file tree
Showing 51 changed files with 1,271 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/app-samples-CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

strategy:
matrix:
projects: [SourceEditor, PhotoEditor, Widget, TwoNote] # add ComposeSample once merged
projects: [SourceEditor, PhotoEditor, Widget, TwoNote, ComposeSample]
fail-fast: false

steps:
Expand Down
45 changes: 45 additions & 0 deletions ComposeSample/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
### Android template
# Built application files
*.apk
*.ap_
*.aab

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/
out/
release/

# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Proguard folder generated by Eclipse
proguard/

# Log Files
*.log

# Android Studio Navigation editor temp files
.navigation/

# Android Studio captures folder
captures/

# IntelliJ
*.iml
.idea/

# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
61 changes: 61 additions & 0 deletions ComposeSample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
page_type: sample
name: "Surface Duo - ComposeSample"
description: "A sample showing how to use Jetpack Compose to build an app on the Surface Duo."
languages:
- kotlin
products:
- surface-duo
urlFragment: compose-sample
---

# ComposeSample

This sample is built with Jetpack Compose, the new UI framework in Android.

Here are the requirements for the sample.

- Jetpack Compose version: `0.1.0-dev17`

- Kotlin version: `1.4.0-rc`

- Gradle plugin version: `4.2.0-alpha07`

- Android Studio version: `4.2 Canary 7`

## Getting Started

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).


## Features

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.

![Screenshot](screenshots/Screenshot.png)

## Contributing

This project welcomes contributions and suggestions. Most contributions require you to agree to a
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.

When you submit a pull request, a CLA bot will automatically determine whether you need to provide
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.

This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [[email protected]](mailto:[email protected]) with any additional questions or comments.

## License

Copyright (c) Microsoft Corporation.

MIT License

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:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

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.
60 changes: 60 additions & 0 deletions ComposeSample/app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
*
* * Copyright (c) Microsoft Corporation. All rights reserved.
* * Licensed under the MIT License.
*
*/

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion

defaultConfig {
applicationId "com.microsoft.device.display.samples.composesample"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"

testInstrumentationRunner config.testInstrumentationRunner
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

kotlinOptions {
jvmTarget = '1.8'
}

buildFeatures {
compose true
}

composeOptions {
kotlinCompilerVersion "$kotlinVersion"
kotlinCompilerExtensionVersion "$composeVersion"
}
}

dependencies {
implementation kotlinDependencies.kotlinStdlib
implementation androidxDependencies.ktxCore
implementation androidxDependencies.appCompat
implementation androidxDependencies.window
implementation androidxDependencies.compose
implementation androidxDependencies.composeRuntime
implementation androidxDependencies.composeMaterial
implementation androidxDependencies.composeUITooling

implementation googleDependencies.material

testImplementation testDependencies.junit
androidTestImplementation instrumentationTestDependencies.junit
androidTestImplementation instrumentationTestDependencies.espressoCore
}
24 changes: 24 additions & 0 deletions ComposeSample/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.microsoft.device.display.samples.composesample">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ComposeSample">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/Theme.ComposeSample">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
*
* * Copyright (c) Microsoft Corporation. All rights reserved.
* * Licensed under the MIT License.
*
*/

package com.microsoft.device.display.samples.composesample

import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.Text
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumnForIndexed
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.ui.tooling.preview.Preview
import com.microsoft.device.display.samples.composesample.models.DataProvider
import com.microsoft.device.display.samples.composesample.models.ImageModel
import com.microsoft.device.display.samples.composesample.viewModels.AppStateViewModel

private lateinit var appStateViewModel: AppStateViewModel
private val DEBUG_TAG = "ComposeSample"

@Preview
@Composable
fun HomePreview() {
val models = DataProvider.imageModels
ShowList(models = models)
}

@Composable
fun Home(viewModel: AppStateViewModel) {
appStateViewModel = viewModel
SetupUI()
}

@Composable
fun SetupUI() {
val models = DataProvider.imageModels
val isScreenSpannedLiveData = appStateViewModel.getIsScreenSpannedLiveData()
val isScreenSpanned = isScreenSpannedLiveData.observeAsState(initial = false).value

Log.i(DEBUG_TAG, "SetupUI isScreenSpanned: $isScreenSpanned")

if (isScreenSpanned) {
ShowDetailWithList(models)
} else {
ShowList(models)
}
}

@Composable
private fun ShowList(models: List<ImageModel>) {
ShowListColumn(models, Modifier.fillMaxHeight() then Modifier.fillMaxWidth())
}

@Composable
private fun ShowListColumn(models: List<ImageModel>, modifier: Modifier) {
val imageSelectionLiveData = appStateViewModel.getImageSelectionLiveData()
val selectedIndex = imageSelectionLiveData.observeAsState(initial = 0).value

// ScrollableColumn(modifier) {
// models.forEachIndexed { index, model ->
LazyColumnForIndexed(
items = models,
modifier = modifier
) { index, item ->
Row(
modifier = Modifier.selectable(
selected = (index == selectedIndex),
onClick = {
appStateViewModel.setImageSelectionLiveData(index)
}
) then Modifier.fillMaxWidth(),
verticalGravity = Alignment.CenterVertically
) {
Image(asset = imageResource(item.image), modifier = Modifier.preferredHeight(100.dp).preferredWidth(150.dp))
Spacer(Modifier.preferredWidth(16.dp))
Column(modifier = Modifier.fillMaxHeight() then Modifier.padding(16.dp)) {
Text(item.id, modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center), fontSize = 20.sp, fontWeight = FontWeight.Bold)
Text(item.title, modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center))
}
}
Divider(color = Color.LightGray)
}
}

@Composable
fun ShowDetailWithList(models: List<ImageModel>) {
val imageSelectionLiveData = appStateViewModel.getImageSelectionLiveData()
val selectedIndex = imageSelectionLiveData.observeAsState(initial = 0).value
val selectedImageModel = models[selectedIndex]

Row(
modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center)
then Modifier.fillMaxWidth().wrapContentSize(Alignment.Center)
) {
ShowListColumn(
models, Modifier.fillMaxHeight().wrapContentSize(Alignment.Center).weight(1f)
)
Column(
modifier = Modifier.fillMaxHeight().wrapContentSize(Alignment.Center).weight(1f),
horizontalGravity = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(space = 40.dp)
) {
Text(text = selectedImageModel.id, fontSize = 60.sp)
Image(asset = imageResource(selectedImageModel.image))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
*
* * Copyright (c) Microsoft Corporation. All rights reserved.
* * Licensed under the MIT License.
*
*/

package com.microsoft.device.display.samples.composesample

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.platform.setContent
import androidx.core.util.Consumer
import androidx.lifecycle.ViewModelProvider
import androidx.window.WindowLayoutInfo
import androidx.window.WindowManager
import com.microsoft.device.display.samples.composesample.ui.ComposeSampleTheme
import com.microsoft.device.display.samples.composesample.viewModels.AppStateViewModel
import java.util.concurrent.Executor

class MainActivity : AppCompatActivity() {
private lateinit var windowManager: WindowManager
private lateinit var appStateViewModel: AppStateViewModel

private val handler = Handler(Looper.getMainLooper())
private val mainThreadExecutor = Executor { r: Runnable -> handler.post(r) }
private val layoutStateChangeCallback = LayoutStateChangeCallback()

override fun onCreate(savedInstanceState: Bundle?) {
windowManager = WindowManager(this, null)
appStateViewModel = ViewModelProvider(this).get(AppStateViewModel::class.java)

super.onCreate(savedInstanceState)

setContent {
ComposeSampleTheme {
Home(appStateViewModel)
}
}
}

override fun onAttachedToWindow() {
super.onAttachedToWindow()
windowManager.registerLayoutChangeCallback(mainThreadExecutor, layoutStateChangeCallback)
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
windowManager.unregisterLayoutChangeCallback(layoutStateChangeCallback)
}

inner class LayoutStateChangeCallback : Consumer<WindowLayoutInfo> {
override fun accept(newLayoutInfo: WindowLayoutInfo) {
val isScreenSpanned = newLayoutInfo.displayFeatures.size > 0
appStateViewModel.setIsScreenSpannedLiveData(isScreenSpanned)
}
}
}
Loading

0 comments on commit 23b1459

Please sign in to comment.