Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
For #12855 - New CFR composable
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Mugurell committed Oct 18, 2022
1 parent 2d5d1ef commit 65024be
Show file tree
Hide file tree
Showing 19 changed files with 1,467 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .buildconfig.yml
Expand Up @@ -8,6 +8,10 @@ projects:
path: components/compose/browser-toolbar
description: 'A customizable toolbar for browsers using Jetpack Compose.'
publish: true
compose-cfr:
path: components/compose/cfr
description: 'A standard Contextual Feature Recommendation popup using Jetpack Compose.'
publish: true
compose-engine:
path: components/compose/engine
description: 'A component for integrating a concept-engine implementation into Jetpack Compose UI.'
Expand Down
2 changes: 2 additions & 0 deletions buildSrc/src/main/java/Dependencies.kt
Expand Up @@ -63,6 +63,7 @@ object Versions {
const val test_ext = "1.1.3"
const val espresso = "3.3.0"
const val room = "2.4.3"
const val savedstate = "1.2.0"
const val paging = "2.1.2"
const val palette = "1.0.0"
const val preferences = "1.1.1"
Expand Down Expand Up @@ -133,6 +134,7 @@ object Dependencies {
const val androidx_room_runtime = "androidx.room:room-ktx:${Versions.AndroidX.room}"
const val androidx_room_compiler = "androidx.room:room-compiler:${Versions.AndroidX.room}"
const val androidx_room_testing = "androidx.room:room-testing:${Versions.AndroidX.room}"
const val androidx_savedstate = "androidx.savedstate:savedstate:${Versions.AndroidX.savedstate}"
const val androidx_test_core = "androidx.test:core-ktx:${Versions.AndroidX.test}"
const val androidx_test_junit = "androidx.test.ext:junit-ktx:${Versions.AndroidX.test_ext}"
const val androidx_test_runner = "androidx.test:runner:${Versions.AndroidX.test}"
Expand Down
49 changes: 49 additions & 0 deletions components/compose/cfr/README.md
@@ -0,0 +1,49 @@
# [Android Components](../../../README.md) > Compose > Tabs tray

A standard Contextual Feature Recommendation popup using Jetpack Compose.

## Usage

```kotlin
CFRPopup(
anchor = <View>,
properties = CFRPopupProperties(
popupWidth = 256.dp,
popupAlignment = INDICATOR_CENTERED_IN_ANCHOR,
popupBodyColors = listOf(
ContextCompat.getColor(context, R.color.color1),
ContextCompat.getColor(context, R.color.color2)
),
dismissButtonColor = ContextCompat.getColor(context, R.color.color3),
),
onDismiss = { <method call> },
text = {
Text(
text = stringResource(R.string.string1),
style = MaterialTheme.typography.body2,
)
},
action = {
Button(onClick = { <method call> }) {
Text(text = stringResource(R.string.string2))
}
},
).apply {
show()
}
```


### Setting up the dependency

Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):

```Groovy
implementation "org.mozilla.components:compose-cfr:{latest-version}"
```

## License

This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/
59 changes: 59 additions & 0 deletions components/compose/cfr/build.gradle
@@ -0,0 +1,59 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdkVersion config.compileSdkVersion

defaultConfig {
minSdkVersion config.minSdkVersion
targetSdkVersion config.targetSdkVersion
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

buildFeatures {
compose true
}

composeOptions {
kotlinCompilerExtensionVersion = Versions.compose_compiler
}

kotlinOptions {
freeCompilerArgs += ['-Xjvm-default=enable']
}
}

dependencies {
implementation project(':support-ktx')
implementation project(':ui-icons')

implementation Dependencies.androidx_compose_ui
implementation Dependencies.androidx_compose_ui_tooling
implementation Dependencies.androidx_compose_foundation
implementation Dependencies.androidx_compose_material
implementation Dependencies.androidx_core
implementation Dependencies.androidx_core_ktx
implementation Dependencies.androidx_lifecycle_runtime
implementation Dependencies.androidx_savedstate

testImplementation project(':support-test')
testImplementation Dependencies.androidx_test_core
testImplementation Dependencies.androidx_test_junit
testImplementation Dependencies.testing_junit
testImplementation Dependencies.testing_mockito
testImplementation Dependencies.testing_robolectric
}

apply from: '../../../publish.gradle'
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
21 changes: 21 additions & 0 deletions components/compose/cfr/proguard-rules.pro
@@ -0,0 +1,21 @@
# 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
5 changes: 5 additions & 0 deletions components/compose/cfr/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="mozilla.components.compose.cfr" />
@@ -0,0 +1,166 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.compose.cfr

import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import mozilla.components.compose.cfr.CFRPopup.IndicatorDirection
import mozilla.components.compose.cfr.CFRPopup.PopupAlignment
import java.lang.ref.WeakReference

/**
* Properties used to customize the behavior of a [CFRPopup].
*
* @property popupWidth Width of the popup. Defaults to [CFRPopup.DEFAULT_WIDTH].
* @property popupAlignment Where in relation to it's anchor should the popup be placed.
* @property popupBodyColors One or more colors serving as the popup background.
* If more colors are provided they will be used in a gradient.
* @property popupVerticalOffset Vertical distance between the indicator arrow and the anchor.
* This only applies if [overlapAnchor] is `false`.
* @property dismissButtonColor The tint color that should be applied to the dismiss button.
* @property dismissOnBackPress Whether the popup can be dismissed by pressing the back button.
* If true, pressing the back button will also call onDismiss().
* @property dismissOnClickOutside Whether the popup can be dismissed by clicking outside the
* popup's bounds. If true, clicking outside the popup will call onDismiss().
* @property overlapAnchor How the popup's indicator will be shown in relation to the anchor:
* - true - indicator will be shown exactly in the middle horizontally and vertically
* - false - indicator will be shown horizontally in the middle of the anchor but immediately below or above it
* @property indicatorDirection The direction the indicator arrow is pointing.
* @property indicatorArrowStartOffset Maximum distance between the popup start and the indicator arrow.
* If there isn't enough space this could automatically be overridden up to 0 such that
* the indicator arrow will be pointing to the middle of the anchor.
*/
data class CFRPopupProperties(
val popupWidth: Dp = CFRPopup.DEFAULT_WIDTH.dp,
val popupAlignment: PopupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER,
val popupBodyColors: List<Int> = listOf(Color.Blue.toArgb()),
val popupVerticalOffset: Dp = CFRPopup.DEFAULT_VERTICAL_OFFSET.dp,
val dismissButtonColor: Int = Color.Black.toArgb(),
val dismissOnBackPress: Boolean = true,
val dismissOnClickOutside: Boolean = true,
val overlapAnchor: Boolean = false,
val indicatorDirection: IndicatorDirection = IndicatorDirection.UP,
val indicatorArrowStartOffset: Dp = CFRPopup.DEFAULT_INDICATOR_START_OFFSET.dp,
)

/**
* CFR - Contextual Feature Recommendation popup.
*
* @param anchor [View] that will serve as the anchor of the popup and serve as lifecycle owner
* for this popup also.
* @param properties [CFRPopupProperties] allowing to customize the popup appearance and behavior.
* @param onDismiss Callback for when the popup is dismissed indicating also if the dismissal
* was explicit - by tapping the "X" button or not.
* @param text [Text] already styled and ready to be shown in the popup.
* @param action Optional other composable to show just below the popup text.
*/
class CFRPopup(
@get:VisibleForTesting internal val anchor: View,
@get:VisibleForTesting internal val properties: CFRPopupProperties,
@get:VisibleForTesting internal val onDismiss: (Boolean) -> Unit = {},
@get:VisibleForTesting internal val text: @Composable (() -> Unit),
@get:VisibleForTesting internal val action: @Composable (() -> Unit) = {},
) {
// This is just a facade for the CFRPopupFullScreenLayout composable offering a cleaner API.

@VisibleForTesting
internal var popup: WeakReference<CFRPopupFullscreenLayout>? = null

/**
* Construct and display a styled CFR popup shown at the coordinates of [anchor].
* This popup will be dismissed when the user clicks on the "x" button or based on other user actions
* with such behavior set in [CFRPopupProperties].
*/
fun show() {
anchor.post {
CFRPopupFullscreenLayout(anchor, properties, onDismiss, text, action).apply {
this.show()
popup = WeakReference(this)
}
}
}

/**
* Immediately dismiss this CFR popup.
* The [onDismiss] callback won't be fired.
*/
fun dismiss() {
popup?.get()?.dismiss()
}

/**
* Possible direction for the arrow indicator of a CFR popup.
* The direction is expressed in relation with the popup body containing the text.
*/
enum class IndicatorDirection {
UP,
DOWN,
}

/**
* Possible alignments of the popup in relation to it's anchor.
*/
enum class PopupAlignment {
/**
* The popup body will be centered in the space occupied by the anchor.
* Recommended to be used when the anchor is wider than the popup.
*/
BODY_TO_ANCHOR_CENTER,

/**
* The popup body will be shown aligned to exactly the anchor start.
*/
BODY_TO_ANCHOR_START,

/**
* The popup will be aligned such that the indicator arrow will point to exactly the middle of the anchor.
* Recommended to be used when there are multiple widgets displayed horizontally so that this will allow
* to indicate exactly which widget the popup refers to.
*/
INDICATOR_CENTERED_IN_ANCHOR,
}

companion object {
/**
* Default width for all CFRs.
*/
internal const val DEFAULT_WIDTH = 335

/**
* Fixed horizontal padding.
* Allows the close button to extend with 10dp more to the end and intercept touches to
* a bit outside of the popup to ensure it respects a11y recommendations of 48dp size while
* also offer a bit more space to the text.
*/
internal const val DEFAULT_EXTRA_HORIZONTAL_PADDING = 10

/**
* How tall the indicator arrow should be.
* This will also affect the width of the indicator's base which is double the height value.
*/
internal const val DEFAULT_INDICATOR_HEIGHT = 7

/**
* Maximum distance between the popup start and the indicator.
*/
internal const val DEFAULT_INDICATOR_START_OFFSET = 30

/**
* Corner radius for the popup body.
*/
internal const val DEFAULT_CORNER_RADIUS = 12

/**
* Vertical distance between the indicator arrow and the anchor.
*/
internal const val DEFAULT_VERTICAL_OFFSET = 9
}
}

0 comments on commit 65024be

Please sign in to comment.