Commit 1f942bd7 authored by Sebastian Kaspari's avatar Sebastian Kaspari
Browse files

Issue: #86: Add ui-doorhanger component for displaying floating "heads-up" popups.

parent 5e429ec7
......@@ -148,6 +148,10 @@ projects:
path: components/ui/colors
description: 'The standard set of Photon colors.'
publish: true
ui-doorhanger:
path: components/ui/doorhanger
description: 'A generic floating heads-up popup that can be anchored to a view.'
publish: true
ui-fonts:
path: components/ui/fonts
description: 'Convenience accessor for fonts used by Mozilla.'
......
......@@ -136,6 +136,8 @@ _Generic low-level UI components for building apps._
* 🔵 [**Colors**](components/ui/colors/README.md) - The standard set of [Photon](https://design.firefox.com/photon/) colors.
*[**Doorhanger**](components/ui/doorhanger/README.md) - A generic floating heads-up popup that can be anchored to a view.
* 🔵 [**Fonts**](components/ui/fonts/README.md) - The standard set of fonts used by Mozilla Android products.
* 🔵 [**Icons**](components/ui/icons/README.md) - A collection of often used browser icons.
......
# [Android Components](../../../README.md) > UI > Doorhanger
A generic floating heads-up popup that can be anchored to a view.
## Usage
### 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:ui-doorhanger:{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/
/* 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 "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation project(':support-ktx')
implementation Dependencies.kotlin_stdlib
implementation Dependencies.support_appcompat
implementation Dependencies.support_cardview
testImplementation project(':support-test')
testImplementation Dependencies.androidx_test_core
testImplementation Dependencies.testing_junit
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
}
apply from: '../../../publish.gradle'
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
# 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
<!-- 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.ui.doorhanger" />
/* 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.ui.doorhanger
import android.annotation.SuppressLint
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.support.annotation.VisibleForTesting
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.PopupWindow
/**
* A [Doorhanger] is a floating heads-up popup that can be anchored to a view. They are presented to notify the user
* of something that is important.
*/
class Doorhanger(
private val view: View,
private val onDismiss: (() -> Unit)? = null
) {
private var currentPopup: PopupWindow? = null
/**
* Show this doorhanger and anchor it to the given [View].
*/
fun show(anchor: View): PopupWindow {
@SuppressLint("InflateParams")
val wrapper = LayoutInflater.from(anchor.context).inflate(
R.layout.mozac_ui_doorhanger_wrapper,
null,
false)
// If we have a wrapper then wrap the view in it. Otherwise use the view directly.
wrapper.findViewById<ViewGroup>(R.id.mozac_ui_doorhanger_content).addView(view)
return createPopupWindow(wrapper).also {
currentPopup = it
it.showAsDropDown(anchor)
}
}
/**
* Dismiss this doorhanger if it is currently showing.
*/
fun dismiss() {
currentPopup?.dismiss()
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun createPopupWindow(view: View) = PopupWindow(
view,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT
).apply {
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
isFocusable = true
setOnDismissListener {
currentPopup = null
onDismiss?.invoke()
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="4dp"
app:cardCornerRadius="4dp">
<FrameLayout
android:id="@+id/mozac_ui_doorhanger_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp" />
</android.support.v7.widget.CardView>
/* 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.ui.doorhanger
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class DoorhangerTest {
private val context: Context
get() = ApplicationProvider.getApplicationContext()
@Test
fun `show returns non-null popup window`() {
val doorhanger = Doorhanger(TextView(context))
val popupWindow = doorhanger.show(View(context))
assertNotNull(popupWindow)
assertTrue(popupWindow.isShowing)
}
@Test
fun `popup window contains passed in view`() {
val view = TextView(context).apply {
id = 42
text = "Mozilla!"
}
val doorhanger = Doorhanger(view)
val popupWindow = doorhanger.show(View(context))
assertEquals(
"Mozilla!",
popupWindow.contentView.findViewById<TextView>(42).text
)
}
@Test
fun `popup window passed in view is wrapped`() {
val view = TextView(context)
val doorhanger = Doorhanger(view)
val popupWindow = doorhanger.show(View(context))
assertNotEquals(view, popupWindow.contentView)
assertTrue(popupWindow.contentView is ViewGroup)
}
@Test
fun `dismissing popup window invokes callback`() {
var dismissCallbackInvoked = false
val doorhanger = Doorhanger(TextView(context), onDismiss = {
dismissCallbackInvoked = true
})
val popupWindow = doorhanger.show(View(context))
assertFalse(dismissCallbackInvoked)
popupWindow.dismiss()
assertTrue(dismissCallbackInvoked)
}
@Test
fun `dismiss on doorhanger is forwarded to popup window`() {
val doorhanger = Doorhanger(TextView(context))
val popupWindow = doorhanger.show(View(context))
assertTrue(popupWindow.isShowing)
doorhanger.dismiss()
assertFalse(popupWindow.isShowing)
}
@Test
fun `calling dismiss on a not-showing doorhanger is a no-op`() {
val doorhanger = Doorhanger(TextView(context))
doorhanger.dismiss()
}
}
mock-maker-inline
// This allows mocking final classes (classes are final by default in Kotlin)
......@@ -30,6 +30,9 @@ permalink: /changelog/
* Added `ViewBoundFeatureWrapper` for wrapping `LifecycleAwareFeature` references that will automatically be cleared if the provided `View` gets detached. This is helpful for fragments that want to keep a reference to a `LifecycleAwareFeature` (e.g. to be able call `onBackPressed()`) that itself has strong references to `View` objects. In cases where the fragment gets detached (e.g. to be added to the backstack) and the `View` gets detached (and destroyed) the wrapper will automatically stop the `LifecycleAwareFeature` and clear all references..
* Added generic `BackHandler` interface for fragments, features and other components that want to handle 'back' button presses.
* **ui-doorhanger**
* 🆕 New component: A `Doorhanger` is a floating heads-up popup that can be anchored to a view. They are presented to notify the user of something that is important (e.g. a content permission request).
# 0.41.0
* [Commits](https://github.com/mozilla-mobile/android-components/compare/v0.40.0...v0.41.0)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment