Skip to content

Commit

Permalink
Add TrackerCountAnimator to animate blocked tracker counts in TabSwit…
Browse files Browse the repository at this point in the history
…cher

Just using random numbers for now. Real tracker count will be added in a future PR.
  • Loading branch information
mikescamell committed Feb 28, 2025
1 parent 6bf1347 commit ce152ee
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,12 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
@Inject
lateinit var tabSwitcherAnimationFeature: TabSwitcherAnimationFeature

@Inject
lateinit var trackerCountAnimator: TrackerCountAnimator

private val viewModel: TabSwitcherViewModel by bindViewModel()

private val tabsAdapter: TabSwitcherAdapter by lazy { TabSwitcherAdapter(this, webViewPreviewPersister, this, faviconManager, dispatchers) }
private val tabsAdapter: TabSwitcherAdapter by lazy { TabSwitcherAdapter(this, webViewPreviewPersister, this, faviconManager, dispatchers, trackerCountAnimator) }

// we need to scroll to show selected tab, but only if it is the first time loading the tabs.
private var firstTimeLoadingTabsList = true
Expand Down
23 changes: 21 additions & 2 deletions app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.databinding.ItemGridTrackerAnimationTileBinding
import com.duckduckgo.app.browser.databinding.ItemListTrackerAnimationTileBinding
import com.duckduckgo.app.browser.databinding.ItemTabGridBinding
Expand All @@ -60,6 +61,7 @@ import kotlin.Int
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import kotlin.random.Random

private const val ANIMATED_TILE_NO_REPLACE_ALPHA = 0.4f
private const val ANIMATED_TILE_DEFAULT_ALPHA = 1f
Expand All @@ -71,6 +73,7 @@ class TabSwitcherAdapter(
private val lifecycleOwner: LifecycleOwner,
private val faviconManager: FaviconManager,
private val dispatchers: DispatcherProvider,
private val trackerCountAnimator: TrackerCountAnimator,
) : Adapter<ViewHolder>() {

private val list = mutableListOf<TabSwitcherItem>()
Expand Down Expand Up @@ -137,13 +140,29 @@ class TabSwitcherAdapter(
bindListTab(holder, tab)
}
is TabSwitcherViewHolder.GridTrackerAnimationTileViewHolder -> {
// TODO animate number of trackers blocked
trackerCountAnimator.animateTrackersBlockedCountView(
context = holder.binding.root.context,
stringRes = R.string.trackersBlockedInTheLast7days,
totalTrackerCount = when (Random.Default.nextInt(10)) {
in 0..6 -> Random.Default.nextInt(10, 1000)
else -> Random.Default.nextInt(1000, 10000)
},
trackerTextView = holder.binding.text,
)
holder.binding.close.setOnClickListener {
// TODO delete
}
}
is TabSwitcherViewHolder.ListTrackerAnimationTileViewHolder -> {
// TODO animate number of trackers blocked
trackerCountAnimator.animateTrackersBlockedCountView(
context = holder.binding.root.context,
stringRes = R.string.trackersBlocked,
totalTrackerCount = when (Random.Default.nextInt(10)) {
in 0..6 -> Random.Default.nextInt(10, 1000)
else -> Random.Default.nextInt(1000, 10000)
},
trackerTextView = holder.binding.title,
)
holder.binding.close.setOnClickListener {
// TODO delete
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2025 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.tabs.ui

import android.animation.ValueAnimator
import android.content.Context
import android.text.Spanned
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.text.HtmlCompat
import com.duckduckgo.app.browser.R
import javax.inject.Inject
import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.seconds

private const val TRACKER_COUNT_LOWER_THRESHOLD_PERCENTAGE = 0.25f
private const val TRACKER_COUNT_UPPER_THRESHOLD_PERCENTAGE = 0.75f
private const val TRACKER_COUNT_UPPER_THRESHOLD = 40
private const val TRACKER_TOTAL_MAX_LIMIT = 9999
private val TRACKER_COUNT_LOWER_THRESHOLD_ANIMATION_DURATION = 0.5.seconds
private val TRACKER_COUNT_UPPER_THRESHOLD_ANIMATION_DURATION = 1.seconds

class TrackerCountAnimator @Inject constructor() {

@StringRes private var stringRes: Int? = null
private var trackerTextView: TextView? = null
private lateinit var context: Context

private val animator = ValueAnimator().apply {
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener { animation ->
val newTrackerCount = animation.animatedValue as Int
trackerTextView?.text = getFormattedText(context, stringRes!!, newTrackerCount)
}
}

fun animateTrackersBlockedCountView(
context: Context,
@StringRes stringRes: Int,
totalTrackerCount: Int,
trackerTextView: TextView,
) {
this.context = context
this.stringRes = stringRes
this.trackerTextView = trackerTextView

val endCount = totalTrackerCount.coerceAtMost(TRACKER_TOTAL_MAX_LIMIT)

val startPercentage = if (endCount >= TRACKER_COUNT_UPPER_THRESHOLD) {
TRACKER_COUNT_UPPER_THRESHOLD_PERCENTAGE
} else {
TRACKER_COUNT_LOWER_THRESHOLD_PERCENTAGE
}

val startCount = (endCount * startPercentage).roundToInt()

val animationDuration = if (endCount >= TRACKER_COUNT_UPPER_THRESHOLD) {
TRACKER_COUNT_UPPER_THRESHOLD_ANIMATION_DURATION
} else {
TRACKER_COUNT_LOWER_THRESHOLD_ANIMATION_DURATION
}

trackerTextView.text = getFormattedText(context, stringRes, startCount)

animator.setIntValues(startCount, endCount)
animator.duration = animationDuration.inWholeMilliseconds
animator.start()
}

private fun getFormattedText(context: Context, @StringRes stringRes: Int, trackerCount: Int): Spanned {
val text = context.getString(stringRes, trackerCount)
return HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
}
4 changes: 4 additions & 0 deletions app/src/main/res/values/donottranslate.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,8 @@
<!-- Skip Onboarding (not user-facing) -->
<string name="skipOnboarding">Skip Onboarding</string>"

<string name="trackersBlockedInTheLast7days" tools:ignore="MissingInstruction">&lt;b&gt;%1$d trackers blocked &lt;/b&gt; in the last 7 days</string>
<string name="trackersBlocked" tools:ignore="MissingInstruction">&lt;b&gt;%1$d trackers blocked &lt;/b&gt;</string>
<string name="inTheLast7Days" tools:ignore="MissingInstruction">in the last 7 days</string>

</resources>

0 comments on commit ce152ee

Please sign in to comment.