1
0
Fork 0

For #904 -Add tab counter to tab icon

master
Emily Kager 2019-04-05 14:33:02 -07:00 committed by Emily Kager
parent 568edda8bc
commit 9f1ec5e2b0
6 changed files with 457 additions and 26 deletions

View File

@ -15,5 +15,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- #1165 - Added doorhanger to the toolbar
- #1195 - Adds telemetry for quick action sheet
- #627 - Sets engine preferred color scheme based on light/dark theme
- #904 - Added tab counter in browser toolbar
### Changed
### Removed

View File

@ -0,0 +1,309 @@
/* 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 org.mozilla.fenix.components.toolbar
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.Typeface
import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.ViewTreeObserver
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import mozilla.components.support.utils.DrawableUtils
import mozilla.components.ui.tabcounter.R
import java.text.NumberFormat
open class TabCounter @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : RelativeLayout(context, attrs, defStyle) {
private val box: ImageView
private val text: TextView
private val animationSet: AnimatorSet
private var count: Int = 0
private var currentTextRatio: Float = 0.toFloat()
init {
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.mozac_ui_tabcounter_layout, this)
box = findViewById(R.id.counter_box)
text = findViewById(R.id.counter_text)
text.text = DEFAULT_TABS_COUNTER_TEXT
val shiftOneDpForDefaultText = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 1f, context.resources.displayMetrics
).toInt()
text.setPadding(0, 0, 0, shiftOneDpForDefaultText)
animationSet = createAnimatorSet()
}
fun getText(): CharSequence {
return text.text
}
fun setCountWithAnimation(count: Int) {
// Don't animate from initial state.
if (this.count == 0) {
setCount(count)
return
}
if (this.count == count) {
return
}
// Don't animate if there are still over MAX_VISIBLE_TABS tabs open.
if (this.count > MAX_VISIBLE_TABS && count > MAX_VISIBLE_TABS) {
this.count = count
return
}
adjustTextSize(count)
text.setPadding(0, 0, 0, 0)
text.text = formatForDisplay(count)
this.count = count
// Cancel previous animations if necessary.
if (animationSet.isRunning) {
animationSet.cancel()
}
// Trigger animations.
animationSet.start()
}
fun setCount(count: Int) {
adjustTextSize(count)
text.setPadding(0, 0, 0, 0)
text.text = formatForDisplay(count)
this.count = count
}
private fun tintDrawables(tabCounterTint: Int) {
val tabCounterBox = DrawableUtils.loadAndTintDrawable(
context,
R.drawable.mozac_ui_tabcounter_box, tabCounterTint
)
box.setImageDrawable(tabCounterBox)
text.setTextColor(tabCounterTint)
}
private fun createAnimatorSet(): AnimatorSet {
val animatorSet = AnimatorSet()
createBoxAnimatorSet(animatorSet)
createTextAnimatorSet(animatorSet)
return animatorSet
}
private fun createBoxAnimatorSet(animatorSet: AnimatorSet) {
// The first animator, fadeout in 33 ms (49~51, 2 frames).
val fadeOut = ObjectAnimator.ofFloat(
box, "alpha",
ANIM_BOX_FADEOUT_FROM, ANIM_BOX_FADEOUT_TO
).setDuration(ANIM_BOX_FADEOUT_DURATION)
// Move up on y-axis, from 0.0 to -5.3 in 50ms, with fadeOut (49~52, 3 frames).
val moveUp1 = ObjectAnimator.ofFloat(
box, "translationY",
ANIM_BOX_MOVEUP1_TO, ANIM_BOX_MOVEUP1_FROM
).setDuration(ANIM_BOX_MOVEUP1_DURATION)
// Move down on y-axis, from -5.3 to -1.0 in 116ms, after moveUp1 (52~59, 7 frames).
val moveDown2 = ObjectAnimator.ofFloat(
box, "translationY",
ANIM_BOX_MOVEDOWN2_FROM, ANIM_BOX_MOVEDOWN2_TO
).setDuration(ANIM_BOX_MOVEDOWN2_DURATION)
// FadeIn in 66ms, with moveDown2 (52~56, 4 frames).
val fadeIn = ObjectAnimator.ofFloat(
box, "alpha",
ANIM_BOX_FADEIN_FROM, ANIM_BOX_FADEIN_TO
).setDuration(ANIM_BOX_FADEIN_DURATION)
// Move down on y-axis, from -1.0 to 2.7 in 116ms, after moveDown2 (59~66, 7 frames).
val moveDown3 = ObjectAnimator.ofFloat(
box, "translationY",
ANIM_BOX_MOVEDOWN3_FROM, ANIM_BOX_MOVEDOWN3_TO
).setDuration(ANIM_BOX_MOVEDOWN3_DURATION)
// Move up on y-axis, from 2.7 to 0 in 133ms, after moveDown3 (66~74, 8 frames).
val moveUp4 = ObjectAnimator.ofFloat(
box, "translationY",
ANIM_BOX_MOVEDOWN4_FROM, ANIM_BOX_MOVEDOWN4_TO
).setDuration(ANIM_BOX_MOVEDOWN4_DURATION)
// Scale up height from 2% to 105% in 100ms, after moveUp1 and delay 16ms (53~59, 6 frames).
val scaleUp1 = ObjectAnimator.ofFloat(
box, "scaleY",
ANIM_BOX_SCALEUP1_FROM, ANIM_BOX_SCALEUP1_TO
).setDuration(ANIM_BOX_SCALEUP1_DURATION)
scaleUp1.startDelay = ANIM_BOX_SCALEUP1_DELAY // delay 1 frame after moveUp1
// Scale down height from 105% to 99% in 116ms, after scaleUp1 (59~66, 7 frames).
val scaleDown2 = ObjectAnimator.ofFloat(
box, "scaleY",
ANIM_BOX_SCALEDOWN2_FROM, ANIM_BOX_SCALEDOWN2_TO
).setDuration(ANIM_BOX_SCALEDOWN2_DURATION)
// Scale up height from 99% to 100% in 133ms, after scaleDown2 (66~74, 8 frames).
val scaleUp3 = ObjectAnimator.ofFloat(
box, "scaleY",
ANIM_BOX_SCALEUP3_FROM, ANIM_BOX_SCALEUP3_TO
).setDuration(ANIM_BOX_SCALEUP3_DURATION)
animatorSet.play(fadeOut).with(moveUp1)
animatorSet.play(moveUp1).before(moveDown2)
animatorSet.play(moveDown2).with(fadeIn)
animatorSet.play(moveDown2).before(moveDown3)
animatorSet.play(moveDown3).before(moveUp4)
animatorSet.play(moveUp1).before(scaleUp1)
animatorSet.play(scaleUp1).before(scaleDown2)
animatorSet.play(scaleDown2).before(scaleUp3)
}
private fun createTextAnimatorSet(animatorSet: AnimatorSet) {
val firstAnimator = animatorSet.childAnimations[0]
// Fadeout in 100ms, with firstAnimator (49~51, 2 frames).
val fadeOut = ObjectAnimator.ofFloat(
text, "alpha",
ANIM_TEXT_FADEOUT_FROM, ANIM_TEXT_FADEOUT_TO
).setDuration(ANIM_TEXT_FADEOUT_DURATION)
// FadeIn in 66 ms, after fadeOut with delay 96ms (57~61, 4 frames).
val fadeIn = ObjectAnimator.ofFloat(
text, "alpha",
ANIM_TEXT_FADEIN_FROM, ANIM_TEXT_FADEIN_TO
).setDuration(ANIM_TEXT_FADEIN_DURATION)
fadeIn.startDelay = (ANIM_TEXT_FADEIN_DELAY).toLong() // delay 6 frames after fadeOut
// Move down on y-axis, from 0 to 4.4 in 66ms, with fadeIn (57~61, 4 frames).
val moveDown = ObjectAnimator.ofFloat(
text, "translationY",
ANIM_TEXT_MOVEDOWN_FROM, ANIM_TEXT_MOVEDOWN_TO
).setDuration(ANIM_TEXT_MOVEDOWN_DURATION)
moveDown.startDelay = (ANIM_TEXT_MOVEDOWN_DELAY) // delay 6 frames after fadeOut
// Move up on y-axis, from 0 to 4.4 in 66ms, after moveDown (61~69, 8 frames).
val moveUp = ObjectAnimator.ofFloat(
text, "translationY",
ANIM_TEXT_MOVEUP_FROM, ANIM_TEXT_MOVEUP_TO
).setDuration(ANIM_TEXT_MOVEUP_DURATION)
animatorSet.play(firstAnimator).with(fadeOut)
animatorSet.play(fadeOut).before(fadeIn)
animatorSet.play(fadeIn).with(moveDown)
animatorSet.play(moveDown).before(moveUp)
}
private fun formatForDisplay(count: Int): String {
return if (count > MAX_VISIBLE_TABS) {
SO_MANY_TABS_OPEN
} else NumberFormat.getInstance().format(count.toLong())
}
private fun adjustTextSize(newCount: Int) {
val newRatio = if (newCount in TWO_DIGITS_TAB_COUNT_THRESHOLD..MAX_VISIBLE_TABS) {
TWO_DIGITS_SIZE_RATIO
} else {
ONE_DIGIT_SIZE_RATIO
}
if (newRatio != currentTextRatio) {
currentTextRatio = newRatio
text.viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
text.viewTreeObserver.removeOnGlobalLayoutListener(this)
val sizeInPixel = (box.width * newRatio).toInt()
if (sizeInPixel > 0) {
// Only apply the size when we calculate a valid value.
text.setTextSize(TypedValue.COMPLEX_UNIT_PX, sizeInPixel.toFloat())
text.setTypeface(null, Typeface.BOLD)
}
}
})
}
}
companion object {
internal const val MAX_VISIBLE_TABS = 99
internal const val SO_MANY_TABS_OPEN = ""
internal const val DEFAULT_TABS_COUNTER_TEXT = ":)"
internal const val ONE_DIGIT_SIZE_RATIO = 0.5f
internal const val TWO_DIGITS_SIZE_RATIO = 0.4f
internal const val TWO_DIGITS_TAB_COUNT_THRESHOLD = 10
// createBoxAnimatorSet
private const val ANIM_BOX_FADEOUT_FROM = 1.0f
private const val ANIM_BOX_FADEOUT_TO = 0.0f
private const val ANIM_BOX_FADEOUT_DURATION = 33L
private const val ANIM_BOX_MOVEUP1_FROM = 0.0f
private const val ANIM_BOX_MOVEUP1_TO = -5.3f
private const val ANIM_BOX_MOVEUP1_DURATION = 50L
private const val ANIM_BOX_MOVEDOWN2_FROM = -5.3f
private const val ANIM_BOX_MOVEDOWN2_TO = -1.0f
private const val ANIM_BOX_MOVEDOWN2_DURATION = 167L
private const val ANIM_BOX_FADEIN_FROM = 0.01f
private const val ANIM_BOX_FADEIN_TO = 1.0f
private const val ANIM_BOX_FADEIN_DURATION = 66L
private const val ANIM_BOX_MOVEDOWN3_FROM = -1.0f
private const val ANIM_BOX_MOVEDOWN3_TO = 2.7f
private const val ANIM_BOX_MOVEDOWN3_DURATION = 116L
private const val ANIM_BOX_MOVEDOWN4_FROM = 2.7f
private const val ANIM_BOX_MOVEDOWN4_TO = 0.0f
private const val ANIM_BOX_MOVEDOWN4_DURATION = 133L
private const val ANIM_BOX_SCALEUP1_FROM = 0.02f
private const val ANIM_BOX_SCALEUP1_TO = 1.05f
private const val ANIM_BOX_SCALEUP1_DURATION = 100L
private const val ANIM_BOX_SCALEUP1_DELAY = 16L
private const val ANIM_BOX_SCALEDOWN2_FROM = 1.05f
private const val ANIM_BOX_SCALEDOWN2_TO = 0.99f
private const val ANIM_BOX_SCALEDOWN2_DURATION = 116L
private const val ANIM_BOX_SCALEUP3_FROM = 0.99f
private const val ANIM_BOX_SCALEUP3_TO = 1.00f
private const val ANIM_BOX_SCALEUP3_DURATION = 133L
// createTextAnimatorSet
private const val ANIM_TEXT_FADEOUT_FROM = 1.0f
private const val ANIM_TEXT_FADEOUT_TO = 0.0f
private const val ANIM_TEXT_FADEOUT_DURATION = 33L
private const val ANIM_TEXT_FADEIN_FROM = 0.01f
private const val ANIM_TEXT_FADEIN_TO = 1.0f
private const val ANIM_TEXT_FADEIN_DURATION = 66L
private const val ANIM_TEXT_FADEIN_DELAY = 16L * 6
private const val ANIM_TEXT_MOVEDOWN_FROM = 0.0f
private const val ANIM_TEXT_MOVEDOWN_TO = 4.4f
private const val ANIM_TEXT_MOVEDOWN_DURATION = 66L
private const val ANIM_TEXT_MOVEDOWN_DELAY = 16L * 6
private const val ANIM_TEXT_MOVEUP_FROM = 4.4f
private const val ANIM_TEXT_MOVEUP_TO = 0.0f
private const val ANIM_TEXT_MOVEUP_DURATION = 66L
}
}

View File

@ -0,0 +1,81 @@
/* 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 org.mozilla.fenix.components.toolbar
import android.util.TypedValue
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.ViewGroup
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.toolbar.Toolbar
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.asActivity
import java.lang.ref.WeakReference
/**
* A [Toolbar.Action] implementation that shows a [TabCounter].
*/
class TabCounterToolbarButton(
private val sessionManager: SessionManager,
private val showTabs: () -> Unit
) : Toolbar.Action {
private var reference: WeakReference<TabCounter> = WeakReference<TabCounter>(null)
override fun createView(parent: ViewGroup): View {
sessionManager.register(sessionManagerObserver)
val view = TabCounter(parent.context).apply {
reference = WeakReference(this)
setCount(
(sessionManager.sessions
.filter {
(context.asActivity() as? HomeActivity)?.browsingModeManager?.isPrivate == it.private
}).size
)
setOnClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
showTabs.invoke()
}
contentDescription =
parent.context.getString(R.string.mozac_feature_tabs_toolbar_tabs_button)
}
// Set selectableItemBackgroundBorderless
val outValue = TypedValue()
parent.context.theme.resolveAttribute(
android.R.attr.selectableItemBackgroundBorderless,
outValue,
true
)
view.setBackgroundResource(outValue.resourceId)
return view
}
override fun bind(view: View) = Unit
private fun updateCount() {
reference.get()?.setCountWithAnimation(sessionManager.sessions.size)
}
private val sessionManagerObserver = object : SessionManager.Observer {
override fun onSessionAdded(session: Session) {
updateCount()
}
override fun onSessionRemoved(session: Session) {
updateCount()
}
override fun onSessionsRestored() {
updateCount()
}
override fun onAllSessionsRemoved() {
updateCount()
}
}
}

