Skip to content
This repository was archived by the owner on Jul 11, 2025. It is now read-only.

Commit ff339a5

Browse files
committed
Add keyboard and mouse support to CanonicalLayouts/feed-compose
1 parent 39f319a commit ff339a5

File tree

21 files changed

+736
-142
lines changed

21 files changed

+736
-142
lines changed

CanonicalLayouts/feed-compose/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
/.idea/workspace.xml
88
/.idea/navEditor.xml
99
/.idea/assetWizardSettings.xml
10+
/.idea/deploymentTargetDropDown.xml
1011
.DS_Store
1112
/build
1213
/captures

CanonicalLayouts/feed-compose/app/build.gradle

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@
1717
plugins {
1818
id 'com.android.application'
1919
id 'org.jetbrains.kotlin.android'
20+
id 'kotlin-parcelize'
2021
}
2122

2223
android {
23-
compileSdk 32
24+
compileSdk 33
2425

2526
defaultConfig {
2627
applicationId "com.example.feed_compose"
2728
minSdk 21
28-
targetSdk 32
29+
targetSdk 33
2930
versionCode 1
3031
versionName "1.0"
3132

@@ -52,26 +53,28 @@ android {
5253
compose true
5354
}
5455
composeOptions {
55-
kotlinCompilerExtensionVersion compose_version
56+
kotlinCompilerExtensionVersion '1.3.0'
5657
}
5758
packagingOptions {
5859
resources {
5960
excludes += '/META-INF/{AL2.0,LGPL2.1}'
6061
}
6162
}
63+
namespace 'com.example.feedcompose'
6264
}
6365

6466
dependencies {
65-
66-
implementation 'androidx.core:core-ktx:1.8.0'
67+
def compose_version = '1.3.0'
68+
def material3_version = '1.0.0'
69+
implementation 'androidx.core:core-ktx:1.9.0'
6770
implementation "androidx.compose.ui:ui:$compose_version"
68-
implementation 'androidx.compose.material3:material3:1.0.0-alpha15'
69-
implementation 'androidx.compose.material3:material3-window-size-class:1.0.0-alpha15'
71+
implementation "androidx.compose.material3:material3:$material3_version"
72+
implementation "androidx.compose.material3:material3-window-size-class:$material3_version"
7073
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
7174
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
72-
implementation 'androidx.activity:activity-compose:1.5.1'
73-
implementation 'androidx.navigation:navigation-compose:2.5.1'
74-
implementation 'io.coil-kt:coil-compose:2.1.0'
75+
implementation 'androidx.activity:activity-compose:1.6.1'
76+
implementation 'androidx.navigation:navigation-compose:2.5.3'
77+
implementation 'io.coil-kt:coil-compose:2.2.2'
7578

7679
testImplementation 'junit:junit:4.13.2'
7780
androidTestImplementation 'androidx.test.ext:junit:1.1.3'

CanonicalLayouts/feed-compose/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
-->
1717

1818
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
19-
xmlns:tools="http://schemas.android.com/tools"
20-
package="com.example.feedcompose">
19+
xmlns:tools="http://schemas.android.com/tools">
2120

2221
<uses-permission android:name="android.permission.INTERNET" />
2322

CanonicalLayouts/feed-compose/app/src/main/java/com/example/feedcompose/MainActivity.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import com.example.feedcompose.ui.FeedSampleApp
2929
import com.example.feedcompose.ui.theme.FeedComposeTheme
3030

3131
class MainActivity : ComponentActivity() {
32+
33+
private val hasHardwareKey = false
34+
3235
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
3336
override fun onCreate(savedInstanceState: Bundle?) {
3437
super.onCreate(savedInstanceState)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2022 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.feedcompose.ui.components
18+
19+
import androidx.compose.foundation.layout.Box
20+
import androidx.compose.foundation.layout.ColumnScope
21+
import androidx.compose.foundation.layout.offset
22+
import androidx.compose.material3.DropdownMenu
23+
import androidx.compose.runtime.Composable
24+
import androidx.compose.runtime.getValue
25+
import androidx.compose.runtime.mutableStateOf
26+
import androidx.compose.runtime.remember
27+
import androidx.compose.runtime.setValue
28+
import androidx.compose.ui.Modifier
29+
import androidx.compose.ui.unit.DpOffset
30+
import androidx.compose.ui.unit.dp
31+
32+
@Composable
33+
internal fun ContextMenu(
34+
modifier: Modifier = Modifier,
35+
offset: DpOffset = DpOffset(0.dp, 0.dp),
36+
content: @Composable ColumnScope.() -> Unit = {}
37+
) {
38+
var isExpanded by remember { mutableStateOf(true) }
39+
if (isExpanded) {
40+
Box(modifier = Modifier.offset(x = offset.x, y = offset.y)) {
41+
DropdownMenu(
42+
expanded = isExpanded,
43+
onDismissRequest = { isExpanded = false },
44+
modifier = modifier
45+
) {
46+
content()
47+
}
48+
}
49+
}
50+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2022 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.feedcompose.ui.components
18+
19+
import androidx.compose.foundation.Indication
20+
import androidx.compose.foundation.IndicationInstance
21+
import androidx.compose.foundation.interaction.InteractionSource
22+
import androidx.compose.foundation.interaction.collectIsFocusedAsState
23+
import androidx.compose.foundation.interaction.collectIsHoveredAsState
24+
import androidx.compose.runtime.Composable
25+
import androidx.compose.runtime.State
26+
import androidx.compose.runtime.getValue
27+
import androidx.compose.runtime.remember
28+
import androidx.compose.ui.graphics.Color
29+
import androidx.compose.ui.graphics.Shape
30+
import androidx.compose.ui.graphics.SolidColor
31+
import androidx.compose.ui.graphics.drawOutline
32+
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
33+
import androidx.compose.ui.graphics.drawscope.Stroke
34+
import androidx.compose.ui.unit.Dp
35+
36+
class OutlinedFocusIndication(
37+
private val shape: Shape,
38+
private val outlineWidth: Dp,
39+
private val outlineColor: Color
40+
) : Indication {
41+
42+
@Composable
43+
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
44+
val isEnabledState = interactionSource.collectIsFocusedAsState()
45+
46+
return remember(interactionSource) {
47+
OutlineIndicationInstance(
48+
shape = shape,
49+
outlineWidth = outlineWidth,
50+
outlineColor = outlineColor,
51+
isEnabledState = isEnabledState
52+
)
53+
}
54+
}
55+
}
56+
57+
private class OutlineIndicationInstance(
58+
private val shape: Shape,
59+
private val outlineWidth: Dp,
60+
private val outlineColor: Color,
61+
isEnabledState: State<Boolean>
62+
) : IndicationInstance {
63+
private val isEnabled by isEnabledState
64+
65+
override fun ContentDrawScope.drawIndication() {
66+
drawContent()
67+
if (isEnabled) {
68+
drawOutline(
69+
outline = shape.createOutline(
70+
size = size,
71+
layoutDirection = layoutDirection,
72+
density = this
73+
),
74+
brush = SolidColor(outlineColor),
75+
style = Stroke(width = outlineWidth.toPx())
76+
)
77+
}
78+
}
79+
}
80+
81+
class HighlightIndication(
82+
private val highlightColor: Color = Color.White,
83+
private val alpha: Float = 0.2f,
84+
private val isEnabled: (isFocused: Boolean, isHovered: Boolean) -> Boolean = { isFocused, isHovered ->
85+
isFocused || isHovered
86+
}
87+
) : Indication {
88+
89+
@Composable
90+
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
91+
val isFocusedState = interactionSource.collectIsFocusedAsState()
92+
val isHoveredState = interactionSource.collectIsHoveredAsState()
93+
return remember(interactionSource) {
94+
HighlightIndicationInstance(
95+
isFocusedState = isFocusedState,
96+
isHoveredState = isHoveredState,
97+
isEnabled = isEnabled,
98+
highlightColor = highlightColor,
99+
alpha = alpha
100+
)
101+
}
102+
}
103+
}
104+
105+
private class HighlightIndicationInstance(
106+
val highlightColor: Color = Color.White,
107+
val alpha: Float = 0.2f,
108+
isFocusedState: State<Boolean>,
109+
isHoveredState: State<Boolean>,
110+
val isEnabled: (isFocused: Boolean, isHovered: Boolean) -> Boolean = { isFocused, isHovered ->
111+
isFocused || isHovered
112+
}
113+
) : IndicationInstance {
114+
private val isFocused by isFocusedState
115+
private val isHovered by isHoveredState
116+
117+
override fun ContentDrawScope.drawIndication() {
118+
drawContent()
119+
if (isEnabled(isFocused, isHovered)) {
120+
drawRect(
121+
size = size,
122+
color = highlightColor,
123+
alpha = alpha
124+
)
125+
}
126+
}
127+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2022 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.feedcompose.ui.components
18+
19+
import android.view.MotionEvent
20+
import androidx.compose.ui.ExperimentalComposeUiApi
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.input.pointer.pointerInteropFilter
23+
24+
@OptIn(ExperimentalComposeUiApi::class)
25+
fun Modifier.rightClickable(onRightClick: (x: Float, y: Float) -> Unit): Modifier =
26+
this.pointerInteropFilter {
27+
if (MotionEvent.BUTTON_SECONDARY == it.buttonState) {
28+
onRightClick(it.x, it.y)
29+
true
30+
} else {
31+
false
32+
}
33+
}

0 commit comments

Comments
 (0)