From 9f1ec5e2b035fcc10c63c0c62557f42066a8d98c Mon Sep 17 00:00:00 2001 From: Emily Kager Date: Fri, 5 Apr 2019 14:33:02 -0700 Subject: [PATCH] For #904 -Add tab counter to tab icon --- CHANGELOG.md | 1 + .../fenix/components/toolbar/TabCounter.kt | 309 ++++++++++++++++++ .../toolbar/TabCounterToolbarButton.kt | 81 +++++ .../components/toolbar/ToolbarIntegration.kt | 40 +-- .../res/drawable/mozac_ui_tabcounter_box.xml | 13 + .../res/layout/mozac_ui_tabcounter_layout.xml | 39 +++ 6 files changed, 457 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounter.kt create mode 100644 app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounterToolbarButton.kt create mode 100644 app/src/main/res/drawable/mozac_ui_tabcounter_box.xml create mode 100644 app/src/main/res/layout/mozac_ui_tabcounter_layout.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index f337cd82e..d1b956c9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 \ No newline at end of file diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounter.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounter.kt new file mode 100644 index 000000000..59c34910d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounter.kt @@ -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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounterToolbarButton.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounterToolbarButton.kt new file mode 100644 index 000000000..a5454af15 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounterToolbarButton.kt @@ -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 = WeakReference(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() + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt index b95bb8916..001822d24 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt @@ -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 { diff --git a/app/src/main/res/drawable/mozac_ui_tabcounter_box.xml b/app/src/main/res/drawable/mozac_ui_tabcounter_box.xml new file mode 100644 index 000000000..5ee8437a7 --- /dev/null +++ b/app/src/main/res/drawable/mozac_ui_tabcounter_box.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/layout/mozac_ui_tabcounter_layout.xml b/app/src/main/res/layout/mozac_ui_tabcounter_layout.xml new file mode 100644 index 000000000..810b5a296 --- /dev/null +++ b/app/src/main/res/layout/mozac_ui_tabcounter_layout.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + +