Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add keyboard and mouse support to CanonicalLayouts/feed-compose #395

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CanonicalLayouts/feed-compose/.gitignore
Expand Up @@ -7,6 +7,7 @@
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
/.idea/deploymentTargetDropDown.xml
.DS_Store
/build
/captures
Expand Down
2 changes: 2 additions & 0 deletions CanonicalLayouts/feed-compose/app/build.gradle
Expand Up @@ -17,6 +17,7 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-parcelize'
}

android {
Expand Down Expand Up @@ -59,6 +60,7 @@ android {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
namespace 'com.example.feedcompose'
}

dependencies {
Expand Down
Expand Up @@ -16,8 +16,7 @@
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.feedcompose">
xmlns:tools="http://schemas.android.com/tools">

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

Expand Down
Expand Up @@ -29,6 +29,9 @@ import com.example.feedcompose.ui.FeedSampleApp
import com.example.feedcompose.ui.theme.FeedComposeTheme

class MainActivity : ComponentActivity() {

private val hasHardwareKey = false

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down
@@ -0,0 +1,50 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.feedcompose.ui.components

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.DropdownMenu
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp

@Composable
internal fun ContextMenu(
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
content: @Composable ColumnScope.() -> Unit = {}
) {
var isExpanded by remember { mutableStateOf(true) }
if (isExpanded) {
Box(modifier = Modifier.offset(x = offset.x, y = offset.y)) {
DropdownMenu(
expanded = isExpanded,
onDismissRequest = { isExpanded = false },
modifier = modifier
) {
content()
}
}
}
}
@@ -0,0 +1,127 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.feedcompose.ui.components

import androidx.compose.foundation.Indication
import androidx.compose.foundation.IndicationInstance
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp

class OutlinedFocusIndication(
private val shape: Shape,
private val outlineWidth: Dp,
private val outlineColor: Color
) : Indication {

@Composable
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
val isEnabledState = interactionSource.collectIsFocusedAsState()

return remember(interactionSource) {
OutlineIndicationInstance(
shape = shape,
outlineWidth = outlineWidth,
outlineColor = outlineColor,
isEnabledState = isEnabledState
)
}
}
}

private class OutlineIndicationInstance(
private val shape: Shape,
private val outlineWidth: Dp,
private val outlineColor: Color,
isEnabledState: State<Boolean>
) : IndicationInstance {
private val isEnabled by isEnabledState

override fun ContentDrawScope.drawIndication() {
drawContent()
if (isEnabled) {
drawOutline(
outline = shape.createOutline(
size = size,
layoutDirection = layoutDirection,
density = this
),
brush = SolidColor(outlineColor),
style = Stroke(width = outlineWidth.toPx())
)
}
}
}

class HighlightIndication(
private val highlightColor: Color = Color.White,
private val alpha: Float = 0.2f,
private val isEnabled: (isFocused: Boolean, isHovered: Boolean) -> Boolean = { isFocused, isHovered ->
isFocused || isHovered
}
) : Indication {

@Composable
override fun rememberUpdatedInstance(interactionSource: InteractionSource): IndicationInstance {
val isFocusedState = interactionSource.collectIsFocusedAsState()
val isHoveredState = interactionSource.collectIsHoveredAsState()
return remember(interactionSource) {
HighlightIndicationInstance(
isFocusedState = isFocusedState,
isHoveredState = isHoveredState,
isEnabled = isEnabled,
highlightColor = highlightColor,
alpha = alpha
)
}
}
}

private class HighlightIndicationInstance(
val highlightColor: Color = Color.White,
val alpha: Float = 0.2f,
isFocusedState: State<Boolean>,
isHoveredState: State<Boolean>,
val isEnabled: (isFocused: Boolean, isHovered: Boolean) -> Boolean = { isFocused, isHovered ->
isFocused || isHovered
}
) : IndicationInstance {
private val isFocused by isFocusedState
private val isHovered by isHoveredState

override fun ContentDrawScope.drawIndication() {
drawContent()
if (isEnabled(isFocused, isHovered)) {
drawRect(
size = size,
color = highlightColor,
alpha = alpha
)
}
}
}
@@ -0,0 +1,33 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.feedcompose.ui.components

import android.view.MotionEvent
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInteropFilter

@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.rightClickable(onRightClick: (x: Float, y: Float) -> Unit): Modifier =
this.pointerInteropFilter {
if (MotionEvent.BUTTON_SECONDARY == it.buttonState) {
onRightClick(it.x, it.y)
true
} else {
false
}
}