Skip to content

Commit 96444dd

Browse files
Mugurellmergify[bot]
authored andcommitted
For mozilla-mobile#12855 - New CFR composable
This upstreams the CFR composable already used on Fenix allowing it to be reused on other projects also. The setup process requires quite a few parameters because as it is highly customizable supporting different indicator orientations or positionings in relation to the anchor and also supporting RTL.
1 parent 45e99f5 commit 96444dd

File tree

19 files changed

+1467
-0
lines changed

19 files changed

+1467
-0
lines changed

.buildconfig.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ projects:
88
path: components/compose/browser-toolbar
99
description: 'A customizable toolbar for browsers using Jetpack Compose.'
1010
publish: true
11+
compose-cfr:
12+
path: components/compose/cfr
13+
description: 'A standard Contextual Feature Recommendation popup using Jetpack Compose.'
14+
publish: true
1115
compose-engine:
1216
path: components/compose/engine
1317
description: 'A component for integrating a concept-engine implementation into Jetpack Compose UI.'

buildSrc/src/main/java/Dependencies.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ object Versions {
6363
const val test_ext = "1.1.3"
6464
const val espresso = "3.3.0"
6565
const val room = "2.4.3"
66+
const val savedstate = "1.2.0"
6667
const val paging = "2.1.2"
6768
const val palette = "1.0.0"
6869
const val preferences = "1.1.1"
@@ -133,6 +134,7 @@ object Dependencies {
133134
const val androidx_room_runtime = "androidx.room:room-ktx:${Versions.AndroidX.room}"
134135
const val androidx_room_compiler = "androidx.room:room-compiler:${Versions.AndroidX.room}"
135136
const val androidx_room_testing = "androidx.room:room-testing:${Versions.AndroidX.room}"
137+
const val androidx_savedstate = "androidx.savedstate:savedstate:${Versions.AndroidX.savedstate}"
136138
const val androidx_test_core = "androidx.test:core-ktx:${Versions.AndroidX.test}"
137139
const val androidx_test_junit = "androidx.test.ext:junit-ktx:${Versions.AndroidX.test_ext}"
138140
const val androidx_test_runner = "androidx.test:runner:${Versions.AndroidX.test}"

components/compose/cfr/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# [Android Components](../../../README.md) > Compose > Tabs tray
2+
3+
A standard Contextual Feature Recommendation popup using Jetpack Compose.
4+
5+
## Usage
6+
7+
```kotlin
8+
CFRPopup(
9+
anchor = <View>,
10+
properties = CFRPopupProperties(
11+
popupWidth = 256.dp,
12+
popupAlignment = INDICATOR_CENTERED_IN_ANCHOR,
13+
popupBodyColors = listOf(
14+
ContextCompat.getColor(context, R.color.color1),
15+
ContextCompat.getColor(context, R.color.color2)
16+
),
17+
dismissButtonColor = ContextCompat.getColor(context, R.color.color3),
18+
),
19+
onDismiss = { <method call> },
20+
text = {
21+
Text(
22+
text = stringResource(R.string.string1),
23+
style = MaterialTheme.typography.body2,
24+
)
25+
},
26+
action = {
27+
Button(onClick = { <method call> }) {
28+
Text(text = stringResource(R.string.string2))
29+
}
30+
},
31+
).apply {
32+
show()
33+
}
34+
```
35+
36+
37+
### Setting up the dependency
38+
39+
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
40+
41+
```Groovy
42+
implementation "org.mozilla.components:compose-cfr:{latest-version}"
43+
```
44+
45+
## License
46+
47+
This Source Code Form is subject to the terms of the Mozilla Public
48+
License, v. 2.0. If a copy of the MPL was not distributed with this
49+
file, You can obtain one at http://mozilla.org/MPL/2.0/

components/compose/cfr/build.gradle

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
apply plugin: 'com.android.library'
6+
apply plugin: 'kotlin-android'
7+
8+
android {
9+
compileSdkVersion config.compileSdkVersion
10+
11+
defaultConfig {
12+
minSdkVersion config.minSdkVersion
13+
targetSdkVersion config.targetSdkVersion
14+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15+
}
16+
17+
buildTypes {
18+
release {
19+
minifyEnabled false
20+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
21+
}
22+
}
23+
24+
buildFeatures {
25+
compose true
26+
}
27+
28+
composeOptions {
29+
kotlinCompilerExtensionVersion = Versions.compose_compiler
30+
}
31+
32+
kotlinOptions {
33+
freeCompilerArgs += "-Xjvm-default=all"
34+
}
35+
}
36+
37+
dependencies {
38+
implementation project(':support-ktx')
39+
implementation project(':ui-icons')
40+
41+
implementation Dependencies.androidx_compose_ui
42+
implementation Dependencies.androidx_compose_ui_tooling
43+
implementation Dependencies.androidx_compose_foundation
44+
implementation Dependencies.androidx_compose_material
45+
implementation Dependencies.androidx_core
46+
implementation Dependencies.androidx_core_ktx
47+
implementation Dependencies.androidx_lifecycle_runtime
48+
implementation Dependencies.androidx_savedstate
49+
50+
testImplementation project(':support-test')
51+
testImplementation Dependencies.androidx_test_core
52+
testImplementation Dependencies.androidx_test_junit
53+
testImplementation Dependencies.testing_junit
54+
testImplementation Dependencies.testing_mockito
55+
testImplementation Dependencies.testing_robolectric
56+
}
57+
58+
apply from: '../../../publish.gradle'
59+
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!-- This Source Code Form is subject to the terms of the Mozilla Public
2+
- License, v. 2.0. If a copy of the MPL was not distributed with this
3+
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
4+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
5+
package="mozilla.components.compose.cfr" />
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.compose.cfr
6+
7+
import android.view.View
8+
import androidx.annotation.VisibleForTesting
9+
import androidx.compose.material.Text
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.ui.graphics.Color
12+
import androidx.compose.ui.graphics.toArgb
13+
import androidx.compose.ui.unit.Dp
14+
import androidx.compose.ui.unit.dp
15+
import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection
16+
import mozilla.components.compose.cfr.CFRPopup.PopupAlignment
17+
import java.lang.ref.WeakReference
18+
19+
/**
20+
* Properties used to customize the behavior of a [CFRPopup].
21+
*
22+
* @property popupWidth Width of the popup. Defaults to [CFRPopup.DEFAULT_WIDTH].
23+
* @property popupAlignment Where in relation to it's anchor should the popup be placed.
24+
* @property popupBodyColors One or more colors serving as the popup background.
25+
* If more colors are provided they will be used in a gradient.
26+
* @property popupVerticalOffset Vertical distance between the indicator arrow and the anchor.
27+
* This only applies if [overlapAnchor] is `false`.
28+
* @property dismissButtonColor The tint color that should be applied to the dismiss button.
29+
* @property dismissOnBackPress Whether the popup can be dismissed by pressing the back button.
30+
* If true, pressing the back button will also call onDismiss().
31+
* @property dismissOnClickOutside Whether the popup can be dismissed by clicking outside the
32+
* popup's bounds. If true, clicking outside the popup will call onDismiss().
33+
* @property overlapAnchor How the popup's indicator will be shown in relation to the anchor:
34+
* - true - indicator will be shown exactly in the middle horizontally and vertically
35+
* - false - indicator will be shown horizontally in the middle of the anchor but immediately below or above it
36+
* @property indicatorDirection The direction the indicator arrow is pointing.
37+
* @property indicatorArrowStartOffset Maximum distance between the popup start and the indicator arrow.
38+
* If there isn't enough space this could automatically be overridden up to 0 such that
39+
* the indicator arrow will be pointing to the middle of the anchor.
40+
*/
41+
data class CFRPopupProperties(
42+
val popupWidth: Dp = CFRPopup.DEFAULT_WIDTH.dp,
43+
val popupAlignment: PopupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER,
44+
val popupBodyColors: List<Int> = listOf(Color.Blue.toArgb()),
45+
val popupVerticalOffset: Dp = CFRPopup.DEFAULT_VERTICAL_OFFSET.dp,
46+
val dismissButtonColor: Int = Color.Black.toArgb(),
47+
val dismissOnBackPress: Boolean = true,
48+
val dismissOnClickOutside: Boolean = true,
49+
val overlapAnchor: Boolean = false,
50+
val indicatorDirection: IndicatorDirection = IndicatorDirection.UP,
51+
val indicatorArrowStartOffset: Dp = CFRPopup.DEFAULT_INDICATOR_START_OFFSET.dp,
52+
)
53+
54+
/**
55+
* CFR - Contextual Feature Recommendation popup.
56+
*
57+
* @param anchor [View] that will serve as the anchor of the popup and serve as lifecycle owner
58+
* for this popup also.
59+
* @param properties [CFRPopupProperties] allowing to customize the popup appearance and behavior.
60+
* @param onDismiss Callback for when the popup is dismissed indicating also if the dismissal
61+
* was explicit - by tapping the "X" button or not.
62+
* @param text [Text] already styled and ready to be shown in the popup.
63+
* @param action Optional other composable to show just below the popup text.
64+
*/
65+
class CFRPopup(
66+
@get:VisibleForTesting internal val anchor: View,
67+
@get:VisibleForTesting internal val properties: CFRPopupProperties,
68+
@get:VisibleForTesting internal val onDismiss: (Boolean) -> Unit = {},
69+
@get:VisibleForTesting internal val text: @Composable (() -> Unit),
70+
@get:VisibleForTesting internal val action: @Composable (() -> Unit) = {},
71+
) {
72+
// This is just a facade for the CFRPopupFullScreenLayout composable offering a cleaner API.
73+
74+
@VisibleForTesting
75+
internal var popup: WeakReference<CFRPopupFullscreenLayout>? = null
76+
77+
/**
78+
* Construct and display a styled CFR popup shown at the coordinates of [anchor].
79+
* This popup will be dismissed when the user clicks on the "x" button or based on other user actions
80+
* with such behavior set in [CFRPopupProperties].
81+
*/
82+
fun show() {
83+
anchor.post {
84+
CFRPopupFullscreenLayout(anchor, properties, onDismiss, text, action).apply {
85+
this.show()
86+
popup = WeakReference(this)
87+
}
88+
}
89+
}
90+
91+
/**
92+
* Immediately dismiss this CFR popup.
93+
* The [onDismiss] callback won't be fired.
94+
*/
95+
fun dismiss() {
96+
popup?.get()?.dismiss()
97+
}
98+
99+
/**
100+
* Possible direction for the arrow indicator of a CFR popup.
101+
* The direction is expressed in relation with the popup body containing the text.
102+
*/
103+
enum class IndicatorDirection {
104+
UP,
105+
DOWN,
106+
}
107+
108+
/**
109+
* Possible alignments of the popup in relation to it's anchor.
110+
*/
111+
enum class PopupAlignment {
112+
/**
113+
* The popup body will be centered in the space occupied by the anchor.
114+
* Recommended to be used when the anchor is wider than the popup.
115+
*/
116+
BODY_TO_ANCHOR_CENTER,
117+
118+
/**
119+
* The popup body will be shown aligned to exactly the anchor start.
120+
*/
121+
BODY_TO_ANCHOR_START,
122+
123+
/**
124+
* The popup will be aligned such that the indicator arrow will point to exactly the middle of the anchor.
125+
* Recommended to be used when there are multiple widgets displayed horizontally so that this will allow
126+
* to indicate exactly which widget the popup refers to.
127+
*/
128+
INDICATOR_CENTERED_IN_ANCHOR,
129+
}
130+
131+
companion object {
132+
/**
133+
* Default width for all CFRs.
134+
*/
135+
internal const val DEFAULT_WIDTH = 335
136+
137+
/**
138+
* Fixed horizontal padding.
139+
* Allows the close button to extend with 10dp more to the end and intercept touches to
140+
* a bit outside of the popup to ensure it respects a11y recommendations of 48dp size while
141+
* also offer a bit more space to the text.
142+
*/
143+
internal const val DEFAULT_EXTRA_HORIZONTAL_PADDING = 10
144+
145+
/**
146+
* How tall the indicator arrow should be.
147+
* This will also affect the width of the indicator's base which is double the height value.
148+
*/
149+
internal const val DEFAULT_INDICATOR_HEIGHT = 7
150+
151+
/**
152+
* Maximum distance between the popup start and the indicator.
153+
*/
154+
internal const val DEFAULT_INDICATOR_START_OFFSET = 30
155+
156+
/**
157+
* Corner radius for the popup body.
158+
*/
159+
internal const val DEFAULT_CORNER_RADIUS = 12
160+
161+
/**
162+
* Vertical distance between the indicator arrow and the anchor.
163+
*/
164+
internal const val DEFAULT_VERTICAL_OFFSET = 9
165+
}
166+
}

0 commit comments

Comments
 (0)