Skip to content

Commit

Permalink
Issue mozilla-mobile#173: Toolbar: Add support for dynamic display ac…
Browse files Browse the repository at this point in the history
…tions.
  • Loading branch information
pocmo committed May 18, 2018
1 parent a09b862 commit 8cb5b7d
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ class BrowserToolbar @JvmOverloads constructor(
this.listener = listener
}

/**
* Adds an action to be displayed on the right side of the toolbar in display mode.
*
* If there is not enough room to show all icons then some icons may be moved to an overflow
* menu.
*/
override fun addDisplayAction(action: Toolbar.Action) {
displayToolbar.addAction(action)
}

/**
* Switches to URL editing mode.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ package mozilla.components.browser.toolbar.display

import android.annotation.SuppressLint
import android.content.Context
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.R
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.support.ktx.android.view.dp
import mozilla.components.support.ktx.android.view.isVisible
import mozilla.components.ui.progress.AnimatedProgressBar
import android.util.TypedValue
import mozilla.components.browser.menu.BrowserMenuBuilder

/**
* Sub-component of the browser toolbar responsible for displaying the URL and related controls.
Expand Down Expand Up @@ -73,6 +75,7 @@ internal class DisplayToolbar(
setPadding(padding, padding, padding, padding)

setImageResource(mozilla.components.ui.icons.R.drawable.mozac_ic_menu)
contentDescription = context.getString(R.string.mozac_browser_toolbar_menu_button)

val outValue = TypedValue()
context.theme.resolveAttribute(
Expand All @@ -90,6 +93,8 @@ internal class DisplayToolbar(

private val progressView = AnimatedProgressBar(context)

private val actions: MutableList<DisplayAction> = mutableListOf()

init {
addView(iconView)
addView(urlView)
Expand All @@ -113,6 +118,26 @@ internal class DisplayToolbar(
progressView.visibility = if (progress < progressView.max) View.VISIBLE else View.GONE
}

/**
* Adds an action to be displayed on the right side of the toolbar.
*
* If there is not enough room to show all icons then some icons may be moved to an overflow
* menu.
*/
fun addAction(action: Toolbar.Action) {
val displayAction = DisplayAction(action)

actions.add(displayAction)

if (actions.size < MAX_VISIBLE_ACTION_ITEMS) {
val view = createActionView(context, action)

displayAction.view = view

addView(view)
}
}

