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

Commit 33fa753

Browse files
pocmocsadilek
authored andcommitted
Issue #94: Toolbar: Create sub-component skeleton (DisplayToolbar / EditToolbar).
1 parent 427bb31 commit 33fa753

File tree

12 files changed

+694
-75
lines changed

12 files changed

+694
-75
lines changed

components/browser/toolbar/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ android {
2828

2929
dependencies {
3030
implementation project(':concept-toolbar')
31+
implementation project(':support-ktx')
32+
33+
implementation project(':ui-autocomplete')
34+
implementation project(':ui-icons')
35+
implementation project(':ui-progress')
36+
37+
implementation "com.android.support:appcompat-v7:${rootProject.ext.dependencies['supportLibraries']}"
3138

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

components/browser/toolbar/src/main/java/mozilla/components/browser/toolbar/BrowserToolbar.kt

Lines changed: 122 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,145 @@
55
package mozilla.components.browser.toolbar
66

77
import android.content.Context
8+
import android.support.annotation.VisibleForTesting
89
import android.util.AttributeSet
9-
import android.view.inputmethod.EditorInfo
10-
import android.widget.EditText
11-
import android.widget.FrameLayout
10+
import android.view.View
11+
import android.view.ViewGroup
12+
import mozilla.components.browser.toolbar.display.DisplayToolbar
13+
import mozilla.components.browser.toolbar.edit.EditToolbar
1214
import mozilla.components.concept.toolbar.Toolbar
15+
import mozilla.components.support.ktx.android.view.dp
16+
import mozilla.components.support.ktx.android.view.forEach
1317

1418
/**
1519
* A customizable toolbar for browsers.
20+
*
21+
* The toolbar can switch between two modes: display and edit. The display mode displays the current
22+
* URL and controls for navigation. In edit mode the current URL can be edited. Those two modes are
23+
* implemented by the DisplayToolbar and EditToolbar classes.
24+
*
25+
* +----------------+
26+
* | BrowserToolbar |
27+
* +--------+-------+
28+
* +
29+
* +-------+-------+
30+
* | |
31+
* +---------v------+ +-------v--------+
32+
* | DisplayToolbar | | EditToolbar |
33+
* +----------------+ +----------------+
34+
*
1635
*/
1736
class BrowserToolbar @JvmOverloads constructor(
1837
context: Context,
1938
attrs: AttributeSet? = null,
2039
defStyleAttr: Int = 0
21-
) : FrameLayout(context, attrs, defStyleAttr), Toolbar {
40+
) : ViewGroup(context, attrs, defStyleAttr), Toolbar {
41+
42+
// displayToolbar and editToolbar are only visible internally and mutable so that we can mock
43+
// them in tests.
44+
@VisibleForTesting internal var displayToolbar = DisplayToolbar(context, this)
45+
@VisibleForTesting internal var editToolbar = EditToolbar(context, this)
46+
47+
private var state: State = State.DISPLAY
48+
private var url: String = ""
49+
private var listener: ((String) -> Unit)? = null
50+
51+
init {
52+
addView(displayToolbar)
53+
addView(editToolbar)
54+
55+
updateState(State.DISPLAY)
56+
}
57+
58+
// We layout the toolbar ourselves to avoid the overhead from using complex ViewGroup implementations
59+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
60+
forEach { child ->
61+
child.layout(left, top, right, bottom)
62+
}
63+
}
64+
65+
// We measure the views manually to avoid overhead by using complex ViewGroup implementations
66+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
67+
// Our toolbar will always use the full width and a fixed height
68+
val width = MeasureSpec.getSize(widthMeasureSpec)
69+
val height = dp(TOOLBAR_HEIGHT_DP)
70+
71+
setMeasuredDimension(width, height)
72+
73+
// Let the children measure themselves using our fixed size
74+
val childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
75+
val childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
76+
77+
forEach { child -> child.measure(childWidthSpec, childHeightSpec) }
78+
}
79+
80+
override fun onBackPressed(): Boolean {
81+
if (state == State.EDIT) {
82+
displayMode()
83+
return true
84+
}
85+
return false
86+
}
2287

2388
override fun displayUrl(url: String) {
24-
val urlView = getUrlViewComponent()
25-
urlView.setText(url)
26-
urlView.setSelection(0, url.length)
89+
// We update the display toolbar immediately. We do not do that for the edit toolbar to not
90+
// mess with what the user is entering. Instead we will remember the value and update the
91+
// edit toolbar whenever we switch to it.
92+
displayToolbar.updateUrl(url)
93+
94+
this.url = url
95+
}
96+
97+
override fun displayProgress(progress: Int) {
98+
displayToolbar.updateProgress(progress)
2799
}
28100

29101
override fun setOnUrlChangeListener(listener: (String) -> Unit) {
30-
val urlView = getUrlViewComponent()
31-
urlView.setOnEditorActionListener { _, actionId, _ ->
32-
if (actionId == EditorInfo.IME_ACTION_GO) {
33-
listener.invoke(urlView.text.toString())
34-
}
35-
true
102+
this.listener = listener
103+
}
104+
105+
/**
106+
* Switches to URL editing mode.
107+
*/
108+
fun editMode() {
109+
editToolbar.updateUrl(url)
110+
111+
updateState(State.EDIT)
112+
113+
editToolbar.focus()
114+
}
115+
116+
/**
117+
* Switches to URL displaying mode.
118+
*/
119+
fun displayMode() {
120+
updateState(State.DISPLAY)
121+
}
122+
123+
internal fun onUrlEntered(url: String) {
124+
displayMode()
125+
126+
listener?.invoke(url)
127+
}
128+
129+
private fun updateState(state: State) {
130+
this.state = state
131+
132+
val (show, hide) = when (state) {
133+
State.DISPLAY -> Pair(displayToolbar, editToolbar)
134+
State.EDIT -> Pair(editToolbar, displayToolbar)
36135
}
136+
137+
show.visibility = View.VISIBLE
138+
hide.visibility = View.GONE
139+
}
140+
141+
private enum class State {
142+
DISPLAY,
143+
EDIT
37144
}
38145

39-
internal fun getUrlViewComponent(): EditText {
40-
return this.findViewById(R.id.urlView)
146+
companion object {
147+
private const val TOOLBAR_HEIGHT_DP = 56
41148
}
42149
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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.browser.toolbar.display
6+
7+
import android.annotation.SuppressLint
8+
import android.content.Context
9+
import android.view.Gravity
10+
import android.view.View
11+
import android.view.ViewGroup
12+
import android.widget.ImageView
13+
import android.widget.TextView
14+
import mozilla.components.browser.toolbar.BrowserToolbar
15+
import mozilla.components.support.ktx.android.view.dp
16+
import mozilla.components.ui.progress.AnimatedProgressBar
17+
18+
/**
19+
* Sub-component of the browser toolbar responsible for displaying the URL and related controls.
20+
*
21+
* Structure:
22+
* +------+-----+--------------------+---------+------+
23+
* | icon | nav | url | actions | menu |
24+
* +------+-----+--------------------+---------+------+
25+
*
26+
* - icon: Security indicator, usually a lock or globe icon.
27+
* - nav: Optional navigation buttons (back/forward) to be displayed on large devices like tablets
28+
* - url: The URL of the currently displayed website (read-only). Clicking this element will switch
29+
* to editing mode.
30+
* - actions: Optional (page) action icons injected by other components (e.g. reader mode).
31+
* - menu: three dot menu button that opens the browser menu.
32+
*/
33+
@SuppressLint("ViewConstructor") // This view is only instantiated in code
34+
internal class DisplayToolbar(
35+
context: Context,
36+
val toolbar: BrowserToolbar
37+
) : ViewGroup(context) {
38+
private val iconView = ImageView(context).apply {
39+
val padding = dp(ICON_PADDING_DP)
40+
setPadding(padding, padding, padding, padding)
41+
42+
setImageResource(mozilla.components.ui.icons.R.drawable.mozac_ic_globe)
43+
}
44+
45+
private val urlView = TextView(context).apply {
46+
gravity = Gravity.CENTER_VERTICAL
47+
textSize = URL_TEXT_SIZE
48+
setFadingEdgeLength(URL_FADING_EDGE_SIZE_DP)
49+
isHorizontalFadingEdgeEnabled = true
50+
51+
setSingleLine(true)
52+
53+
setOnClickListener {
54+
toolbar.editMode()
55+
}
56+
}
57+
58+
private val progressView = AnimatedProgressBar(context)
59+
60+
init {
61+
addView(iconView)
62+
addView(urlView)
63+
addView(progressView)
64+
}
65+
66+
/**
67+
* Updates the URL to be displayed.
68+
*/
69+
fun updateUrl(url: String) {
70+
urlView.text = url
71+
}
72+
73+
/**
74+
* Updates the progress to be displayed.
75+
*/
76+
fun updateProgress(progress: Int) {
77+
progressView.progress = progress
78+
79+
progressView.visibility = if (progress < progressView.max) View.VISIBLE else View.GONE
80+
}
81+
82+
// We measure the views manually to avoid overhead by using complex ViewGroup implementations
83+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
84+
// This toolbar is using the full size provided by the parent
85+
val width = MeasureSpec.getSize(widthMeasureSpec)
86+
val height = MeasureSpec.getSize(heightMeasureSpec)
87+
88+
setMeasuredDimension(width, height)
89+
90+
// The icon fills the whole height and has a square shape
91+
val iconSquareSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
92+
iconView.measure(iconSquareSpec, iconSquareSpec)
93+
94+
// The url uses whatever space is left
95+
val urlWidthSpec = MeasureSpec.makeMeasureSpec(width - height, MeasureSpec.EXACTLY)
96+
urlView.measure(urlWidthSpec, heightMeasureSpec)
97+
98+
val progressHeightSpec = MeasureSpec.makeMeasureSpec(dp(PROGRESS_BAR_HEIGHT_DP), MeasureSpec.EXACTLY)
99+
progressView.measure(widthMeasureSpec, progressHeightSpec)
100+
}
101+
102+
// We layout the toolbar ourselves to avoid the overhead from using complex ViewGroup implementations
103+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
104+
iconView.layout(left, top, left + iconView.measuredWidth, bottom)
105+
106+
urlView.layout(left + iconView.measuredWidth, top, right, bottom)
107+
108+
progressView.layout(left, bottom - progressView.measuredHeight, right, bottom)
109+
}
110+
111+
companion object {
112+
private const val ICON_PADDING_DP = 16
113+
private const val URL_TEXT_SIZE = 15f
114+
private const val URL_FADING_EDGE_SIZE_DP = 24
115+
private const val PROGRESS_BAR_HEIGHT_DP = 3
116+
}
117+
}

0 commit comments

Comments
 (0)