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
This commit is contained in:
Jordan Demeulenaere
2022-09-23 10:49:50 +00:00
committed by Android (Google) Code Review
49 changed files with 1363 additions and 2052 deletions

View File

@@ -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
}

View File

@@ -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,
)
}
)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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"],
}

View File

@@ -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>

View File

@@ -1,15 +0,0 @@
{
"presubmit": [
{
"name": "SystemUIComposeGalleryTests",
"options": [
{
"exclude-annotation": "org.junit.Ignore"
},
{
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
}
]
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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")
}
}
}

View File

@@ -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)
}
}

View File

@@ -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,
)

View File

@@ -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!") }
}

View File

@@ -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,
}

View File

@@ -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 })
}
}
}
}
}

View File

@@ -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)
}

View File

@@ -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)
}
}
}
}
}
}

View File

@@ -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,
)
}

View File

@@ -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,
)
}

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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"],
}

View File

@@ -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>

View File

@@ -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() } }
}
}