View File

@ -5,8 +5,6 @@
package org.mozilla.fenix.components.toolbar
import android.content.Context
import android.graphics.PorterDuff
import androidx.core.content.ContextCompat
import androidx.navigation.Navigation
import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider
import mozilla.components.browser.session.SessionManager
@ -16,8 +14,6 @@ import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature
import mozilla.components.feature.toolbar.ToolbarPresenter
import mozilla.components.support.base.feature.LifecycleAwareFeature
import org.mozilla.fenix.DefaultThemeManager
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.ext.components
@ -35,29 +31,21 @@ class ToolbarIntegration(
toolbar.setMenuBuilder(toolbarMenu.menuBuilder)
toolbar.private = isPrivate
val tabsIcon = context.getDrawable(R.drawable.ic_tabs)
tabsIcon?.setColorFilter(
ContextCompat.getColor(
context,
DefaultThemeManager.resolveAttribute(R.attr.browserToolbarIcons, context)
), PorterDuff.Mode.SRC_IN
)
tabsIcon?.let {
val home = BrowserToolbar.Button(
it,
context.getString(R.string.browser_tabs_button),
visible = {
sessionId == null ||
sessionManager.runWithSession(sessionId) {
it.isCustomTabSession().not()
}
}
) {
Navigation.findNavController(toolbar)
.navigate(BrowserFragmentDirections.actionBrowserFragmentToHomeFragment())
}
run {
sessionManager.runWithSession(sessionId) {
it.isCustomTabSession()
}.also { isCustomTab ->
if (isCustomTab) return@run
toolbar.addBrowserAction(home)
val tabsAction = TabCounterToolbarButton(
sessionManager,
showTabs = {
Navigation.findNavController(toolbar)
.navigate(BrowserFragmentDirections.actionBrowserFragmentToHomeFragment())
}
)
toolbar.addBrowserAction(tabsAction)
}
}
ToolbarAutocompleteFeature(toolbar).apply {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,39 @@
<?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/. -->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:background="@drawable/mozac_ui_tabcounter_round_rectangle_ripple"
tools:layout_height="wrap_content"
tools:layout_width="wrap_content">
<FrameLayout
android:id="@+id/counter_root"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true">
<ImageView
android:id="@+id/counter_box"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@string/mozac_ui_tabcounter_description"
android:src="@drawable/mozac_ui_tabcounter_box"
android:tint="?attr/browserToolbarIcons" />
<!-- This text size auto adjusts based on num digits in `TabCounter` -->
<TextView
android:id="@+id/counter_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="1dp"
android:layout_marginEnd="1dp"
android:textColor="?attr/browserToolbarIcons"
android:textSize="12sp"
android:textStyle="bold"
tools:text="16" />
</FrameLayout>
</merge>