Skip to content

Commit

Permalink
Add Vertical pager components (#1937)
Browse files Browse the repository at this point in the history
* Add VerticalPager components

* Add VerticalPager components

* Add VerticalPager components

* Add VerticalPager components

* 🤖 reformat

---------

Co-authored-by: yschimke <yschimke@users.noreply.github.com>
  • Loading branch information
yschimke and yschimke committed Jan 4, 2024
1 parent d5645c5 commit 95ecbfe
Show file tree
Hide file tree
Showing 16 changed files with 338 additions and 11 deletions.
12 changes: 12 additions & 0 deletions compose-layout/api/current.api
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,14 @@ package com.google.android.horologist.compose.pager {
method @androidx.compose.runtime.Composable public static void PagerScreen(optional androidx.compose.ui.Modifier modifier, androidx.compose.foundation.pager.PagerState state, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> content);
}

public final class VerticalPageIndicatorKt {
method @androidx.compose.runtime.Composable @com.google.android.horologist.annotations.ExperimentalHorologistApi public static void VerticalPageIndicator(androidx.wear.compose.material.PageIndicatorState pageIndicatorState, optional androidx.compose.ui.Modifier modifier, optional int indicatorStyle, optional long selectedColor, optional long unselectedColor, optional float indicatorSize, optional float spacing, optional androidx.compose.ui.graphics.Shape indicatorShape);
}

public final class VerticalPagerScreenKt {
method @androidx.compose.runtime.Composable @com.google.android.horologist.annotations.ExperimentalHorologistApi public static void VerticalPagerScreen(androidx.compose.foundation.pager.PagerState state, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> content);
}

}

