Merge changes from topic "move-gallery" into tm-qpr-dev
* changes: Move the Compose Gallery to our vendor repository (2/2) Introduce lambda-based modifiers Fork Pager from Accompanist
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.layout.pager
|
||||
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.DecayAnimationSpec
|
||||
import androidx.compose.animation.rememberSplineBasedDecay
|
||||
import androidx.compose.foundation.gestures.FlingBehavior
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filter
|
||||
|
||||
/** Library-wide switch to turn on debug logging. */
|
||||
internal const val DebugLog = false
|
||||
|
||||
@RequiresOptIn(message = "Accompanist Pager is experimental. The API may be changed in the future.")
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class ExperimentalPagerApi
|
||||
|
||||
/** Contains the default values used by [HorizontalPager] and [VerticalPager]. */
|
||||
@ExperimentalPagerApi
|
||||
object PagerDefaults {
|
||||
/**
|
||||
* Remember the default [FlingBehavior] that represents the scroll curve.
|
||||
*
|
||||
* @param state The [PagerState] to update.
|
||||
* @param decayAnimationSpec The decay animation spec to use for decayed flings.
|
||||
* @param snapAnimationSpec The animation spec to use when snapping.
|
||||
*/
|
||||
@Composable
|
||||
fun flingBehavior(
|
||||
state: PagerState,
|
||||
decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
|
||||
snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
|
||||
): FlingBehavior =
|
||||
rememberSnappingFlingBehavior(
|
||||
lazyListState = state.lazyListState,
|
||||
decayAnimationSpec = decayAnimationSpec,
|
||||
snapAnimationSpec = snapAnimationSpec,
|
||||
)
|
||||
|
||||
@Deprecated(
|
||||
"Replaced with PagerDefaults.flingBehavior()",
|
||||
ReplaceWith("PagerDefaults.flingBehavior(state, decayAnimationSpec, snapAnimationSpec)")
|
||||
)
|
||||
@Composable
|
||||
fun rememberPagerFlingConfig(
|
||||
state: PagerState,
|
||||
decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
|
||||
snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
|
||||
): FlingBehavior = flingBehavior(state, decayAnimationSpec, snapAnimationSpec)
|
||||
}
|
||||
|
||||
/**
|
||||
* A horizontally scrolling layout that allows users to flip between items to the left and right.
|
||||
*
|
||||
* @sample com.google.accompanist.sample.pager.HorizontalPagerSample
|
||||
*
|
||||
* @param count the number of pages.
|
||||
* @param modifier the modifier to apply to this layout.
|
||||
* @param state the state object to be used to control or observe the pager's state.
|
||||
* @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
|
||||
* composed from the end to the start and [PagerState.currentPage] == 0 will mean the first item is
|
||||
* located at the end.
|
||||
* @param itemSpacing horizontal spacing to add between items.
|
||||
* @param flingBehavior logic describing fling behavior.
|
||||
* @param key the scroll position will be maintained based on the key, which means if you add/remove
|
||||
* items before the current visible item the item with the given key will be kept as the first
|
||||
* visible one.
|
||||
* @param content a block which describes the content. Inside this block you can reference
|
||||
* [PagerScope.currentPage] and other properties in [PagerScope].
|
||||
*/
|
||||
@ExperimentalPagerApi
|
||||
@Composable
|
||||
fun HorizontalPager(
|
||||
count: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
state: PagerState = rememberPagerState(),
|
||||
reverseLayout: Boolean = false,
|
||||
itemSpacing: Dp = 0.dp,
|
||||
flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state),
|
||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
||||
key: ((page: Int) -> Any)? = null,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
content: @Composable PagerScope.(page: Int) -> Unit,
|
||||
) {
|
||||
Pager(
|
||||
count = count,
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
isVertical = false,
|
||||
reverseLayout = reverseLayout,
|
||||
itemSpacing = itemSpacing,
|
||||
verticalAlignment = verticalAlignment,
|
||||
flingBehavior = flingBehavior,
|
||||
key = key,
|
||||
contentPadding = contentPadding,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A vertically scrolling layout that allows users to flip between items to the top and bottom.
|
||||
*
|
||||
* @sample com.google.accompanist.sample.pager.VerticalPagerSample
|
||||
*
|
||||
* @param count the number of pages.
|
||||
* @param modifier the modifier to apply to this layout.
|
||||
* @param state the state object to be used to control or observe the pager's state.
|
||||
* @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
|
||||
* composed from the bottom to the top and [PagerState.currentPage] == 0 will mean the first item is
|
||||
* located at the bottom.
|
||||
* @param itemSpacing vertical spacing to add between items.
|
||||
* @param flingBehavior logic describing fling behavior.
|
||||
* @param key the scroll position will be maintained based on the key, which means if you add/remove
|
||||
* items before the current visible item the item with the given key will be kept as the first
|
||||
* visible one.
|
||||
* @param content a block which describes the content. Inside this block you can reference
|
||||
* [PagerScope.currentPage] and other properties in [PagerScope].
|
||||
*/
|
||||
@ExperimentalPagerApi
|
||||
@Composable
|
||||
fun VerticalPager(
|
||||
count: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
state: PagerState = rememberPagerState(),
|
||||
reverseLayout: Boolean = false,
|
||||
itemSpacing: Dp = 0.dp,
|
||||
flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state),
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
||||
key: ((page: Int) -> Any)? = null,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
content: @Composable PagerScope.(page: Int) -> Unit,
|
||||
) {
|
||||
Pager(
|
||||
count = count,
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
isVertical = true,
|
||||
reverseLayout = reverseLayout,
|
||||
itemSpacing = itemSpacing,
|
||||
horizontalAlignment = horizontalAlignment,
|
||||
flingBehavior = flingBehavior,
|
||||
key = key,
|
||||
contentPadding = contentPadding,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalPagerApi
|
||||
@Composable
|
||||
internal fun Pager(
|
||||
count: Int,
|
||||
modifier: Modifier,
|
||||
state: PagerState,
|
||||
reverseLayout: Boolean,
|
||||
itemSpacing: Dp,
|
||||
isVertical: Boolean,
|
||||
flingBehavior: FlingBehavior,
|
||||
key: ((page: Int) -> Any)?,
|
||||
contentPadding: PaddingValues,
|
||||
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
|
||||
content: @Composable PagerScope.(page: Int) -> Unit,
|
||||
) {
|
||||
require(count >= 0) { "pageCount must be >= 0" }
|
||||
|
||||
// Provide our PagerState with access to the SnappingFlingBehavior animation target
|
||||
// TODO: can this be done in a better way?
|
||||
state.flingAnimationTarget = { (flingBehavior as? SnappingFlingBehavior)?.animationTarget }
|
||||
|
||||
LaunchedEffect(count) {
|
||||
state.currentPage = minOf(count - 1, state.currentPage).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
// Once a fling (scroll) has finished, notify the state
|
||||
LaunchedEffect(state) {
|
||||
// When a 'scroll' has finished, notify the state
|
||||
snapshotFlow { state.isScrollInProgress }
|
||||
.filter { !it }
|
||||
.collect { state.onScrollFinished() }
|
||||
}
|
||||
|
||||
val pagerScope = remember(state) { PagerScopeImpl(state) }
|
||||
|
||||
// We only consume nested flings in the main-axis, allowing cross-axis flings to propagate
|
||||
// as normal
|
||||
val consumeFlingNestedScrollConnection =
|
||||
ConsumeFlingNestedScrollConnection(
|
||||
consumeHorizontal = !isVertical,
|
||||
consumeVertical = isVertical,
|
||||
)
|
||||
|
||||
if (isVertical) {
|
||||
LazyColumn(
|
||||
state = state.lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(itemSpacing, verticalAlignment),
|
||||
horizontalAlignment = horizontalAlignment,
|
||||
flingBehavior = flingBehavior,
|
||||
reverseLayout = reverseLayout,
|
||||
contentPadding = contentPadding,
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(
|
||||
count = count,
|
||||
key = key,
|
||||
) { page ->
|
||||
Box(
|
||||
Modifier
|
||||
// We don't any nested flings to continue in the pager, so we add a
|
||||
// connection which consumes them.
|
||||
// See: https://github.com/google/accompanist/issues/347
|
||||
.nestedScroll(connection = consumeFlingNestedScrollConnection)
|
||||
// Constraint the content to be <= than the size of the pager.
|
||||
.fillParentMaxHeight()
|
||||
.wrapContentSize()
|
||||
) { pagerScope.content(page) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyRow(
|
||||
state = state.lazyListState,
|
||||
verticalAlignment = verticalAlignment,
|
||||
horizontalArrangement = Arrangement.spacedBy(itemSpacing, horizontalAlignment),
|
||||
flingBehavior = flingBehavior,
|
||||
reverseLayout = reverseLayout,
|
||||
contentPadding = contentPadding,
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(
|
||||
count = count,
|
||||
key = key,
|
||||
) { page ->
|
||||
Box(
|
||||
Modifier
|
||||
// We don't any nested flings to continue in the pager, so we add a
|
||||
// connection which consumes them.
|
||||
// See: https://github.com/google/accompanist/issues/347
|
||||
.nestedScroll(connection = consumeFlingNestedScrollConnection)
|
||||
// Constraint the content to be <= than the size of the pager.
|
||||
.fillParentMaxWidth()
|
||||
.wrapContentSize()
|
||||
) { pagerScope.content(page) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConsumeFlingNestedScrollConnection(
|
||||
private val consumeHorizontal: Boolean,
|
||||
private val consumeVertical: Boolean,
|
||||
) : NestedScrollConnection {
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset =
|
||||
when (source) {
|
||||
// We can consume all resting fling scrolls so that they don't propagate up to the
|
||||
// Pager
|
||||
NestedScrollSource.Fling -> available.consume(consumeHorizontal, consumeVertical)
|
||||
else -> Offset.Zero
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
// We can consume all post fling velocity on the main-axis
|
||||
// so that it doesn't propagate up to the Pager
|
||||
return available.consume(consumeHorizontal, consumeVertical)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Offset.consume(
|
||||
consumeHorizontal: Boolean,
|
||||
consumeVertical: Boolean,
|
||||
): Offset =
|
||||
Offset(
|
||||
x = if (consumeHorizontal) this.x else 0f,
|
||||
y = if (consumeVertical) this.y else 0f,
|
||||
)
|
||||
|
||||
private fun Velocity.consume(
|
||||
consumeHorizontal: Boolean,
|
||||
consumeVertical: Boolean,
|
||||
): Velocity =
|
||||
Velocity(
|
||||
x = if (consumeHorizontal) this.x else 0f,
|
||||
y = if (consumeVertical) this.y else 0f,
|
||||
)
|
||||
|
||||
/** Scope for [HorizontalPager] content. */
|
||||
@ExperimentalPagerApi
|
||||
@Stable
|
||||
interface PagerScope {
|
||||
/** Returns the current selected page */
|
||||
val currentPage: Int
|
||||
|
||||
/** The current offset from the start of [currentPage], as a ratio of the page width. */
|
||||
val currentPageOffset: Float
|
||||
}
|
||||
|
||||
@ExperimentalPagerApi
|
||||
private class PagerScopeImpl(
|
||||
private val state: PagerState,
|
||||
) : PagerScope {
|
||||
override val currentPage: Int
|
||||
get() = state.currentPage
|
||||
override val currentPageOffset: Float
|
||||
get() = state.currentPageOffset
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the offset for the given [page] from the current scroll position. This is useful when
|
||||
* using the scroll position to apply effects or animations to items.
|
||||
*
|
||||
* The returned offset can positive or negative, depending on whether which direction the [page] is
|
||||
* compared to the current scroll position.
|
||||
*
|
||||
* @sample com.google.accompanist.sample.pager.HorizontalPagerWithOffsetTransition
|
||||
*/
|
||||
@ExperimentalPagerApi
|
||||
fun PagerScope.calculateCurrentOffsetForPage(page: Int): Float {
|
||||
return (currentPage + currentPageOffset) - page
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.layout.pager
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.MutatePriority
|
||||
import androidx.compose.foundation.gestures.ScrollScope
|
||||
import androidx.compose.foundation.gestures.ScrollableState
|
||||
import androidx.compose.foundation.interaction.InteractionSource
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Deprecated(
|
||||
"Replaced with rememberPagerState(initialPage) and count parameter on Pager composables",
|
||||
ReplaceWith("rememberPagerState(initialPage)"),
|
||||
level = DeprecationLevel.ERROR,
|
||||
)
|
||||
@Suppress("UNUSED_PARAMETER", "NOTHING_TO_INLINE")
|
||||
@ExperimentalPagerApi
|
||||
@Composable
|
||||
inline fun rememberPagerState(
|
||||
@IntRange(from = 0) pageCount: Int,
|
||||
@IntRange(from = 0) initialPage: Int = 0,
|
||||
@FloatRange(from = 0.0, to = 1.0) initialPageOffset: Float = 0f,
|
||||
@IntRange(from = 1) initialOffscreenLimit: Int = 1,
|
||||
infiniteLoop: Boolean = false
|
||||
): PagerState {
|
||||
return rememberPagerState(initialPage = initialPage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [PagerState] that is remembered across compositions.
|
||||
*
|
||||
* Changes to the provided values for [initialPage] will **not** result in the state being recreated
|
||||
* or changed in any way if it has already been created.
|
||||
*
|
||||
* @param initialPage the initial value for [PagerState.currentPage]
|
||||
*/
|
||||
@ExperimentalPagerApi
|
||||
@Composable
|
||||
fun rememberPagerState(
|
||||
@IntRange(from = 0) initialPage: Int = 0,
|
||||
): PagerState =
|
||||
rememberSaveable(saver = PagerState.Saver) {
|
||||
PagerState(
|
||||
currentPage = initialPage,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A state object that can be hoisted to control and observe scrolling for [HorizontalPager].
|
||||
*
|
||||
* In most cases, this will be created via [rememberPagerState].
|
||||
*
|
||||
* @param currentPage the initial value for [PagerState.currentPage]
|
||||
*/
|
||||
@ExperimentalPagerApi
|
||||
@Stable
|
||||
class PagerState(
|
||||
@IntRange(from = 0) currentPage: Int = 0,
|
||||
) : ScrollableState {
|
||||
// Should this be public?
|
||||
internal val lazyListState = LazyListState(firstVisibleItemIndex = currentPage)
|
||||
|
||||
private var _currentPage by mutableStateOf(currentPage)
|
||||
|
||||
private val currentLayoutPageInfo: LazyListItemInfo?
|
||||
get() =
|
||||
lazyListState.layoutInfo.visibleItemsInfo
|
||||
.asSequence()
|
||||
.filter { it.offset <= 0 && it.offset + it.size > 0 }
|
||||
.lastOrNull()
|
||||
|
||||
private val currentLayoutPageOffset: Float
|
||||
get() =
|
||||
currentLayoutPageInfo?.let { current ->
|
||||
// We coerce since itemSpacing can make the offset > 1f.
|
||||
// We don't want to count spacing in the offset so cap it to 1f
|
||||
(-current.offset / current.size.toFloat()).coerceIn(0f, 1f)
|
||||
}
|
||||
?: 0f
|
||||
|
||||
/**
|
||||
* [InteractionSource] that will be used to dispatch drag events when this list is being
|
||||
* dragged. If you want to know whether the fling (or animated scroll) is in progress, use
|
||||
* [isScrollInProgress].
|
||||
*/
|
||||
val interactionSource: InteractionSource
|
||||
get() = lazyListState.interactionSource
|
||||
|
||||
/** The number of pages to display. */
|
||||
@get:IntRange(from = 0)
|
||||
val pageCount: Int by derivedStateOf { lazyListState.layoutInfo.totalItemsCount }
|
||||
|
||||
/**
|
||||
* The index of the currently selected page. This may not be the page which is currently
|
||||
* displayed on screen.
|
||||
*
|
||||
* To update the scroll position, use [scrollToPage] or [animateScrollToPage].
|
||||
*/
|
||||
@get:IntRange(from = 0)
|
||||
var currentPage: Int
|
||||
get() = _currentPage
|
||||
internal set(value) {
|
||||
if (value != _currentPage) {
|
||||
_currentPage = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The current offset from the start of [currentPage], as a ratio of the page width.
|
||||
*
|
||||
* To update the scroll position, use [scrollToPage] or [animateScrollToPage].
|
||||
*/
|
||||
val currentPageOffset: Float by derivedStateOf {
|
||||
currentLayoutPageInfo?.let {
|
||||
// The current page offset is the current layout page delta from `currentPage`
|
||||
// (which is only updated after a scroll/animation).
|
||||
// We calculate this by looking at the current layout page + it's offset,
|
||||
// then subtracting the 'current page'.
|
||||
it.index + currentLayoutPageOffset - _currentPage
|
||||
}
|
||||
?: 0f
|
||||
}
|
||||
|
||||
/** The target page for any on-going animations. */
|
||||
private var animationTargetPage: Int? by mutableStateOf(null)
|
||||
|
||||
internal var flingAnimationTarget: (() -> Int?)? by mutableStateOf(null)
|
||||
|
||||
/**
|
||||
* The target page for any on-going animations or scrolls by the user. Returns the current page
|
||||
* if a scroll or animation is not currently in progress.
|
||||
*/
|
||||
val targetPage: Int
|
||||
get() =
|
||||
animationTargetPage
|
||||
?: flingAnimationTarget?.invoke()
|
||||
?: when {
|
||||
// If a scroll isn't in progress, return the current page
|
||||
!isScrollInProgress -> currentPage
|
||||
// If the offset is 0f (or very close), return the current page
|
||||
currentPageOffset.absoluteValue < 0.001f -> currentPage
|
||||
// If we're offset towards the start, guess the previous page
|
||||
currentPageOffset < -0.5f -> (currentPage - 1).coerceAtLeast(0)
|
||||
// If we're offset towards the end, guess the next page
|
||||
else -> (currentPage + 1).coerceAtMost(pageCount - 1)
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Replaced with animateScrollToPage(page, pageOffset)",
|
||||
ReplaceWith("animateScrollToPage(page = page, pageOffset = pageOffset)")
|
||||
)
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
suspend fun animateScrollToPage(
|
||||
@IntRange(from = 0) page: Int,
|
||||
@FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
|
||||
animationSpec: AnimationSpec<Float> = spring(),
|
||||
initialVelocity: Float = 0f,
|
||||
skipPages: Boolean = true,
|
||||
) {
|
||||
animateScrollToPage(page = page, pageOffset = pageOffset)
|
||||
}
|
||||
|
||||
/**
|
||||
* Animate (smooth scroll) to the given page to the middle of the viewport.
|
||||
*
|
||||
* Cancels the currently running scroll, if any, and suspends until the cancellation is
|
||||
* complete.
|
||||
*
|
||||
* @param page the page to animate to. Must be between 0 and [pageCount] (inclusive).
|
||||
* @param pageOffset the percentage of the page width to offset, from the start of [page]. Must
|
||||
* be in the range 0f..1f.
|
||||
*/
|
||||
suspend fun animateScrollToPage(
|
||||
@IntRange(from = 0) page: Int,
|
||||
@FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
|
||||
) {
|
||||
requireCurrentPage(page, "page")
|
||||
requireCurrentPageOffset(pageOffset, "pageOffset")
|
||||
try {
|
||||
animationTargetPage = page
|
||||
|
||||
if (pageOffset <= 0.005f) {
|
||||
// If the offset is (close to) zero, just call animateScrollToItem and we're done
|
||||
lazyListState.animateScrollToItem(index = page)
|
||||
} else {
|
||||
// Else we need to figure out what the offset is in pixels...
|
||||
|
||||
var target =
|
||||
lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == page }
|
||||
|
||||
if (target != null) {
|
||||
// If we have access to the target page layout, we can calculate the pixel
|
||||
// offset from the size
|
||||
lazyListState.animateScrollToItem(
|
||||
index = page,
|
||||
scrollOffset = (target.size * pageOffset).roundToInt()
|
||||
)
|
||||
} else {
|
||||
// If we don't, we use the current page size as a guide
|
||||
val currentSize = currentLayoutPageInfo!!.size
|
||||
lazyListState.animateScrollToItem(
|
||||
index = page,
|
||||
scrollOffset = (currentSize * pageOffset).roundToInt()
|
||||
)
|
||||
|
||||
// The target should be visible now
|
||||
target = lazyListState.layoutInfo.visibleItemsInfo.first { it.index == page }
|
||||
|
||||
if (target.size != currentSize) {
|
||||
// If the size we used for calculating the offset differs from the actual
|
||||
// target page size, we need to scroll again. This doesn't look great,
|
||||
// but there's not much else we can do.
|
||||
lazyListState.animateScrollToItem(
|
||||
index = page,
|
||||
scrollOffset = (target.size * pageOffset).roundToInt()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// We need to manually call this, as the `animateScrollToItem` call above will happen
|
||||
// in 1 frame, which is usually too fast for the LaunchedEffect in Pager to detect
|
||||
// the change. This is especially true when running unit tests.
|
||||
onScrollFinished()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantly brings the item at [page] to the middle of the viewport.
|
||||
*
|
||||
* Cancels the currently running scroll, if any, and suspends until the cancellation is
|
||||
* complete.
|
||||
*
|
||||
* @param page the page to snap to. Must be between 0 and [pageCount] (inclusive).
|
||||
*/
|
||||
suspend fun scrollToPage(
|
||||
@IntRange(from = 0) page: Int,
|
||||
@FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
|
||||
) {
|
||||
requireCurrentPage(page, "page")
|
||||
requireCurrentPageOffset(pageOffset, "pageOffset")
|
||||
try {
|
||||
animationTargetPage = page
|
||||
|
||||
// First scroll to the given page. It will now be laid out at offset 0
|
||||
lazyListState.scrollToItem(index = page)
|
||||
|
||||
// If we have a start spacing, we need to offset (scroll) by that too
|
||||
if (pageOffset > 0.0001f) {
|
||||
scroll { currentLayoutPageInfo?.let { scrollBy(it.size * pageOffset) } }
|
||||
}
|
||||
} finally {
|
||||
// We need to manually call this, as the `scroll` call above will happen in 1 frame,
|
||||
// which is usually too fast for the LaunchedEffect in Pager to detect the change.
|
||||
// This is especially true when running unit tests.
|
||||
onScrollFinished()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun onScrollFinished() {
|
||||
// Then update the current page to our layout page
|
||||
currentPage = currentLayoutPageInfo?.index ?: 0
|
||||
// Clear the animation target page
|
||||
animationTargetPage = null
|
||||
}
|
||||
|
||||
override suspend fun scroll(
|
||||
scrollPriority: MutatePriority,
|
||||
block: suspend ScrollScope.() -> Unit
|
||||
) = lazyListState.scroll(scrollPriority, block)
|
||||
|
||||
override fun dispatchRawDelta(delta: Float): Float {
|
||||
return lazyListState.dispatchRawDelta(delta)
|
||||
}
|
||||
|
||||
override val isScrollInProgress: Boolean
|
||||
get() = lazyListState.isScrollInProgress
|
||||
|
||||
override fun toString(): String =
|
||||
"PagerState(" +
|
||||
"pageCount=$pageCount, " +
|
||||
"currentPage=$currentPage, " +
|
||||
"currentPageOffset=$currentPageOffset" +
|
||||
")"
|
||||
|
||||
private fun requireCurrentPage(value: Int, name: String) {
|
||||
if (pageCount == 0) {
|
||||
require(value == 0) { "$name must be 0 when pageCount is 0" }
|
||||
} else {
|
||||
require(value in 0 until pageCount) { "$name[$value] must be >= 0 and < pageCount" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireCurrentPageOffset(value: Float, name: String) {
|
||||
if (pageCount == 0) {
|
||||
require(value == 0f) { "$name must be 0f when pageCount is 0" }
|
||||
} else {
|
||||
require(value in 0f..1f) { "$name must be >= 0 and <= 1" }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** The default [Saver] implementation for [PagerState]. */
|
||||
val Saver: Saver<PagerState, *> =
|
||||
listSaver(
|
||||
save = {
|
||||
listOf<Any>(
|
||||
it.currentPage,
|
||||
)
|
||||
},
|
||||
restore = {
|
||||
PagerState(
|
||||
currentPage = it[0] as Int,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.layout.pager
|
||||
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.AnimationState
|
||||
import androidx.compose.animation.core.DecayAnimationSpec
|
||||
import androidx.compose.animation.core.animateDecay
|
||||
import androidx.compose.animation.core.animateTo
|
||||
import androidx.compose.animation.core.calculateTargetValue
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.rememberSplineBasedDecay
|
||||
import androidx.compose.foundation.gestures.FlingBehavior
|
||||
import androidx.compose.foundation.gestures.ScrollScope
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
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 kotlin.math.abs
|
||||
|
||||
/** Default values used for [SnappingFlingBehavior] & [rememberSnappingFlingBehavior]. */
|
||||
internal object SnappingFlingBehaviorDefaults {
|
||||
/** TODO */
|
||||
val snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = 600f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and remember a snapping [FlingBehavior] to be used with [LazyListState].
|
||||
*
|
||||
* TODO: move this to a new module and make it public
|
||||
*
|
||||
* @param lazyListState The [LazyListState] to update.
|
||||
* @param decayAnimationSpec The decay animation spec to use for decayed flings.
|
||||
* @param snapAnimationSpec The animation spec to use when snapping.
|
||||
*/
|
||||
@Composable
|
||||
internal fun rememberSnappingFlingBehavior(
|
||||
lazyListState: LazyListState,
|
||||
decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
|
||||
snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
|
||||
): SnappingFlingBehavior =
|
||||
remember(lazyListState, decayAnimationSpec, snapAnimationSpec) {
|
||||
SnappingFlingBehavior(
|
||||
lazyListState = lazyListState,
|
||||
decayAnimationSpec = decayAnimationSpec,
|
||||
snapAnimationSpec = snapAnimationSpec,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A snapping [FlingBehavior] for [LazyListState]. Typically this would be created via
|
||||
* [rememberSnappingFlingBehavior].
|
||||
*
|
||||
* @param lazyListState The [LazyListState] to update.
|
||||
* @param decayAnimationSpec The decay animation spec to use for decayed flings.
|
||||
* @param snapAnimationSpec The animation spec to use when snapping.
|
||||
*/
|
||||
internal class SnappingFlingBehavior(
|
||||
private val lazyListState: LazyListState,
|
||||
private val decayAnimationSpec: DecayAnimationSpec<Float>,
|
||||
private val snapAnimationSpec: AnimationSpec<Float>,
|
||||
) : FlingBehavior {
|
||||
/** The target item index for any on-going animations. */
|
||||
var animationTarget: Int? by mutableStateOf(null)
|
||||
private set
|
||||
|
||||
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
|
||||
val itemInfo = currentItemInfo ?: return initialVelocity
|
||||
|
||||
// If the decay fling can scroll past the current item, fling with decay
|
||||
return if (decayAnimationSpec.canFlingPastCurrentItem(itemInfo, initialVelocity)) {
|
||||
performDecayFling(initialVelocity, itemInfo)
|
||||
} else {
|
||||
// Otherwise we 'spring' to current/next item
|
||||
performSpringFling(
|
||||
index =
|
||||
when {
|
||||
// If the velocity is greater than 1 item per second (velocity is px/s),
|
||||
// spring
|
||||
// in the relevant direction
|
||||
initialVelocity > itemInfo.size -> {
|
||||
(itemInfo.index + 1).coerceAtMost(
|
||||
lazyListState.layoutInfo.totalItemsCount - 1
|
||||
)
|
||||
}
|
||||
initialVelocity < -itemInfo.size -> itemInfo.index
|
||||
// If the velocity is 0 (or less than the size of the item), spring to
|
||||
// whichever item is closest to the snap point
|
||||
itemInfo.offset < -itemInfo.size / 2 -> itemInfo.index + 1
|
||||
else -> itemInfo.index
|
||||
},
|
||||
initialVelocity = initialVelocity,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ScrollScope.performDecayFling(
|
||||
initialVelocity: Float,
|
||||
startItem: LazyListItemInfo,
|
||||
): Float {
|
||||
val index =
|
||||
when {
|
||||
initialVelocity > 0 -> startItem.index + 1
|
||||
else -> startItem.index
|
||||
}
|
||||
val forward = index > (currentItemInfo?.index ?: return initialVelocity)
|
||||
|
||||
// Update the animationTarget
|
||||
animationTarget = index
|
||||
|
||||
var velocityLeft = initialVelocity
|
||||
var lastValue = 0f
|
||||
AnimationState(
|
||||
initialValue = 0f,
|
||||
initialVelocity = initialVelocity,
|
||||
)
|
||||
.animateDecay(decayAnimationSpec) {
|
||||
val delta = value - lastValue
|
||||
val consumed = scrollBy(delta)
|
||||
lastValue = value
|
||||
velocityLeft = this.velocity
|
||||
|
||||
val current = currentItemInfo
|
||||
if (current == null) {
|
||||
cancelAnimation()
|
||||
return@animateDecay
|
||||
}
|
||||
|
||||
if (
|
||||
!forward &&
|
||||
(current.index < index || current.index == index && current.offset >= 0)
|
||||
) {
|
||||
// 'snap back' to the item as we may have scrolled past it
|
||||
scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
|
||||
cancelAnimation()
|
||||
} else if (
|
||||
forward &&
|
||||
(current.index > index || current.index == index && current.offset <= 0)
|
||||
) {
|
||||
// 'snap back' to the item as we may have scrolled past it
|
||||
scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
|
||||
cancelAnimation()
|
||||
} else if (abs(delta - consumed) > 0.5f) {
|
||||
// avoid rounding errors and stop if anything is unconsumed
|
||||
cancelAnimation()
|
||||
}
|
||||
}
|
||||
animationTarget = null
|
||||
return velocityLeft
|
||||
}
|
||||
|
||||
private suspend fun ScrollScope.performSpringFling(
|
||||
index: Int,
|
||||
scrollOffset: Int = 0,
|
||||
initialVelocity: Float = 0f,
|
||||
): Float {
|
||||
// If we don't have a current layout, we can't snap
|
||||
val initialItem = currentItemInfo ?: return initialVelocity
|
||||
|
||||
val forward = index > initialItem.index
|
||||
// We add 10% on to the size of the current item, to compensate for any item spacing, etc
|
||||
val target = (if (forward) initialItem.size else -initialItem.size) * 1.1f
|
||||
|
||||
// Update the animationTarget
|
||||
animationTarget = index
|
||||
|
||||
var velocityLeft = initialVelocity
|
||||
var lastValue = 0f
|
||||
AnimationState(
|
||||
initialValue = 0f,
|
||||
initialVelocity = initialVelocity,
|
||||
)
|
||||
.animateTo(
|
||||
targetValue = target,
|
||||
animationSpec = snapAnimationSpec,
|
||||
) {
|
||||
// Springs can overshoot their target, clamp to the desired range
|
||||
val coercedValue =
|
||||
if (forward) {
|
||||
value.coerceAtMost(target)
|
||||
} else {
|
||||
value.coerceAtLeast(target)
|
||||
}
|
||||
val delta = coercedValue - lastValue
|
||||
val consumed = scrollBy(delta)
|
||||
lastValue = coercedValue
|
||||
velocityLeft = this.velocity
|
||||
|
||||
val current = currentItemInfo
|
||||
if (current == null) {
|
||||
cancelAnimation()
|
||||
return@animateTo
|
||||
}
|
||||
|
||||
if (scrolledPastItem(initialVelocity, current, index, scrollOffset)) {
|
||||
// If we've scrolled to/past the item, stop the animation. We may also need to
|
||||
// 'snap back' to the item as we may have scrolled past it
|
||||
scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
|
||||
cancelAnimation()
|
||||
} else if (abs(delta - consumed) > 0.5f) {
|
||||
// avoid rounding errors and stop if anything is unconsumed
|
||||
cancelAnimation()
|
||||
}
|
||||
}
|
||||
animationTarget = null
|
||||
return velocityLeft
|
||||
}
|
||||
|
||||
private fun LazyListState.calculateScrollOffsetToItem(index: Int): Int {
|
||||
return layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }?.offset ?: 0
|
||||
}
|
||||
|
||||
private val currentItemInfo: LazyListItemInfo?
|
||||
get() =
|
||||
lazyListState.layoutInfo.visibleItemsInfo
|
||||
.asSequence()
|
||||
.filter { it.offset <= 0 && it.offset + it.size > 0 }
|
||||
.lastOrNull()
|
||||
}
|
||||
|
||||
private fun scrolledPastItem(
|
||||
initialVelocity: Float,
|
||||
currentItem: LazyListItemInfo,
|
||||
targetIndex: Int,
|
||||
targetScrollOffset: Int = 0,
|
||||
): Boolean {
|
||||
return if (initialVelocity > 0) {
|
||||
// forward
|
||||
currentItem.index > targetIndex ||
|
||||
(currentItem.index == targetIndex && currentItem.offset <= targetScrollOffset)
|
||||
} else {
|
||||
// backwards
|
||||
currentItem.index < targetIndex ||
|
||||
(currentItem.index == targetIndex && currentItem.offset >= targetScrollOffset)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DecayAnimationSpec<Float>.canFlingPastCurrentItem(
|
||||
currentItem: LazyListItemInfo,
|
||||
initialVelocity: Float,
|
||||
): Boolean {
|
||||
val targetValue =
|
||||
calculateTargetValue(
|
||||
initialValue = currentItem.offset.toFloat(),
|
||||
initialVelocity = initialVelocity,
|
||||
)
|
||||
return when {
|
||||
// forward. We add 10% onto the size to cater for any item spacing
|
||||
initialVelocity > 0 -> targetValue <= -(currentItem.size * 1.1f)
|
||||
// backwards. We add 10% onto the size to cater for any item spacing
|
||||
else -> targetValue >= (currentItem.size * 0.1f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.modifiers
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.LayoutModifier
|
||||
import androidx.compose.ui.layout.Measurable
|
||||
import androidx.compose.ui.layout.MeasureResult
|
||||
import androidx.compose.ui.layout.MeasureScope
|
||||
import androidx.compose.ui.platform.InspectorInfo
|
||||
import androidx.compose.ui.platform.InspectorValueInfo
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.constrainHeight
|
||||
import androidx.compose.ui.unit.constrainWidth
|
||||
import androidx.compose.ui.unit.offset
|
||||
|
||||
// This file was mostly copy/pasted from by androidx.compose.foundation.layout.Padding.kt and
|
||||
// contains modifiers with lambda parameters to change the padding of a Composable without
|
||||
// triggering recomposition when the paddings change.
|
||||
//
|
||||
// These should be used instead of the traditional size modifiers when the size changes often, for
|
||||
// instance when it is animated.
|
||||
//
|
||||
// TODO(b/247473910): Remove these modifiers once they can be fully replaced by layout animations
|
||||
// APIs.
|
||||
|
||||
/** @see androidx.compose.foundation.layout.padding */
|
||||
fun Modifier.padding(
|
||||
start: Density.() -> Int = PaddingUnspecified,
|
||||
top: Density.() -> Int = PaddingUnspecified,
|
||||
end: Density.() -> Int = PaddingUnspecified,
|
||||
bottom: Density.() -> Int = PaddingUnspecified,
|
||||
) =
|
||||
this.then(
|
||||
PaddingModifier(
|
||||
start,
|
||||
top,
|
||||
end,
|
||||
bottom,
|
||||
rtlAware = true,
|
||||
inspectorInfo =
|
||||
debugInspectorInfo {
|
||||
name = "padding"
|
||||
properties["start"] = start
|
||||
properties["top"] = top
|
||||
properties["end"] = end
|
||||
properties["bottom"] = bottom
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
/** @see androidx.compose.foundation.layout.padding */
|
||||
fun Modifier.padding(
|
||||
horizontal: Density.() -> Int = PaddingUnspecified,
|
||||
vertical: Density.() -> Int = PaddingUnspecified,
|
||||
): Modifier {
|
||||
return this.then(
|
||||
PaddingModifier(
|
||||
start = horizontal,
|
||||
top = vertical,
|
||||
end = horizontal,
|
||||
bottom = vertical,
|
||||
rtlAware = true,
|
||||
inspectorInfo =
|
||||
debugInspectorInfo {
|
||||
name = "padding"
|
||||
properties["horizontal"] = horizontal
|
||||
properties["vertical"] = vertical
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val PaddingUnspecified: Density.() -> Int = { 0 }
|
||||
|
||||
private class PaddingModifier(
|
||||
val start: Density.() -> Int,
|
||||
val top: Density.() -> Int,
|
||||
val end: Density.() -> Int,
|
||||
val bottom: Density.() -> Int,
|
||||
val rtlAware: Boolean,
|
||||
inspectorInfo: InspectorInfo.() -> Unit
|
||||
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
|
||||
override fun MeasureScope.measure(
|
||||
measurable: Measurable,
|
||||
constraints: Constraints
|
||||
): MeasureResult {
|
||||
val start = start()
|
||||
val top = top()
|
||||
val end = end()
|
||||
val bottom = bottom()
|
||||
|
||||
val horizontal = start + end
|
||||
val vertical = top + bottom
|
||||
|
||||
val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
|
||||
|
||||
val width = constraints.constrainWidth(placeable.width + horizontal)
|
||||
val height = constraints.constrainHeight(placeable.height + vertical)
|
||||
return layout(width, height) {
|
||||
if (rtlAware) {
|
||||
placeable.placeRelative(start, top)
|
||||
} else {
|
||||
placeable.place(start, top)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = start.hashCode()
|
||||
result = 31 * result + top.hashCode()
|
||||
result = 31 * result + end.hashCode()
|
||||
result = 31 * result + bottom.hashCode()
|
||||
result = 31 * result + rtlAware.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
val otherModifier = other as? PaddingModifier ?: return false
|
||||
return start == otherModifier.start &&
|
||||
top == otherModifier.top &&
|
||||
end == otherModifier.end &&
|
||||
bottom == otherModifier.bottom &&
|
||||
rtlAware == otherModifier.rtlAware
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.modifiers
|
||||
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.IntrinsicMeasurable
|
||||
import androidx.compose.ui.layout.IntrinsicMeasureScope
|
||||
import androidx.compose.ui.layout.LayoutModifier
|
||||
import androidx.compose.ui.layout.Measurable
|
||||
import androidx.compose.ui.layout.MeasureResult
|
||||
import androidx.compose.ui.layout.MeasureScope
|
||||
import androidx.compose.ui.platform.InspectorInfo
|
||||
import androidx.compose.ui.platform.InspectorValueInfo
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.constrain
|
||||
import androidx.compose.ui.unit.constrainHeight
|
||||
import androidx.compose.ui.unit.constrainWidth
|
||||
|
||||
// This file was mostly copy pasted from androidx.compose.foundation.layout.Size.kt and contains
|
||||
// modifiers with lambda parameters to change the (min/max) size of a Composable without triggering
|
||||
// recomposition when the sizes change.
|
||||
//
|
||||
// These should be used instead of the traditional size modifiers when the size changes often, for
|
||||
// instance when it is animated.
|
||||
//
|
||||
// TODO(b/247473910): Remove these modifiers once they can be fully replaced by layout animations
|
||||
// APIs.
|
||||
|
||||
/** @see androidx.compose.foundation.layout.width */
|
||||
fun Modifier.width(width: Density.() -> Int) =
|
||||
this.then(
|
||||
SizeModifier(
|
||||
minWidth = width,
|
||||
maxWidth = width,
|
||||
enforceIncoming = true,
|
||||
inspectorInfo =
|
||||
debugInspectorInfo {
|
||||
name = "width"
|
||||
value = width
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
/** @see androidx.compose.foundation.layout.height */
|
||||
fun Modifier.height(height: Density.() -> Int) =
|
||||
this.then(
|
||||
SizeModifier(
|
||||
minHeight = height,
|
||||
maxHeight = height,
|
||||
enforceIncoming = true,
|
||||
inspectorInfo =
|
||||
debugInspectorInfo {
|
||||
name = "height"
|
||||
value = height
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
/** @see androidx.compose.foundation.layout.size */
|
||||
fun Modifier.size(width: Density.() -> Int, height: Density.() -> Int) =
|
||||
this.then(
|
||||
SizeModifier(
|
||||
minWidth = width,
|
||||
maxWidth = width,
|
||||
minHeight = height,
|
||||
maxHeight = height,
|
||||
enforceIncoming = true,
|
||||
inspectorInfo =
|
||||
debugInspectorInfo {
|
||||
name = "size"
|
||||
properties["width"] = width
|
||||
properties["height"] = height
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
private val SizeUnspecified: Density.() -> Int = { 0 }
|
||||
|
||||
private class SizeModifier(
|
||||
private val minWidth: Density.() -> Int = SizeUnspecified,
|
||||
private val minHeight: Density.() -> Int = SizeUnspecified,
|
||||
private val maxWidth: Density.() -> Int = SizeUnspecified,
|
||||
private val maxHeight: Density.() -> Int = SizeUnspecified,
|
||||
private val enforceIncoming: Boolean,
|
||||
inspectorInfo: InspectorInfo.() -> Unit
|
||||
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
|
||||
private val Density.targetConstraints: Constraints
|
||||
get() {
|
||||
val maxWidth =
|
||||
if (maxWidth != SizeUnspecified) {
|
||||
maxWidth().coerceAtLeast(0)
|
||||
} else {
|
||||
Constraints.Infinity
|
||||
}
|
||||
val maxHeight =
|
||||
if (maxHeight != SizeUnspecified) {
|
||||
maxHeight().coerceAtLeast(0)
|
||||
} else {
|
||||
Constraints.Infinity
|
||||
}
|
||||
val minWidth =
|
||||
if (minWidth != SizeUnspecified) {
|
||||
minWidth().coerceAtMost(maxWidth).coerceAtLeast(0).let {
|
||||
if (it != Constraints.Infinity) it else 0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
val minHeight =
|
||||
if (minHeight != SizeUnspecified) {
|
||||
minHeight().coerceAtMost(maxHeight).coerceAtLeast(0).let {
|
||||
if (it != Constraints.Infinity) it else 0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
return Constraints(
|
||||
minWidth = minWidth,
|
||||
minHeight = minHeight,
|
||||
maxWidth = maxWidth,
|
||||
maxHeight = maxHeight
|
||||
)
|
||||
}
|
||||
|
||||
override fun MeasureScope.measure(
|
||||
measurable: Measurable,
|
||||
constraints: Constraints
|
||||
): MeasureResult {
|
||||
val wrappedConstraints =
|
||||
targetConstraints.let { targetConstraints ->
|
||||
if (enforceIncoming) {
|
||||
constraints.constrain(targetConstraints)
|
||||
} else {
|
||||
val resolvedMinWidth =
|
||||
if (minWidth != SizeUnspecified) {
|
||||
targetConstraints.minWidth
|
||||
} else {
|
||||
constraints.minWidth.coerceAtMost(targetConstraints.maxWidth)
|
||||
}
|
||||
val resolvedMaxWidth =
|
||||
if (maxWidth != SizeUnspecified) {
|
||||
targetConstraints.maxWidth
|
||||
} else {
|
||||
constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth)
|
||||
}
|
||||
val resolvedMinHeight =
|
||||
if (minHeight != SizeUnspecified) {
|
||||
targetConstraints.minHeight
|
||||
} else {
|
||||
constraints.minHeight.coerceAtMost(targetConstraints.maxHeight)
|
||||
}
|
||||
val resolvedMaxHeight =
|
||||
if (maxHeight != SizeUnspecified) {
|
||||
targetConstraints.maxHeight
|
||||
} else {
|
||||
constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight)
|
||||
}
|
||||
Constraints(
|
||||
resolvedMinWidth,
|
||||
resolvedMaxWidth,
|
||||
resolvedMinHeight,
|
||||
resolvedMaxHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
val placeable = measurable.measure(wrappedConstraints)
|
||||
return layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
|
||||
}
|
||||
|
||||
override fun IntrinsicMeasureScope.minIntrinsicWidth(
|
||||
measurable: IntrinsicMeasurable,
|
||||
height: Int
|
||||
): Int {
|
||||
val constraints = targetConstraints
|
||||
return if (constraints.hasFixedWidth) {
|
||||
constraints.maxWidth
|
||||
} else {
|
||||
constraints.constrainWidth(measurable.minIntrinsicWidth(height))
|
||||
}
|
||||
}
|
||||
|
||||
override fun IntrinsicMeasureScope.minIntrinsicHeight(
|
||||
measurable: IntrinsicMeasurable,
|
||||
width: Int
|
||||
): Int {
|
||||
val constraints = targetConstraints
|
||||
return if (constraints.hasFixedHeight) {
|
||||
constraints.maxHeight
|
||||
} else {
|
||||
constraints.constrainHeight(measurable.minIntrinsicHeight(width))
|
||||
}
|
||||
}
|
||||
|
||||
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
|
||||
measurable: IntrinsicMeasurable,
|
||||
height: Int
|
||||
): Int {
|
||||
val constraints = targetConstraints
|
||||
return if (constraints.hasFixedWidth) {
|
||||
constraints.maxWidth
|
||||
} else {
|
||||
constraints.constrainWidth(measurable.maxIntrinsicWidth(height))
|
||||
}
|
||||
}
|
||||
|
||||
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
|
||||
measurable: IntrinsicMeasurable,
|
||||
width: Int
|
||||
): Int {
|
||||
val constraints = targetConstraints
|
||||
return if (constraints.hasFixedHeight) {
|
||||
constraints.maxHeight
|
||||
} else {
|
||||
constraints.constrainHeight(measurable.maxIntrinsicHeight(width))
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is SizeModifier) return false
|
||||
return minWidth == other.minWidth &&
|
||||
minHeight == other.minHeight &&
|
||||
maxWidth == other.maxWidth &&
|
||||
maxHeight == other.maxHeight &&
|
||||
enforceIncoming == other.enforceIncoming
|
||||
}
|
||||
|
||||
override fun hashCode() =
|
||||
(((((minWidth.hashCode() * 31 + minHeight.hashCode()) * 31) + maxWidth.hashCode()) * 31) +
|
||||
maxHeight.hashCode()) * 31
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright (C) 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 {
|
||||
// See: http://go/android-license-faq
|
||||
// A large-scale-change added 'default_applicable_licenses' to import
|
||||
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
|
||||
// to get the below license kinds:
|
||||
// SPDX-license-identifier-Apache-2.0
|
||||
default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
|
||||
}
|
||||
|
||||
android_library {
|
||||
name: "SystemUIComposeGalleryLib",
|
||||
manifest: "AndroidManifest.xml",
|
||||
|
||||
srcs: [
|
||||
"src/**/*.kt",
|
||||
":SystemUI-tests-utils",
|
||||
],
|
||||
|
||||
resource_dirs: [
|
||||
"res",
|
||||
],
|
||||
|
||||
static_libs: [
|
||||
"SystemUI-core",
|
||||
"SystemUIComposeCore",
|
||||
"SystemUIComposeFeatures",
|
||||
|
||||
"androidx.compose.runtime_runtime",
|
||||
"androidx.compose.material3_material3",
|
||||
"androidx.compose.material_material-icons-extended",
|
||||
"androidx.activity_activity-compose",
|
||||
"androidx.navigation_navigation-compose",
|
||||
|
||||
"androidx.appcompat_appcompat",
|
||||
|
||||
// TODO(b/240431193): Remove the dependencies and depend on
|
||||
// SystemUI-test-utils directly.
|
||||
"androidx.test.runner",
|
||||
"mockito-target-extended-minus-junit4",
|
||||
"testables",
|
||||
"truth-prebuilt",
|
||||
"androidx.test.uiautomator",
|
||||
"kotlinx_coroutines_test",
|
||||
],
|
||||
|
||||
libs: [
|
||||
"android.test.mock",
|
||||
],
|
||||
|
||||
kotlincflags: ["-Xjvm-default=all"],
|
||||
}
|
||||
|
||||
android_app {
|
||||
name: "SystemUIComposeGallery",
|
||||
defaults: ["platform_app_defaults"],
|
||||
manifest: "app/AndroidManifest.xml",
|
||||
|
||||
static_libs: [
|
||||
"SystemUIComposeGalleryLib",
|
||||
],
|
||||
|
||||
platform_apis: true,
|
||||
system_ext_specific: true,
|
||||
certificate: "platform",
|
||||
privileged: true,
|
||||
|
||||
optimize: {
|
||||
proguard_flags_files: ["proguard-rules.pro"],
|
||||
},
|
||||
|
||||
dxflags: ["--multi-dex"],
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.android.systemui.compose.gallery">
|
||||
<!-- To emulate a display size and density. -->
|
||||
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
|
||||
|
||||
<application
|
||||
android:name="android.app.Application"
|
||||
android:appComponentFactory="androidx.core.app.AppComponentFactory"
|
||||
tools:replace="android:name,android:appComponentFactory">
|
||||
<!-- Disable providers from SystemUI -->
|
||||
<provider android:name="com.android.systemui.keyguard.KeyguardSliceProvider"
|
||||
android:authorities="com.android.systemui.test.keyguard.disabled"
|
||||
android:enabled="false"
|
||||
tools:replace="android:authorities"
|
||||
tools:node="remove" />
|
||||
<provider android:name="com.google.android.systemui.keyguard.KeyguardSliceProviderGoogle"
|
||||
android:authorities="com.android.systemui.test.keyguard.disabled"
|
||||
android:enabled="false"
|
||||
tools:replace="android:authorities"
|
||||
tools:node="remove" />
|
||||
<provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
|
||||
android:authorities="com.android.systemui.test.keyguard.clock.disabled"
|
||||
android:enabled="false"
|
||||
tools:replace="android:authorities"
|
||||
tools:node="remove" />
|
||||
<provider android:name="com.android.systemui.people.PeopleProvider"
|
||||
android:authorities="com.android.systemui.test.people.disabled"
|
||||
android:enabled="false"
|
||||
tools:replace="android:authorities"
|
||||
tools:node="remove" />
|
||||
<provider android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="com.android.systemui.test.fileprovider.disabled"
|
||||
android:enabled="false"
|
||||
tools:replace="android:authorities"
|
||||
tools:node="remove"/>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"presubmit": [
|
||||
{
|
||||
"name": "SystemUIComposeGalleryTests",
|
||||
"options": [
|
||||
{
|
||||
"exclude-annotation": "org.junit.Ignore"
|
||||
},
|
||||
{
|
||||
"exclude-annotation": "androidx.test.filters.FlakyTest"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.android.systemui.compose.gallery.app">
|
||||
<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.SystemUI.Gallery"
|
||||
tools:replace="android:icon,android:theme,android:label">
|
||||
<activity
|
||||
android:name="com.android.systemui.compose.gallery.GalleryActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -1,21 +0,0 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -1,30 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
@@ -1,170 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportHeight="108"
|
||||
android:viewportWidth="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeColor="#33FFFFFF"
|
||||
android:strokeWidth="0.8" />
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 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.
|
||||
-->
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 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.
|
||||
-->
|
||||
<resources>
|
||||
<!-- Application name [CHAR LIMIT=NONE] -->
|
||||
<string name="app_name">SystemUI Gallery</string>
|
||||
</resources>
|
||||
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 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.
|
||||
-->
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.SystemUI.Gallery">
|
||||
<item name="android:windowActionBar">false</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
|
||||
<item name="android:statusBarColor" tools:targetApi="l">
|
||||
@android:color/transparent
|
||||
</item>
|
||||
<item name="android:navigationBarColor" tools:targetApi="l">
|
||||
@android:color/transparent
|
||||
</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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.
|
||||
*
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package com.android.systemui.compose.gallery
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.android.systemui.compose.SysUiButton
|
||||
import com.android.systemui.compose.SysUiOutlinedButton
|
||||
import com.android.systemui.compose.SysUiTextButton
|
||||
|
||||
@Composable
|
||||
fun ButtonsScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
SysUiButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Text("SysUiButton")
|
||||
}
|
||||
|
||||
SysUiButton(
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
) {
|
||||
Text("SysUiButton - disabled")
|
||||
}
|
||||
|
||||
SysUiOutlinedButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Text("SysUiOutlinedButton")
|
||||
}
|
||||
|
||||
SysUiOutlinedButton(
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
) {
|
||||
Text("SysUiOutlinedButton - disabled")
|
||||
}
|
||||
|
||||
SysUiTextButton(
|
||||
onClick = {},
|
||||
) {
|
||||
Text("SysUiTextButton")
|
||||
}
|
||||
|
||||
SysUiTextButton(
|
||||
onClick = {},
|
||||
enabled = false,
|
||||
) {
|
||||
Text("SysUiTextButton - disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.gallery
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.android.systemui.compose.theme.LocalAndroidColorScheme
|
||||
|
||||
/** The screen that shows all the Material 3 colors. */
|
||||
@Composable
|
||||
fun MaterialColorsScreen() {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
ColorsScreen(
|
||||
listOf(
|
||||
"primary" to colors.primary,
|
||||
"onPrimary" to colors.onPrimary,
|
||||
"primaryContainer" to colors.primaryContainer,
|
||||
"onPrimaryContainer" to colors.onPrimaryContainer,
|
||||
"inversePrimary" to colors.inversePrimary,
|
||||
"secondary" to colors.secondary,
|
||||
"onSecondary" to colors.onSecondary,
|
||||
"secondaryContainer" to colors.secondaryContainer,
|
||||
"onSecondaryContainer" to colors.onSecondaryContainer,
|
||||
"tertiary" to colors.tertiary,
|
||||
"onTertiary" to colors.onTertiary,
|
||||
"tertiaryContainer" to colors.tertiaryContainer,
|
||||
"onTertiaryContainer" to colors.onTertiaryContainer,
|
||||
"background" to colors.background,
|
||||
"onBackground" to colors.onBackground,
|
||||
"surface" to colors.surface,
|
||||
"onSurface" to colors.onSurface,
|
||||
"surfaceVariant" to colors.surfaceVariant,
|
||||
"onSurfaceVariant" to colors.onSurfaceVariant,
|
||||
"inverseSurface" to colors.inverseSurface,
|
||||
"inverseOnSurface" to colors.inverseOnSurface,
|
||||
"error" to colors.error,
|
||||
"onError" to colors.onError,
|
||||
"errorContainer" to colors.errorContainer,
|
||||
"onErrorContainer" to colors.onErrorContainer,
|
||||
"outline" to colors.outline,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/** The screen that shows all the Android colors. */
|
||||
@Composable
|
||||
fun AndroidColorsScreen() {
|
||||
val colors = LocalAndroidColorScheme.current
|
||||
ColorsScreen(
|
||||
listOf(
|
||||
"colorPrimary" to colors.colorPrimary,
|
||||
"colorPrimaryDark" to colors.colorPrimaryDark,
|
||||
"colorAccent" to colors.colorAccent,
|
||||
"colorAccentPrimary" to colors.colorAccentPrimary,
|
||||
"colorAccentSecondary" to colors.colorAccentSecondary,
|
||||
"colorAccentTertiary" to colors.colorAccentTertiary,
|
||||
"colorAccentPrimaryVariant" to colors.colorAccentPrimaryVariant,
|
||||
"colorAccentSecondaryVariant" to colors.colorAccentSecondaryVariant,
|
||||
"colorAccentTertiaryVariant" to colors.colorAccentTertiaryVariant,
|
||||
"colorSurface" to colors.colorSurface,
|
||||
"colorSurfaceHighlight" to colors.colorSurfaceHighlight,
|
||||
"colorSurfaceVariant" to colors.colorSurfaceVariant,
|
||||
"colorSurfaceHeader" to colors.colorSurfaceHeader,
|
||||
"colorError" to colors.colorError,
|
||||
"colorBackground" to colors.colorBackground,
|
||||
"colorBackgroundFloating" to colors.colorBackgroundFloating,
|
||||
"panelColorBackground" to colors.panelColorBackground,
|
||||
"textColorPrimary" to colors.textColorPrimary,
|
||||
"textColorSecondary" to colors.textColorSecondary,
|
||||
"textColorTertiary" to colors.textColorTertiary,
|
||||
"textColorPrimaryInverse" to colors.textColorPrimaryInverse,
|
||||
"textColorSecondaryInverse" to colors.textColorSecondaryInverse,
|
||||
"textColorTertiaryInverse" to colors.textColorTertiaryInverse,
|
||||
"textColorOnAccent" to colors.textColorOnAccent,
|
||||
"colorForeground" to colors.colorForeground,
|
||||
"colorForegroundInverse" to colors.colorForegroundInverse,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColorsScreen(
|
||||
colors: List<Pair<String, Color>>,
|
||||
) {
|
||||
LazyColumn(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
colors.forEach { (name, color) -> item { ColorTile(color, name) } }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColorTile(
|
||||
color: Color,
|
||||
name: String,
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
val shape = RoundedCornerShape(16.dp)
|
||||
Spacer(
|
||||
Modifier.border(1.dp, MaterialTheme.colorScheme.onBackground, shape)
|
||||
.background(color, shape)
|
||||
.size(64.dp)
|
||||
)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Text(name)
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
package com.android.systemui.compose.gallery
|
||||
|
||||
import android.graphics.Point
|
||||
import android.os.UserHandle
|
||||
import android.view.Display
|
||||
import android.view.WindowManagerGlobal
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DarkMode
|
||||
import androidx.compose.material.icons.filled.FormatSize
|
||||
import androidx.compose.material.icons.filled.FormatTextdirectionLToR
|
||||
import androidx.compose.material.icons.filled.FormatTextdirectionRToL
|
||||
import androidx.compose.material.icons.filled.InvertColors
|
||||
import androidx.compose.material.icons.filled.LightMode
|
||||
import androidx.compose.material.icons.filled.Smartphone
|
||||
import androidx.compose.material.icons.filled.Tablet
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
enum class FontScale(val scale: Float) {
|
||||
Small(0.85f),
|
||||
Normal(1f),
|
||||
Big(1.15f),
|
||||
Bigger(1.30f),
|
||||
}
|
||||
|
||||
/** A configuration panel that allows to toggle the theme, font scale and layout direction. */
|
||||
@Composable
|
||||
fun ConfigurationControls(
|
||||
theme: Theme,
|
||||
fontScale: FontScale,
|
||||
layoutDirection: LayoutDirection,
|
||||
onChangeTheme: () -> Unit,
|
||||
onChangeLayoutDirection: () -> Unit,
|
||||
onChangeFontScale: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
// The display we are emulating, if any.
|
||||
var emulatedDisplayName by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val emulatedDisplay =
|
||||
emulatedDisplayName?.let { name -> EmulatedDisplays.firstOrNull { it.name == name } }
|
||||
|
||||
LaunchedEffect(emulatedDisplay) {
|
||||
val wm = WindowManagerGlobal.getWindowManagerService()
|
||||
|
||||
val defaultDisplayId = Display.DEFAULT_DISPLAY
|
||||
if (emulatedDisplay == null) {
|
||||
wm.clearForcedDisplayDensityForUser(defaultDisplayId, UserHandle.myUserId())
|
||||
wm.clearForcedDisplaySize(defaultDisplayId)
|
||||
} else {
|
||||
val density = emulatedDisplay.densityDpi
|
||||
|
||||
// Emulate the display and make sure that we use the maximum available space possible.
|
||||
val initialSize = Point()
|
||||
wm.getInitialDisplaySize(defaultDisplayId, initialSize)
|
||||
val width = emulatedDisplay.width
|
||||
val height = emulatedDisplay.height
|
||||
val minOfSize = min(width, height)
|
||||
val maxOfSize = max(width, height)
|
||||
if (initialSize.x < initialSize.y) {
|
||||
wm.setForcedDisplaySize(defaultDisplayId, minOfSize, maxOfSize)
|
||||
} else {
|
||||
wm.setForcedDisplaySize(defaultDisplayId, maxOfSize, minOfSize)
|
||||
}
|
||||
wm.setForcedDisplayDensityForUser(defaultDisplayId, density, UserHandle.myUserId())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(b/231131244): Fork FlowRow from Accompanist and use that instead to make sure that users
|
||||
// don't miss any available configuration.
|
||||
LazyRow(modifier) {
|
||||
// Dark/light theme.
|
||||
item {
|
||||
TextButton(onChangeTheme) {
|
||||
val text: String
|
||||
val icon: ImageVector
|
||||
|
||||
when (theme) {
|
||||
Theme.System -> {
|
||||
icon = Icons.Default.InvertColors
|
||||
text = "System"
|
||||
}
|
||||
Theme.Dark -> {
|
||||
icon = Icons.Default.DarkMode
|
||||
text = "Dark"
|
||||
}
|
||||
Theme.Light -> {
|
||||
icon = Icons.Default.LightMode
|
||||
text = "Light"
|
||||
}
|
||||
}
|
||||
|
||||
Icon(icon, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Font scale.
|
||||
item {
|
||||
TextButton(onChangeFontScale) {
|
||||
Icon(Icons.Default.FormatSize, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
Text(fontScale.name)
|
||||
}
|
||||
}
|
||||
|
||||
// Layout direction.
|
||||
item {
|
||||
TextButton(onChangeLayoutDirection) {
|
||||
when (layoutDirection) {
|
||||
LayoutDirection.Ltr -> {
|
||||
Icon(Icons.Default.FormatTextdirectionLToR, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("LTR")
|
||||
}
|
||||
LayoutDirection.Rtl -> {
|
||||
Icon(Icons.Default.FormatTextdirectionRToL, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text("RTL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display emulation.
|
||||
EmulatedDisplays.forEach { display ->
|
||||
item {
|
||||
DisplayButton(
|
||||
display,
|
||||
emulatedDisplay == display,
|
||||
{ emulatedDisplayName = it?.name },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayButton(
|
||||
display: EmulatedDisplay,
|
||||
selected: Boolean,
|
||||
onChangeEmulatedDisplay: (EmulatedDisplay?) -> Unit,
|
||||
) {
|
||||
val onClick = {
|
||||
if (selected) {
|
||||
onChangeEmulatedDisplay(null)
|
||||
} else {
|
||||
onChangeEmulatedDisplay(display)
|
||||
}
|
||||
}
|
||||
|
||||
val content: @Composable RowScope.() -> Unit = {
|
||||
Icon(display.icon, null)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(display.name)
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
Button(onClick, contentPadding = ButtonDefaults.TextButtonContentPadding, content = content)
|
||||
} else {
|
||||
TextButton(onClick, content = content)
|
||||
}
|
||||
}
|
||||
|
||||
/** The displays that can be emulated from this Gallery app. */
|
||||
private val EmulatedDisplays =
|
||||
listOf(
|
||||
EmulatedDisplay(
|
||||
"Phone",
|
||||
Icons.Default.Smartphone,
|
||||
width = 1440,
|
||||
height = 3120,
|
||||
densityDpi = 560,
|
||||
),
|
||||
EmulatedDisplay(
|
||||
"Tablet",
|
||||
Icons.Default.Tablet,
|
||||
width = 2560,
|
||||
height = 1600,
|
||||
densityDpi = 320,
|
||||
),
|
||||
)
|
||||
|
||||
private data class EmulatedDisplay(
|
||||
val name: String,
|
||||
val icon: ImageVector,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val densityDpi: Int,
|
||||
)
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.gallery
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.android.systemui.ExampleFeature
|
||||
|
||||
/** The screen that shows ExampleFeature. */
|
||||
@Composable
|
||||
fun ExampleFeatureScreen(modifier: Modifier = Modifier) {
|
||||
Column(modifier) { ExampleFeature("This is an example feature!") }
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.gallery
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.android.systemui.compose.rememberSystemUiController
|
||||
|
||||
class GalleryActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
|
||||
|
||||
setContent {
|
||||
var theme by rememberSaveable { mutableStateOf(Theme.System) }
|
||||
val onChangeTheme = {
|
||||
// Change to the next theme for a toggle behavior.
|
||||
theme =
|
||||
when (theme) {
|
||||
Theme.System -> Theme.Dark
|
||||
Theme.Dark -> Theme.Light
|
||||
Theme.Light -> Theme.System
|
||||
}
|
||||
}
|
||||
|
||||
val isSystemInDarkTheme = isSystemInDarkTheme()
|
||||
val isDark = theme == Theme.Dark || (theme == Theme.System && isSystemInDarkTheme)
|
||||
val useDarkIcons = !isDark
|
||||
val systemUiController = rememberSystemUiController()
|
||||
SideEffect {
|
||||
systemUiController.setSystemBarsColor(
|
||||
color = Color.Transparent,
|
||||
darkIcons = useDarkIcons,
|
||||
)
|
||||
|
||||
uiModeManager.setApplicationNightMode(
|
||||
when (theme) {
|
||||
Theme.System -> UiModeManager.MODE_NIGHT_AUTO
|
||||
Theme.Dark -> UiModeManager.MODE_NIGHT_YES
|
||||
Theme.Light -> UiModeManager.MODE_NIGHT_NO
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
GalleryApp(theme, onChangeTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Theme {
|
||||
System,
|
||||
Dark,
|
||||
Light,
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
package com.android.systemui.compose.gallery
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.android.systemui.compose.theme.SystemUITheme
|
||||
|
||||
/** The gallery app screens. */
|
||||
object GalleryAppScreens {
|
||||
private val Typography = ChildScreen("typography") { TypographyScreen() }
|
||||
private val MaterialColors = ChildScreen("material_colors") { MaterialColorsScreen() }
|
||||
private val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
|
||||
private val Buttons = ChildScreen("buttons") { ButtonsScreen() }
|
||||
private val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() }
|
||||
|
||||
private val PeopleEmpty =
|
||||
ChildScreen("people_empty") { navController ->
|
||||
EmptyPeopleScreen(onResult = { navController.popBackStack() })
|
||||
}
|
||||
private val PeopleFew =
|
||||
ChildScreen("people_few") { navController ->
|
||||
FewPeopleScreen(onResult = { navController.popBackStack() })
|
||||
}
|
||||
private val PeopleFull =
|
||||
ChildScreen("people_full") { navController ->
|
||||
FullPeopleScreen(onResult = { navController.popBackStack() })
|
||||
}
|
||||
private val People =
|
||||
ParentScreen(
|
||||
"people",
|
||||
mapOf(
|
||||
"Empty" to PeopleEmpty,
|
||||
"Few" to PeopleFew,
|
||||
"Full" to PeopleFull,
|
||||
)
|
||||
)
|
||||
private val UserSwitcherSingleUser =
|
||||
ChildScreen("user_switcher_single") { navController ->
|
||||
UserSwitcherScreen(
|
||||
userCount = 1,
|
||||
onFinished = navController::popBackStack,
|
||||
)
|
||||
}
|
||||
private val UserSwitcherThreeUsers =
|
||||
ChildScreen("user_switcher_three") { navController ->
|
||||
UserSwitcherScreen(
|
||||
userCount = 3,
|
||||
onFinished = navController::popBackStack,
|
||||
)
|
||||
}
|
||||
private val UserSwitcherFourUsers =
|
||||
ChildScreen("user_switcher_four") { navController ->
|
||||
UserSwitcherScreen(
|
||||
userCount = 4,
|
||||
onFinished = navController::popBackStack,
|
||||
)
|
||||
}
|
||||
private val UserSwitcherFiveUsers =
|
||||
ChildScreen("user_switcher_five") { navController ->
|
||||
UserSwitcherScreen(
|
||||
userCount = 5,
|
||||
onFinished = navController::popBackStack,
|
||||
)
|
||||
}
|
||||
private val UserSwitcherSixUsers =
|
||||
ChildScreen("user_switcher_six") { navController ->
|
||||
UserSwitcherScreen(
|
||||
userCount = 6,
|
||||
onFinished = navController::popBackStack,
|
||||
)
|
||||
}
|
||||
private val UserSwitcher =
|
||||
ParentScreen(
|
||||
"user_switcher",
|
||||
mapOf(
|
||||
"Single" to UserSwitcherSingleUser,
|
||||
"Three" to UserSwitcherThreeUsers,
|
||||
"Four" to UserSwitcherFourUsers,
|
||||
"Five" to UserSwitcherFiveUsers,
|
||||
"Six" to UserSwitcherSixUsers,
|
||||
)
|
||||
)
|
||||
|
||||
val Home =
|
||||
ParentScreen(
|
||||
"home",
|
||||
mapOf(
|
||||
"Typography" to Typography,
|
||||
"Material colors" to MaterialColors,
|
||||
"Android colors" to AndroidColors,
|
||||
"Example feature" to ExampleFeature,
|
||||
"Buttons" to Buttons,
|
||||
"People" to People,
|
||||
"User Switcher" to UserSwitcher,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/** The main content of the app, that shows [GalleryAppScreens.Home] by default. */
|
||||
@Composable
|
||||
private fun MainContent(onControlToggleRequested: () -> Unit) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
val navController = rememberNavController()
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = GalleryAppScreens.Home.identifier,
|
||||
) {
|
||||
screen(GalleryAppScreens.Home, navController, onControlToggleRequested)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The top-level composable shown when starting the app. This composable always shows a
|
||||
* [ConfigurationControls] at the top of the screen, above the [MainContent].
|
||||
*/
|
||||
@Composable
|
||||
fun GalleryApp(
|
||||
theme: Theme,
|
||||
onChangeTheme: () -> Unit,
|
||||
) {
|
||||
val systemFontScale = LocalDensity.current.fontScale
|
||||
var fontScale: FontScale by rememberSaveable {
|
||||
mutableStateOf(
|
||||
FontScale.values().firstOrNull { it.scale == systemFontScale } ?: FontScale.Normal
|
||||
)
|
||||
}
|
||||
val context = LocalContext.current
|
||||
val density = Density(context.resources.displayMetrics.density, fontScale.scale)
|
||||
val onChangeFontScale = {
|
||||
fontScale =
|
||||
when (fontScale) {
|
||||
FontScale.Small -> FontScale.Normal
|
||||
FontScale.Normal -> FontScale.Big
|
||||
FontScale.Big -> FontScale.Bigger
|
||||
FontScale.Bigger -> FontScale.Small
|
||||
}
|
||||
}
|
||||
|
||||
val systemLayoutDirection = LocalLayoutDirection.current
|
||||
var layoutDirection by rememberSaveable { mutableStateOf(systemLayoutDirection) }
|
||||
val onChangeLayoutDirection = {
|
||||
layoutDirection =
|
||||
when (layoutDirection) {
|
||||
LayoutDirection.Ltr -> LayoutDirection.Rtl
|
||||
LayoutDirection.Rtl -> LayoutDirection.Ltr
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalDensity provides density,
|
||||
LocalLayoutDirection provides layoutDirection,
|
||||
) {
|
||||
SystemUITheme {
|
||||
Surface(
|
||||
Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
) {
|
||||
Column(Modifier.fillMaxSize().systemBarsPadding()) {
|
||||
var showControls by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
if (showControls) {
|
||||
ConfigurationControls(
|
||||
theme,
|
||||
fontScale,
|
||||
layoutDirection,
|
||||
onChangeTheme,
|
||||
onChangeLayoutDirection,
|
||||
onChangeFontScale,
|
||||
Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
MainContent(onControlToggleRequested = { showControls = !showControls })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.gallery
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.android.systemui.people.emptyPeopleSpaceViewModel
|
||||
import com.android.systemui.people.fewPeopleSpaceViewModel
|
||||
import com.android.systemui.people.fullPeopleSpaceViewModel
|
||||
import com.android.systemui.people.ui.compose.PeopleScreen
|
||||
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
||||
|
||||
@Composable
|
||||
fun EmptyPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
|
||||
val context = LocalContext.current.applicationContext
|
||||
val viewModel = emptyPeopleSpaceViewModel(context)
|
||||
PeopleScreen(viewModel, onResult)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FewPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
|
||||
val context = LocalContext.current.applicationContext
|
||||
val viewModel = fewPeopleSpaceViewModel(context)
|
||||
PeopleScreen(viewModel, onResult)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FullPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
|
||||
val context = LocalContext.current.applicationContext
|
||||
val viewModel = fullPeopleSpaceViewModel(context)
|
||||
PeopleScreen(viewModel, onResult)
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.gallery
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.navigation
|
||||
|
||||
/**
|
||||
* A screen in an app. It is either an [ParentScreen] which lists its child screens to navigate to
|
||||
* them or a [ChildScreen] which shows some content.
|
||||
*/
|
||||
sealed class Screen(val identifier: String)
|
||||
|
||||
class ParentScreen(
|
||||
identifier: String,
|
||||
val children: Map<String, Screen>,
|
||||
) : Screen(identifier)
|
||||
|
||||
class ChildScreen(
|
||||
identifier: String,
|
||||
val content: @Composable (NavController) -> Unit,
|
||||
) : Screen(identifier)
|
||||
|
||||
/** Create the navigation graph for [screen]. */
|
||||
fun NavGraphBuilder.screen(
|
||||
screen: Screen,
|
||||
navController: NavController,
|
||||
onControlToggleRequested: () -> Unit,
|
||||
) {
|
||||
when (screen) {
|
||||
is ChildScreen -> composable(screen.identifier) { screen.content(navController) }
|
||||
is ParentScreen -> {
|
||||
val menuRoute = "${screen.identifier}_menu"
|
||||
navigation(startDestination = menuRoute, route = screen.identifier) {
|
||||
// The menu to navigate to one of the children screens.
|
||||
composable(menuRoute) {
|
||||
ScreenMenu(screen, navController, onControlToggleRequested)
|
||||
}
|
||||
|
||||
// The content of the child screens.
|
||||
screen.children.forEach { (_, child) ->
|
||||
screen(
|
||||
child,
|
||||
navController,
|
||||
onControlToggleRequested,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenMenu(
|
||||
screen: ParentScreen,
|
||||
navController: NavController,
|
||||
onControlToggleRequested: () -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
Modifier.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
item {
|
||||
Surface(
|
||||
Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
shape = CircleShape,
|
||||
) {
|
||||
Column(
|
||||
Modifier.clickable(onClick = onControlToggleRequested).padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text("Toggle controls")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screen.children.forEach { (name, child) ->
|
||||
item {
|
||||
Surface(
|
||||
Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
shape = CircleShape,
|
||||
) {
|
||||
Column(
|
||||
Modifier.clickable { navController.navigate(child.identifier) }
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.gallery
|
||||
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
||||
/** The screen that shows the Material text styles. */
|
||||
@Composable
|
||||
fun TypographyScreen() {
|
||||
val typography = MaterialTheme.typography
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxSize()
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
FontLine("displayLarge", typography.displayLarge)
|
||||
FontLine("displayMedium", typography.displayMedium)
|
||||
FontLine("displaySmall", typography.displaySmall)
|
||||
FontLine("headlineLarge", typography.headlineLarge)
|
||||
FontLine("headlineMedium", typography.headlineMedium)
|
||||
FontLine("headlineSmall", typography.headlineSmall)
|
||||
FontLine("titleLarge", typography.titleLarge)
|
||||
FontLine("titleMedium", typography.titleMedium)
|
||||
FontLine("titleSmall", typography.titleSmall)
|
||||
FontLine("bodyLarge", typography.bodyLarge)
|
||||
FontLine("bodyMedium", typography.bodyMedium)
|
||||
FontLine("bodySmall", typography.bodySmall)
|
||||
FontLine("labelLarge", typography.labelLarge)
|
||||
FontLine("labelMedium", typography.labelMedium)
|
||||
FontLine("labelSmall", typography.labelSmall)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FontLine(name: String, style: TextStyle) {
|
||||
Text(
|
||||
"$name (${style.fontSize}/${style.lineHeight}, W${style.fontWeight?.weight})",
|
||||
style = style,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.gallery
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.android.systemui.user.Fakes.fakeUserSwitcherViewModel
|
||||
import com.android.systemui.user.ui.compose.UserSwitcherScreen
|
||||
|
||||
@Composable
|
||||
fun UserSwitcherScreen(
|
||||
userCount: Int,
|
||||
onFinished: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current.applicationContext
|
||||
UserSwitcherScreen(
|
||||
viewModel = fakeUserSwitcherViewModel(context, userCount = userCount),
|
||||
onFinished = onFinished,
|
||||
)
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.people
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.drawable.Icon
|
||||
import androidx.core.graphics.drawable.toIcon
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.people.data.model.PeopleTileModel
|
||||
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
|
||||
import com.android.systemui.people.widget.PeopleTileKey
|
||||
|
||||
/** A [PeopleViewModel] that does not have any conversations. */
|
||||
fun emptyPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
|
||||
return fakePeopleSpaceViewModel(context, emptyList(), emptyList())
|
||||
}
|
||||
|
||||
/** A [PeopleViewModel] that has a few conversations. */
|
||||
fun fewPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
|
||||
return fakePeopleSpaceViewModel(
|
||||
context,
|
||||
priorityTiles =
|
||||
listOf(
|
||||
fakeTile(context, id = "0", Color.RED, "Priority"),
|
||||
fakeTile(context, id = "1", Color.BLUE, "Priority NewStory", hasNewStory = true),
|
||||
),
|
||||
recentTiles =
|
||||
listOf(
|
||||
fakeTile(context, id = "2", Color.GREEN, "Recent Important", isImportant = true),
|
||||
fakeTile(context, id = "3", Color.CYAN, "Recent DndBlocking", isDndBlocking = true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/** A [PeopleViewModel] that has a lot of conversations. */
|
||||
fun fullPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
|
||||
return fakePeopleSpaceViewModel(
|
||||
context,
|
||||
priorityTiles =
|
||||
listOf(
|
||||
fakeTile(context, id = "0", Color.RED, "Priority"),
|
||||
fakeTile(context, id = "1", Color.BLUE, "Priority NewStory", hasNewStory = true),
|
||||
fakeTile(context, id = "2", Color.GREEN, "Priority Important", isImportant = true),
|
||||
fakeTile(
|
||||
context,
|
||||
id = "3",
|
||||
Color.CYAN,
|
||||
"Priority DndBlocking",
|
||||
isDndBlocking = true,
|
||||
),
|
||||
fakeTile(
|
||||
context,
|
||||
id = "4",
|
||||
Color.MAGENTA,
|
||||
"Priority NewStory Important",
|
||||
hasNewStory = true,
|
||||
isImportant = true,
|
||||
),
|
||||
),
|
||||
recentTiles =
|
||||
listOf(
|
||||
fakeTile(
|
||||
context,
|
||||
id = "5",
|
||||
Color.RED,
|
||||
"Recent NewStory DndBlocking",
|
||||
hasNewStory = true,
|
||||
isDndBlocking = true,
|
||||
),
|
||||
fakeTile(
|
||||
context,
|
||||
id = "6",
|
||||
Color.BLUE,
|
||||
"Recent Important DndBlocking",
|
||||
isImportant = true,
|
||||
isDndBlocking = true,
|
||||
),
|
||||
fakeTile(
|
||||
context,
|
||||
id = "7",
|
||||
Color.GREEN,
|
||||
"Recent NewStory Important DndBlocking",
|
||||
hasNewStory = true,
|
||||
isImportant = true,
|
||||
isDndBlocking = true,
|
||||
),
|
||||
fakeTile(context, id = "8", Color.CYAN, "Recent"),
|
||||
fakeTile(context, id = "9", Color.MAGENTA, "Recent"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun fakePeopleSpaceViewModel(
|
||||
@Application context: Context,
|
||||
priorityTiles: List<PeopleTileModel>,
|
||||
recentTiles: List<PeopleTileModel>,
|
||||
): PeopleViewModel {
|
||||
return PeopleViewModel(
|
||||
context,
|
||||
FakePeopleTileRepository(priorityTiles, recentTiles),
|
||||
FakePeopleWidgetRepository(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun fakeTile(
|
||||
@Application context: Context,
|
||||
id: String,
|
||||
iconColor: Int,
|
||||
username: String,
|
||||
hasNewStory: Boolean = false,
|
||||
isImportant: Boolean = false,
|
||||
isDndBlocking: Boolean = false
|
||||
): PeopleTileModel {
|
||||
return PeopleTileModel(
|
||||
PeopleTileKey(id, /* userId= */ 0, /* packageName */ ""),
|
||||
username,
|
||||
fakeUserIcon(context, iconColor),
|
||||
hasNewStory,
|
||||
isImportant,
|
||||
isDndBlocking,
|
||||
)
|
||||
}
|
||||
|
||||
private fun fakeUserIcon(@Application context: Context, color: Int): Icon {
|
||||
val size = context.resources.getDimensionPixelSize(R.dimen.avatar_size_for_medium)
|
||||
val bitmap =
|
||||
Bitmap.createBitmap(
|
||||
size,
|
||||
size,
|
||||
Bitmap.Config.ARGB_8888,
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
val paint = Paint().apply { this.color = color }
|
||||
val radius = size / 2f
|
||||
canvas.drawCircle(/* cx= */ radius, /* cy= */ radius, /* radius= */ radius, paint)
|
||||
return bitmap.toIcon()
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.qs.footer
|
||||
|
||||
import android.content.Context
|
||||
import android.os.UserHandle
|
||||
import android.view.View
|
||||
import com.android.internal.util.UserIcons
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.animation.Expandable
|
||||
import com.android.systemui.classifier.FalsingManagerFake
|
||||
import com.android.systemui.common.shared.model.Icon
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.globalactions.GlobalActionsDialogLite
|
||||
import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
|
||||
import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
|
||||
import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig
|
||||
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
/** A list of fake [FooterActionsViewModel] to be used in screenshot tests and the gallery. */
|
||||
fun fakeFooterActionsViewModels(
|
||||
@Application context: Context,
|
||||
): List<FooterActionsViewModel> {
|
||||
return listOf(
|
||||
fakeFooterActionsViewModel(context),
|
||||
fakeFooterActionsViewModel(context, showPowerButton = false, isGuestUser = true),
|
||||
fakeFooterActionsViewModel(context, showUserSwitcher = false),
|
||||
fakeFooterActionsViewModel(context, showUserSwitcher = false, foregroundServices = 4),
|
||||
fakeFooterActionsViewModel(
|
||||
context,
|
||||
foregroundServices = 4,
|
||||
hasNewForegroundServices = true,
|
||||
userId = 1,
|
||||
),
|
||||
fakeFooterActionsViewModel(
|
||||
context,
|
||||
securityText = "Security",
|
||||
foregroundServices = 4,
|
||||
showUserSwitcher = false,
|
||||
),
|
||||
fakeFooterActionsViewModel(
|
||||
context,
|
||||
securityText = "Security (not clickable)",
|
||||
securityClickable = false,
|
||||
foregroundServices = 4,
|
||||
hasNewForegroundServices = true,
|
||||
userId = 2,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun fakeFooterActionsViewModel(
|
||||
@Application context: Context,
|
||||
securityText: String? = null,
|
||||
securityClickable: Boolean = true,
|
||||
foregroundServices: Int = 0,
|
||||
hasNewForegroundServices: Boolean = false,
|
||||
showUserSwitcher: Boolean = true,
|
||||
showPowerButton: Boolean = true,
|
||||
userId: Int = UserHandle.USER_OWNER,
|
||||
isGuestUser: Boolean = false,
|
||||
): FooterActionsViewModel {
|
||||
val interactor =
|
||||
FakeFooterActionsInteractor(
|
||||
securityButtonConfig =
|
||||
flowOf(
|
||||
securityText?.let { text ->
|
||||
SecurityButtonConfig(
|
||||
icon =
|
||||
Icon.Resource(
|
||||
R.drawable.ic_info_outline,
|
||||
contentDescription = null,
|
||||
),
|
||||
text = text,
|
||||
isClickable = securityClickable,
|
||||
)
|
||||
}
|
||||
),
|
||||
foregroundServicesCount = flowOf(foregroundServices),
|
||||
hasNewForegroundServices = flowOf(hasNewForegroundServices),
|
||||
userSwitcherStatus =
|
||||
flowOf(
|
||||
if (showUserSwitcher) {
|
||||
UserSwitcherStatusModel.Enabled(
|
||||
currentUserName = "foo",
|
||||
currentUserImage =
|
||||
UserIcons.getDefaultUserIcon(
|
||||
context.resources,
|
||||
userId,
|
||||
/* light= */ false,
|
||||
),
|
||||
isGuestUser = isGuestUser,
|
||||
)
|
||||
} else {
|
||||
UserSwitcherStatusModel.Disabled
|
||||
}
|
||||
),
|
||||
deviceMonitoringDialogRequests = flowOf(),
|
||||
)
|
||||
|
||||
return FooterActionsViewModel(
|
||||
context,
|
||||
interactor,
|
||||
FalsingManagerFake(),
|
||||
globalActionsDialogLite = mock(),
|
||||
showPowerButton = showPowerButton,
|
||||
)
|
||||
}
|
||||
|
||||
private class FakeFooterActionsInteractor(
|
||||
override val securityButtonConfig: Flow<SecurityButtonConfig?> = flowOf(null),
|
||||
override val foregroundServicesCount: Flow<Int> = flowOf(0),
|
||||
override val hasNewForegroundServices: Flow<Boolean> = flowOf(false),
|
||||
override val userSwitcherStatus: Flow<UserSwitcherStatusModel> =
|
||||
flowOf(UserSwitcherStatusModel.Disabled),
|
||||
override val deviceMonitoringDialogRequests: Flow<Unit> = flowOf(),
|
||||
private val onShowDeviceMonitoringDialogFromView: (View) -> Unit = {},
|
||||
private val onShowDeviceMonitoringDialog: (Context) -> Unit = {},
|
||||
private val onShowForegroundServicesDialog: (View) -> Unit = {},
|
||||
private val onShowPowerMenuDialog: (GlobalActionsDialogLite, View) -> Unit = { _, _ -> },
|
||||
private val onShowSettings: (Expandable) -> Unit = {},
|
||||
private val onShowUserSwitcher: (View) -> Unit = {},
|
||||
) : FooterActionsInteractor {
|
||||
override fun showDeviceMonitoringDialog(view: View) {
|
||||
onShowDeviceMonitoringDialogFromView(view)
|
||||
}
|
||||
|
||||
override fun showDeviceMonitoringDialog(quickSettingsContext: Context) {
|
||||
onShowDeviceMonitoringDialog(quickSettingsContext)
|
||||
}
|
||||
|
||||
override fun showForegroundServicesDialog(view: View) {
|
||||
onShowForegroundServicesDialog(view)
|
||||
}
|
||||
|
||||
override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) {
|
||||
onShowPowerMenuDialog(globalActionsDialogLite, view)
|
||||
}
|
||||
|
||||
override fun showSettings(expandable: Expandable) {
|
||||
onShowSettings(expandable)
|
||||
}
|
||||
|
||||
override fun showUserSwitcher(view: View) {
|
||||
onShowUserSwitcher(view)
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.user
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import com.android.systemui.common.shared.model.Text
|
||||
import com.android.systemui.compose.gallery.R
|
||||
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
|
||||
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
|
||||
import com.android.systemui.power.data.repository.FakePowerRepository
|
||||
import com.android.systemui.power.domain.interactor.PowerInteractor
|
||||
import com.android.systemui.user.data.repository.FakeUserRepository
|
||||
import com.android.systemui.user.domain.interactor.UserInteractor
|
||||
import com.android.systemui.user.shared.model.UserActionModel
|
||||
import com.android.systemui.user.shared.model.UserModel
|
||||
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
|
||||
import com.android.systemui.util.mockito.mock
|
||||
|
||||
object Fakes {
|
||||
private val USER_TINT_COLORS =
|
||||
arrayOf(
|
||||
0x000000,
|
||||
0x0000ff,
|
||||
0x00ff00,
|
||||
0x00ffff,
|
||||
0xff0000,
|
||||
0xff00ff,
|
||||
0xffff00,
|
||||
0xffffff,
|
||||
)
|
||||
|
||||
fun fakeUserSwitcherViewModel(
|
||||
context: Context,
|
||||
userCount: Int,
|
||||
): UserSwitcherViewModel {
|
||||
return UserSwitcherViewModel.Factory(
|
||||
userInteractor =
|
||||
UserInteractor(
|
||||
repository =
|
||||
FakeUserRepository().apply {
|
||||
setUsers(
|
||||
(0 until userCount).map { index ->
|
||||
UserModel(
|
||||
id = index,
|
||||
name =
|
||||
Text.Loaded(
|
||||
when (index % 6) {
|
||||
0 -> "Ross Geller"
|
||||
1 -> "Phoebe Buffay"
|
||||
2 -> "Monica Geller"
|
||||
3 -> "Rachel Greene"
|
||||
4 -> "Chandler Bing"
|
||||
else -> "Joey Tribbiani"
|
||||
}
|
||||
),
|
||||
image =
|
||||
checkNotNull(
|
||||
AppCompatResources.getDrawable(
|
||||
context,
|
||||
when (index % 6) {
|
||||
0 -> R.drawable.kitten1
|
||||
1 -> R.drawable.kitten2
|
||||
2 -> R.drawable.kitten3
|
||||
3 -> R.drawable.kitten4
|
||||
4 -> R.drawable.kitten5
|
||||
else -> R.drawable.kitten6
|
||||
},
|
||||
)
|
||||
),
|
||||
isSelected = index == 0,
|
||||
isSelectable = true,
|
||||
)
|
||||
}
|
||||
)
|
||||
setActions(
|
||||
UserActionModel.values().mapNotNull {
|
||||
if (it == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) {
|
||||
null
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
controller = mock(),
|
||||
activityStarter = mock(),
|
||||
keyguardInteractor =
|
||||
KeyguardInteractor(
|
||||
repository =
|
||||
FakeKeyguardRepository().apply { setKeyguardShowing(false) },
|
||||
),
|
||||
),
|
||||
powerInteractor =
|
||||
PowerInteractor(
|
||||
repository = FakePowerRepository(),
|
||||
)
|
||||
)
|
||||
.create(UserSwitcherViewModel::class.java)
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (C) 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 {
|
||||
// See: http://go/android-license-faq
|
||||
// A large-scale-change added 'default_applicable_licenses' to import
|
||||
// all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
|
||||
// to get the below license kinds:
|
||||
// SPDX-license-identifier-Apache-2.0
|
||||
default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
|
||||
}
|
||||
|
||||
android_test {
|
||||
name: "SystemUIComposeGalleryTests",
|
||||
manifest: "AndroidManifest.xml",
|
||||
test_suites: ["device-tests"],
|
||||
sdk_version: "current",
|
||||
certificate: "platform",
|
||||
|
||||
srcs: [
|
||||
"src/**/*.kt",
|
||||
],
|
||||
|
||||
static_libs: [
|
||||
"SystemUIComposeGalleryLib",
|
||||
|
||||
"androidx.test.runner",
|
||||
"androidx.test.ext.junit",
|
||||
|
||||
"androidx.compose.runtime_runtime",
|
||||
"androidx.compose.ui_ui-test-junit4",
|
||||
"androidx.compose.ui_ui-test-manifest",
|
||||
],
|
||||
|
||||
kotlincflags: ["-Xjvm-default=enable"],
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 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.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.android.systemui.compose.gallery.tests" >
|
||||
|
||||
<application>
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
|
||||
android:targetPackage="com.android.systemui.compose.gallery.tests"
|
||||
android:label="Tests for SystemUIComposeGallery"/>
|
||||
|
||||
</manifest>
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 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.android.systemui.compose.gallery
|
||||
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.android.systemui.compose.theme.SystemUITheme
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ScreenshotsTests {
|
||||
@get:Rule val composeRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun exampleFeatureScreenshotTest() {
|
||||
// TODO(b/230832101): Wire this with the screenshot diff testing infra. We should reuse the
|
||||
// configuration of the features in the gallery app to populate the UIs.
|
||||
composeRule.setContent { SystemUITheme { ExampleFeatureScreen() } }
|
||||
}
|
||||
}
|
||||