// We measure the views manually to avoid overhead by using complex ViewGroup implementations
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// This toolbar is using the full size provided by the parent
Expand All @@ -126,8 +151,17 @@ internal class DisplayToolbar(
iconView.measure(squareSpec, squareSpec)
menuView.measure(squareSpec, squareSpec)

// If there are actions with a view then use the same square shape for them
var actionWidth = 0
actions
.mapNotNull { it.view }
.forEach { view ->
view.measure(squareSpec, squareSpec)
actionWidth += height
}

// The url uses whatever space is left. Substract the icon and (optionally) the menu
val urlWidth = width - height - if (menuView.isVisible()) height else 0
val urlWidth = width - height - actionWidth - if (menuView.isVisible()) height else 0
val urlWidthSpec = MeasureSpec.makeMeasureSpec(urlWidth, MeasureSpec.EXACTLY)
urlView.measure(urlWidthSpec, heightMeasureSpec)

Expand All @@ -139,7 +173,20 @@ internal class DisplayToolbar(
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
iconView.layout(left, top, left + iconView.measuredWidth, bottom)

val urlRight = right - if (menuView.isVisible()) height else 0
var actionWidth = 0
actions
.mapNotNull { it.view }
.reversed()
.forEach { view ->
val viewRight = right - actionWidth - if (menuView.isVisible()) height else 0
val viewLeft = viewRight - view.measuredWidth

view.layout(viewLeft, top, viewRight, bottom)

actionWidth += view.measuredWidth
}

val urlRight = right - actionWidth - if (menuView.isVisible()) height else 0
urlView.layout(left + iconView.measuredWidth, top, urlRight, bottom)

progressView.layout(left, bottom - progressView.measuredHeight, right, bottom)
Expand All @@ -150,8 +197,33 @@ internal class DisplayToolbar(
companion object {
private const val ICON_PADDING_DP = 16
private const val MENU_PADDING_DP = 16
private const val ACTION_PADDING_DP = 16
private const val URL_TEXT_SIZE = 15f
private const val URL_FADING_EDGE_SIZE_DP = 24
private const val PROGRESS_BAR_HEIGHT_DP = 3
private const val MAX_VISIBLE_ACTION_ITEMS = 2

fun createActionView(context: Context, action: Toolbar.Action) = ImageButton(context).apply {
val padding = dp(ACTION_PADDING_DP)
setPadding(padding, padding, padding, padding)

setImageResource(action.imageResource)
contentDescription = action.contentDescription

val outValue = TypedValue()
context.theme.resolveAttribute(
android.R.attr.selectableItemBackgroundBorderless,
outValue,
true)

setBackgroundResource(outValue.resourceId)

setOnClickListener { action.listener.invoke() }
}
}
}

private class DisplayAction(
var actual: Toolbar.Action,
var view: View? = null
)
8 changes: 8 additions & 0 deletions components/browser/toolbar/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?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/. -->
<resources>
<!-- Content description (not visible, for screen readers etc.): Description for the overflow menu button in the browser toolbar. -->
<string name="mozac_browser_toolbar_menu_button">Menu</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.view.View
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.toolbar.display.DisplayToolbar
import mozilla.components.browser.toolbar.edit.EditToolbar
import mozilla.components.concept.toolbar.Toolbar
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
Expand Down Expand Up @@ -231,4 +232,20 @@ class BrowserToolbarTest {
assertNotNull(toolbar.displayToolbar.menuBuilder)
assertEquals(menuBuilder, toolbar.displayToolbar.menuBuilder)
}

@Test
fun `add action will be forwarded to display toolbar`() {
val toolbar = BrowserToolbar(RuntimeEnvironment.application)
val displayToolbar = mock(DisplayToolbar::class.java)

toolbar.displayToolbar = displayToolbar

val action = Toolbar.Action(0, "Hello") {
// Do nothing
}

toolbar.addDisplayAction(action)

verify(displayToolbar).addAction(action)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import android.widget.TextView
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.support.ktx.android.view.forEach
import mozilla.components.ui.progress.AnimatedProgressBar
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
Expand Down Expand Up @@ -145,6 +147,64 @@ class DisplayToolbarTest {
verify(menu).show(menuView)
}

@Test
fun `action gets added as view to toolbar`() {
val contentDescription = "Mozilla"

val toolbar = mock(BrowserToolbar::class.java)
val displayToolbar = DisplayToolbar(RuntimeEnvironment.application, toolbar)

assertNull(extractActionView(displayToolbar, contentDescription))

val action = Toolbar.Action(0, contentDescription) {}
displayToolbar.addAction(action)

val view = extractActionView(displayToolbar, contentDescription)
assertNotNull(view)
assertEquals(contentDescription, view?.contentDescription)
}

@Test
fun `clicking action view triggers listener of action`() {
var callbackExecuted = false

val action = Toolbar.Action(0, "Button") {
callbackExecuted = true
}

val toolbar = mock(BrowserToolbar::class.java)
val displayToolbar = DisplayToolbar(RuntimeEnvironment.application, toolbar)
displayToolbar.addAction(action)

val view = extractActionView(displayToolbar, "Button")
assertNotNull(view)

assertFalse(callbackExecuted)

view?.performClick()

assertTrue(callbackExecuted)
}

@Test
fun `action view will use square size`() {
val toolbar = mock(BrowserToolbar::class.java)
val displayToolbar = DisplayToolbar(RuntimeEnvironment.application, toolbar)

val action = Toolbar.Action(0, "action") {}
displayToolbar.addAction(action)

val widthSpec = View.MeasureSpec.makeMeasureSpec(1024, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(56, View.MeasureSpec.EXACTLY)

displayToolbar.measure(widthSpec, heightSpec)

val view = extractActionView(displayToolbar, "action")!!

assertEquals(56, view.measuredWidth)
assertEquals(56, view.measuredHeight)
}

companion object {
private fun extractUrlView(displayToolbar: DisplayToolbar): TextView {
var textView: TextView? = null
Expand Down Expand Up @@ -197,5 +257,21 @@ class DisplayToolbarTest {

return menuButton ?: throw AssertionError("Could not find menu view")
}

private fun extractActionView(
displayToolbar: DisplayToolbar,
contentDescription: String
): ImageButton? {
var actionView: ImageButton? = null

displayToolbar.forEach {
if (it is ImageButton && it.contentDescription == contentDescription) {
actionView = it
return@forEach
}
}

return actionView
}
}
}
2 changes: 2 additions & 0 deletions components/concept/toolbar/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ android {

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:${rootProject.ext.dependencies['kotlin']}"

implementation "com.android.support:support-annotations:${rootProject.ext.dependencies['supportLibraries']}"
}

archivesBaseName = "abstract-toolbar"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

package mozilla.components.concept.toolbar

import android.support.annotation.DrawableRes

/**
* Interface to be implemented by components that provide browser toolbar functionality.
*/
interface Toolbar {

/**
* Displays the given URL string as part of this Toolbar.
*
Expand All @@ -35,4 +36,18 @@ interface Toolbar {
* @param listener the listener function
*/
fun setOnUrlChangeListener(listener: (String) -> Unit)

/**
* Adds an action to be displayed on the right side of the toolbar in display mode.
*/
fun addDisplayAction(action: Action)

/**
* An action button to be added to the toolbar.
*/
data class Action(
@DrawableRes val imageResource: Int,
val contentDescription: String,
val listener: () -> Unit
)
}

0 comments on commit 8cb5b7d

Please sign in to comment.