package com.google.android.horologist.compose.paging {
Expand Down Expand Up @@ -392,6 +400,10 @@ package com.google.android.horologist.compose.rotaryinput {
property public final float velocity;
}

public final class RotaryWithPagerKt {
method public static androidx.compose.ui.Modifier rotaryWithPager(androidx.compose.ui.Modifier, androidx.compose.foundation.pager.PagerState state, androidx.compose.ui.focus.FocusRequester focusRequester);
}

@com.google.android.horologist.annotations.ExperimentalHorologistApi public final class ScalingLazyColumnRotaryScrollAdapter implements com.google.android.horologist.compose.rotaryinput.RotaryScrollAdapter {
ctor public ScalingLazyColumnRotaryScrollAdapter(androidx.wear.compose.foundation.lazy.ScalingLazyListState scrollableState);
method public float averageItemSize();
Expand Down
4 changes: 3 additions & 1 deletion compose-layout/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ android {
compileSdk = 34

defaultConfig {
minSdk = 25
minSdk = 26
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

Expand Down Expand Up @@ -114,6 +114,8 @@ dependencies {
testImplementation(libs.compose.ui.test.junit4)
testImplementation(libs.espresso.core)
testImplementation(libs.robolectric)
testImplementation(projects.composeTools)
testImplementation(projects.roboscreenshots)
}

apply(plugin = "com.vanniktech.maven.publish")
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ import com.google.android.horologist.compose.layout.PagerScaffold
/**
* A Wear Material Compliant Pager screen.
*
* Combines the Accompanist Pager, with the Wear Compose HorizontalPageIndicator.
* Also uses lifecycle, which allows attaching logic such requesting focus
* to page events.
* Combines the Compose Foundation Pager, with the Wear Compose HorizontalPageIndicator.
*
* The current page gets the Hierarchical Focus.
*/
@Composable
public fun PagerScreen(
Expand All @@ -69,7 +69,7 @@ public fun PagerScreen(
}

@Composable
private fun ClippedBox(pagerState: PagerState, content: @Composable () -> Unit) {
internal fun ClippedBox(pagerState: PagerState, content: @Composable () -> Unit) {
val shape = rememberClipWhenScrolling(pagerState)
Box(
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright 2024 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
*
* https://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.google.android.horologist.compose.pager

import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.wear.compose.material.HorizontalPageIndicator
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.PageIndicatorDefaults
import androidx.wear.compose.material.PageIndicatorState
import androidx.wear.compose.material.PageIndicatorStyle
import com.google.android.horologist.annotations.ExperimentalHorologistApi

/**
* A Wear Material Compliant Vertical Page Indicator.
*/
@Composable
@ExperimentalHorologistApi
fun VerticalPageIndicator(
pageIndicatorState: PageIndicatorState,
modifier: Modifier = Modifier,
indicatorStyle: PageIndicatorStyle = PageIndicatorDefaults.style(),
selectedColor: Color = MaterialTheme.colors.onBackground,
unselectedColor: Color = selectedColor.copy(alpha = 0.3f),
indicatorSize: Dp = 6.dp,
spacing: Dp = 4.dp,
indicatorShape: Shape = CircleShape,
) {
HorizontalPageIndicator(
pageIndicatorState = pageIndicatorState,
modifier = modifier
.rotate(90f)
.scale(scaleY = -1f, scaleX = 1f),
indicatorStyle = indicatorStyle,
selectedColor = selectedColor,
unselectedColor = unselectedColor,
indicatorSize = indicatorSize,
spacing = spacing,
indicatorShape = indicatorShape,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2024 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
*
* https://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(ExperimentalFoundationApi::class, ExperimentalWearFoundationApi::class)

package com.google.android.horologist.compose.pager

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
import androidx.wear.compose.foundation.rememberActiveFocusRequester
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScreenScaffold
import com.google.android.horologist.compose.rotaryinput.onRotaryInputAccumulated
import com.google.android.horologist.compose.rotaryinput.rotaryWithPager

/**
* A Wear Material Compliant Vertical Pager screen.
*
* Combines the Compose Foundation Pager, with a VerticalPageIndicator.
*
* The screens gets an [onRotaryInputAccumulated] modifier added for RSB handling.
*/
@Composable
@ExperimentalHorologistApi
public fun VerticalPagerScreen(
state: PagerState,
modifier: Modifier = Modifier,
content: @Composable ((Int) -> Unit),
) {
ScreenScaffold(
modifier = modifier.fillMaxSize(),
positionIndicator = {
VerticalPageIndicator(
pageIndicatorState = PageScreenIndicatorState(state),
modifier = Modifier.padding(6.dp),
)
},
) {
VerticalPager(
modifier = Modifier
.fillMaxSize()
.rotaryWithPager(state, rememberActiveFocusRequester()),
state = state,
flingBehavior = HorizontalPagerDefaults.flingParams(state),
) { page ->
ClippedBox(state) {
content(page)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2024 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
*
* https://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(ExperimentalFoundationApi::class)

package com.google.android.horologist.compose.rotaryinput

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.focusable
import androidx.compose.foundation.pager.PagerState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import com.google.android.horologist.compose.rotaryinput.RotaryInputConfigDefaults.DEFAULT_MIN_VALUE_CHANGE_DISTANCE_PX
import kotlinx.coroutines.launch

public fun Modifier.rotaryWithPager(
state: PagerState,
focusRequester: FocusRequester,
): Modifier = composed {
val coroutineScope = rememberCoroutineScope()
val haptics = rememberDefaultRotaryHapticFeedback()

onRotaryInputAccumulated(minValueChangeDistancePx = DEFAULT_MIN_VALUE_CHANGE_DISTANCE_PX * 3) {
val pageChange = if (it > 0f) 1 else -1

if ((pageChange == 1 && state.currentPage >= state.pageCount - 1) || (pageChange == -1 && state.currentPage == 0)) {
haptics.performHapticFeedback(RotaryHapticsType.ScrollLimit)
} else {
haptics.performHapticFeedback(RotaryHapticsType.ScrollItemFocus)

coroutineScope.launch {
state.animateScrollToPage(state.currentPage + pageChange)
}
}
}
.focusRequester(focusRequester)
.focusable()
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsFocused
Expand All @@ -42,7 +40,7 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onParent
import androidx.test.filters.MediumTest
import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
import androidx.wear.compose.foundation.RequestFocusWhenActive
import androidx.wear.compose.foundation.rememberActiveFocusRequester
import androidx.wear.compose.material.Text
import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll
import com.google.common.truth.Truth.assertThat
Expand Down Expand Up @@ -77,7 +75,7 @@ class PagerScreenTest {
5
}
PagerScreen(modifier = Modifier.fillMaxSize(), state = pagerState) { i ->
val focusRequester = remember { FocusRequester() }
val focusRequester = rememberActiveFocusRequester()
val scrollState = rememberScrollState()
Column(
modifier = Modifier
Expand All @@ -88,7 +86,6 @@ class PagerScreenTest {
) {
Text(modifier = Modifier.testTag("text$i"), text = "Text $i")
}
RequestFocusWhenActive(focusRequester)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2023 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
*
* https://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(ExperimentalFoundationApi::class)

package com.google.android.horologist.compose.pager

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.wear.compose.material.Text
import com.google.android.horologist.screenshots.ScreenshotBaseTest
import com.google.android.horologist.screenshots.ScreenshotTestRule
import org.junit.Test

class VerticalPagerScreenScreenshotTest : ScreenshotBaseTest(
params = ScreenshotTestRule.screenshotTestRuleParams {
screenTimeText = {}
record = ScreenshotTestRule.RecordMode.Record
},
) {

@Test
fun screens() {
screenshotTestRule.setContent(takeScreenshot = true, roundScreen = true) {
VerticalPagerScreen(
state = rememberPagerState {
10
},
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.DarkGray),
) {
Text(text = "Item $it", modifier = Modifier.align(Alignment.Center))
}
}
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ fun SamplePagerScreen(swipeToDismissBoxState: SwipeToDismissBoxState) {
}

@Composable
private fun PagerItemScreen(
internal fun PagerItemScreen(
item: String,
) {
Box(
Expand Down
Loading

0 comments on commit 95ecbfe

Please sign in to comment.