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

Commit

Permalink
Issue #94: Toolbar: Create sub-component skeleton (DisplayToolbar / E…
Browse files Browse the repository at this point in the history
…ditToolbar).
  • Loading branch information
pocmo authored and csadilek committed May 2, 2018
1 parent 427bb31 commit 33fa753
Show file tree
Hide file tree
Showing 12 changed files with 694 additions and 75 deletions.
7 changes: 7 additions & 0 deletions components/browser/toolbar/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ android {

dependencies {
implementation project(':concept-toolbar')
implementation project(':support-ktx')

implementation project(':ui-autocomplete')
implementation project(':ui-icons')
implementation project(':ui-progress')

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,145 @@
package mozilla.components.browser.toolbar

import android.content.Context
import android.support.annotation.VisibleForTesting
import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.FrameLayout
import android.view.View
import android.view.ViewGroup
import mozilla.components.browser.toolbar.display.DisplayToolbar
import mozilla.components.browser.toolbar.edit.EditToolbar
import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.support.ktx.android.view.dp
import mozilla.components.support.ktx.android.view.forEach

/**
* A customizable toolbar for browsers.
*
* The toolbar can switch between two modes: display and edit. The display mode displays the current
* URL and controls for navigation. In edit mode the current URL can be edited. Those two modes are
* implemented by the DisplayToolbar and EditToolbar classes.
*
* +----------------+
* | BrowserToolbar |
* +--------+-------+
* +
* +-------+-------+
* | |
* +---------v------+ +-------v--------+
* | DisplayToolbar | | EditToolbar |
* +----------------+ +----------------+
*
*/
class BrowserToolbar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr), Toolbar {
) : ViewGroup(context, attrs, defStyleAttr), Toolbar {

// displayToolbar and editToolbar are only visible internally and mutable so that we can mock
// them in tests.
@VisibleForTesting internal var displayToolbar = DisplayToolbar(context, this)
@VisibleForTesting internal var editToolbar = EditToolbar(context, this)

private var state: State = State.DISPLAY
private var url: String = ""
private var listener: ((String) -> Unit)? = null

init {
addView(displayToolbar)
addView(editToolbar)

updateState(State.DISPLAY)
}

// We layout the toolbar ourselves to avoid the overhead from using complex ViewGroup implementations
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
forEach { child ->
child.layout(left, top, right, bottom)
}
}

// We measure the views manually to avoid overhead by using complex ViewGroup implementations
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// Our toolbar will always use the full width and a fixed height
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = dp(TOOLBAR_HEIGHT_DP)

setMeasuredDimension(width, height)

// Let the children measure themselves using our fixed size
val childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
val childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)

forEach { child -> child.measure(childWidthSpec, childHeightSpec) }
}

override fun onBackPressed(): Boolean {
if (state == State.EDIT) {
displayMode()
return true
}
return false
}

override fun displayUrl(url: String) {
val urlView = getUrlViewComponent()
urlView.setText(url)
urlView.setSelection(0, url.length)
// We update the display toolbar immediately. We do not do that for the edit toolbar to not
// mess with what the user is entering. Instead we will remember the value and update the
// edit toolbar whenever we switch to it.
displayToolbar.updateUrl(url)

this.url = url
}

override fun displayProgress(progress: Int) {
displayToolbar.updateProgress(progress)
}

override fun setOnUrlChangeListener(listener: (String) -> Unit) {
val urlView = getUrlViewComponent()
urlView.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_GO) {
listener.invoke(urlView.text.toString())
}
true
this.listener = listener
}

/**
* Switches to URL editing mode.
*/
fun editMode() {
editToolbar.updateUrl(url)

updateState(State.EDIT)

editToolbar.focus()
}

/**
* Switches to URL displaying mode.
*/
fun displayMode() {
updateState(State.DISPLAY)
}

internal fun onUrlEntered(url: String) {
displayMode()

listener?.invoke(url)
}

private fun updateState(state: State) {
this.state = state

val (show, hide) = when (state) {
State.DISPLAY -> Pair(displayToolbar, editToolbar)
State.EDIT -> Pair(editToolbar, displayToolbar)
}

show.visibility = View.VISIBLE
hide.visibility = View.GONE
}

private enum class State {
DISPLAY,
EDIT
}

internal fun getUrlViewComponent(): EditText {
return this.findViewById(R.id.urlView)
companion object {
private const val TOOLBAR_HEIGHT_DP = 56
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/* 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.browser.toolbar.display

import android.annotation.SuppressLint
import android.content.Context
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.support.ktx.android.view.dp
import mozilla.components.ui.progress.AnimatedProgressBar

/**
* Sub-component of the browser toolbar responsible for displaying the URL and related controls.
*
* Structure:
* +------+-----+--------------------+---------+------+
* | icon | nav | url | actions | menu |
* +------+-----+--------------------+---------+------+
*
* - icon: Security indicator, usually a lock or globe icon.
* - nav: Optional navigation buttons (back/forward) to be displayed on large devices like tablets
* - url: The URL of the currently displayed website (read-only). Clicking this element will switch
* to editing mode.
* - actions: Optional (page) action icons injected by other components (e.g. reader mode).
* - menu: three dot menu button that opens the browser menu.
*/
@SuppressLint("ViewConstructor") // This view is only instantiated in code
internal class DisplayToolbar(
context: Context,
val toolbar: BrowserToolbar
) : ViewGroup(context) {
private val iconView = ImageView(context).apply {
val padding = dp(ICON_PADDING_DP)
setPadding(padding, padding, padding, padding)

setImageResource(mozilla.components.ui.icons.R.drawable.mozac_ic_globe)
}

private val urlView = TextView(context).apply {
gravity = Gravity.CENTER_VERTICAL
textSize = URL_TEXT_SIZE
setFadingEdgeLength(URL_FADING_EDGE_SIZE_DP)
isHorizontalFadingEdgeEnabled = true

setSingleLine(true)

setOnClickListener {
toolbar.editMode()
}
}

private val progressView = AnimatedProgressBar(context)

init {
addView(iconView)
addView(urlView)
addView(progressView)
}

/**
* Updates the URL to be displayed.
*/
fun updateUrl(url: String) {
urlView.text = url
}

/**
* Updates the progress to be displayed.
*/
fun updateProgress(progress: Int) {
progressView.progress = progress

progressView.visibility = if (progress < progressView.max) View.VISIBLE else View.GONE
}

// 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
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)

setMeasuredDimension(width, height)

// The icon fills the whole height and has a square shape
val iconSquareSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
iconView.measure(iconSquareSpec, iconSquareSpec)

// The url uses whatever space is left
val urlWidthSpec = MeasureSpec.makeMeasureSpec(width - height, MeasureSpec.EXACTLY)
urlView.measure(urlWidthSpec, heightMeasureSpec)

val progressHeightSpec = MeasureSpec.makeMeasureSpec(dp(PROGRESS_BAR_HEIGHT_DP), MeasureSpec.EXACTLY)
progressView.measure(widthMeasureSpec, progressHeightSpec)
}

// We layout the toolbar ourselves to avoid the overhead from using complex ViewGroup implementations
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
iconView.layout(left, top, left + iconView.measuredWidth, bottom)

urlView.layout(left + iconView.measuredWidth, top, right, bottom)

progressView.layout(left, bottom - progressView.measuredHeight, right, bottom)
}

companion object {
private const val ICON_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
}
}
Loading

0 comments on commit 33fa753

Please sign in to comment.