| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * 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.android.deskclock.stopwatch |
| |
| import android.R.attr.state_activated |
| import android.R.attr.state_pressed |
| import android.annotation.SuppressLint |
| import android.app.Activity |
| import android.content.ActivityNotFoundException |
| import android.content.Context |
| import android.content.Intent |
| import android.content.res.ColorStateList |
| import android.content.res.Resources |
| import android.graphics.Canvas |
| import android.graphics.drawable.GradientDrawable |
| import android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM |
| import android.os.Bundle |
| import android.transition.TransitionManager |
| import android.view.LayoutInflater |
| import android.view.MotionEvent |
| import android.view.View |
| import android.view.View.GONE |
| import android.view.View.INVISIBLE |
| import android.view.View.VISIBLE |
| import android.view.ViewGroup |
| import android.view.WindowManager |
| import android.widget.Button |
| import android.widget.ImageView |
| import android.widget.TextView |
| import androidx.annotation.ColorInt |
| import androidx.core.graphics.ColorUtils |
| import androidx.recyclerview.widget.LinearLayoutManager |
| import androidx.recyclerview.widget.RecyclerView |
| import androidx.recyclerview.widget.SimpleItemAnimator |
| |
| import com.android.deskclock.AnimatorUtils |
| import com.android.deskclock.DeskClockFragment |
| import com.android.deskclock.FabContainer |
| import com.android.deskclock.FabContainer.UpdateFabFlag |
| import com.android.deskclock.data.DataModel |
| import com.android.deskclock.data.Lap |
| import com.android.deskclock.data.Stopwatch |
| import com.android.deskclock.data.StopwatchListener |
| import com.android.deskclock.events.Events |
| import com.android.deskclock.LogUtils |
| import com.android.deskclock.R |
| import com.android.deskclock.StopwatchTextController |
| import com.android.deskclock.ThemeUtils |
| import com.android.deskclock.Utils |
| import com.android.deskclock.uidata.TabListener |
| import com.android.deskclock.uidata.UiDataModel |
| |
| import kotlin.math.max |
| import kotlin.math.min |
| import kotlin.math.pow |
| import kotlin.math.roundToInt |
| |
| /** |
| * Fragment that shows the stopwatch and recorded laps. |
| */ |
| // TODO(colinmarsch) Replace deprecated Fragment related calls |
| class StopwatchFragment : DeskClockFragment(UiDataModel.Tab.STOPWATCH) { |
| |
| /** Keep the screen on when this tab is selected. */ |
| private val mTabWatcher: TabListener = TabWatcher() |
| |
| /** Scheduled to update the stopwatch time and current lap time while stopwatch is running. */ |
| private val mTimeUpdateRunnable: Runnable = TimeUpdateRunnable() |
| |
| /** Updates the user interface in response to stopwatch changes. */ |
| private val mStopwatchWatcher: StopwatchListener = StopwatchWatcher() |
| |
| /** Draws a gradient over the bottom of the [.mLapsList] to reduce clash with the fab. */ |
| private var mGradientItemDecoration: GradientItemDecoration? = null |
| |
| /** The data source for [.mLapsList]. */ |
| private lateinit var mLapsAdapter: LapsAdapter |
| |
| /** The layout manager for the [.mLapsAdapter]. */ |
| private lateinit var mLapsLayoutManager: LinearLayoutManager |
| |
| /** Draws the reference lap while the stopwatch is running. */ |
| private var mTime: StopwatchCircleView? = null |
| |
| /** The View containing both TextViews of the stopwatch. */ |
| private lateinit var mStopwatchWrapper: View |
| |
| /** Displays the recorded lap times. */ |
| private lateinit var mLapsList: RecyclerView |
| |
| /** Displays the current stopwatch time (seconds and above only). */ |
| private lateinit var mMainTimeText: TextView |
| |
| /** Displays the current stopwatch time (hundredths only). */ |
| private lateinit var mHundredthsTimeText: TextView |
| |
| /** Formats and displays the text in the stopwatch. */ |
| private lateinit var mStopwatchTextController: StopwatchTextController |
| |
| override fun onCreateView( |
| inflater: LayoutInflater, |
| container: ViewGroup?, |
| state: Bundle? |
| ): View { |
| mLapsAdapter = LapsAdapter(getActivity()) |
| mLapsLayoutManager = LinearLayoutManager(getActivity()) |
| mGradientItemDecoration = GradientItemDecoration(getActivity()) |
| |
| val v: View = inflater.inflate(R.layout.stopwatch_fragment, container, false) |
| mTime = v.findViewById(R.id.stopwatch_circle) |
| mLapsList = v.findViewById(R.id.laps_list) as RecyclerView |
| (mLapsList.getItemAnimator() as SimpleItemAnimator).setSupportsChangeAnimations(false) |
| mLapsList.setLayoutManager(mLapsLayoutManager) |
| mLapsList.addItemDecoration(mGradientItemDecoration!!) |
| |
| // In landscape layouts, the laps list can reach the top of the screen and thus can cause |
| // a drop shadow to appear. The same is not true for portrait landscapes. |
| if (Utils.isLandscape(getActivity())) { |
| val scrollPositionWatcher = ScrollPositionWatcher() |
| mLapsList.addOnLayoutChangeListener(scrollPositionWatcher) |
| mLapsList.addOnScrollListener(scrollPositionWatcher) |
| } else { |
| setTabScrolledToTop(true) |
| } |
| mLapsList.setAdapter(mLapsAdapter) |
| |
| // Timer text serves as a virtual start/stop button. |
| mMainTimeText = v.findViewById(R.id.stopwatch_time_text) as TextView |
| mHundredthsTimeText = v.findViewById(R.id.stopwatch_hundredths_text) as TextView |
| mStopwatchTextController = StopwatchTextController(mMainTimeText, mHundredthsTimeText) |
| mStopwatchWrapper = v.findViewById(R.id.stopwatch_time_wrapper) |
| |
| DataModel.dataModel.addStopwatchListener(mStopwatchWatcher) |
| |
| mStopwatchWrapper.setOnClickListener(TimeClickListener()) |
| if (mTime != null) { |
| mStopwatchWrapper.setOnTouchListener(CircleTouchListener()) |
| } |
| |
| val c: Context = mMainTimeText.getContext() |
| val colorAccent = ThemeUtils.resolveColor(c, R.attr.colorAccent) |
| val textColorPrimary = ThemeUtils.resolveColor(c, android.R.attr.textColorPrimary) |
| val timeTextColor = |
| ColorStateList( |
| arrayOf(intArrayOf(-state_activated, -state_pressed), intArrayOf()), |
| intArrayOf(textColorPrimary, colorAccent) |
| ) |
| mMainTimeText.setTextColor(timeTextColor) |
| mHundredthsTimeText.setTextColor(timeTextColor) |
| |
| return v |
| } |
| |
| override fun onStart() { |
| super.onStart() |
| |
| val activity: Activity = getActivity() |
| val intent: Intent? = activity.getIntent() |
| if (intent != null) { |
| val action: String? = intent.getAction() |
| if (StopwatchService.Companion.ACTION_START_STOPWATCH == action) { |
| DataModel.dataModel.startStopwatch() |
| // Consume the intent |
| activity.setIntent(null) |
| } else if (StopwatchService.Companion.ACTION_PAUSE_STOPWATCH == action) { |
| DataModel.dataModel.pauseStopwatch() |
| // Consume the intent |
| activity.setIntent(null) |
| } |
| } |
| |
| // Conservatively assume the data in the adapter has changed while the fragment was paused. |
| mLapsAdapter.notifyDataSetChanged() |
| |
| // Synchronize the user interface with the data model. |
| updateUI(FabContainer.FAB_AND_BUTTONS_IMMEDIATE) |
| |
| // Start watching for page changes away from this fragment. |
| UiDataModel.uiDataModel.addTabListener(mTabWatcher) |
| } |
| |
| override fun onStop() { |
| super.onStop() |
| |
| // Stop all updates while the fragment is not visible. |
| stopUpdatingTime() |
| |
| // Stop watching for page changes away from this fragment. |
| UiDataModel.uiDataModel.removeTabListener(mTabWatcher) |
| |
| // Release the wake lock if it is currently held. |
| releaseWakeLock() |
| } |
| |
| override fun onDestroyView() { |
| super.onDestroyView() |
| |
| DataModel.dataModel.removeStopwatchListener(mStopwatchWatcher) |
| } |
| |
| override fun onFabClick(fab: ImageView) { |
| toggleStopwatchState() |
| } |
| |
| override fun onLeftButtonClick(left: Button) { |
| doReset() |
| } |
| |
| override fun onRightButtonClick(right: Button) { |
| when (stopwatch.state) { |
| Stopwatch.State.RUNNING -> doAddLap() |
| Stopwatch.State.PAUSED -> doShare() |
| Stopwatch.State.RESET -> { |
| } |
| null -> { |
| } |
| } |
| } |
| |
| private fun updateFab(fab: ImageView, animate: Boolean) { |
| if (stopwatch.isRunning) { |
| if (animate) { |
| fab.setImageResource(R.drawable.ic_play_pause_animation) |
| } else { |
| fab.setImageResource(R.drawable.ic_play_pause) |
| } |
| fab.setContentDescription(fab.getResources().getString(R.string.sw_pause_button)) |
| } else { |
| if (animate) { |
| fab.setImageResource(R.drawable.ic_pause_play_animation) |
| } else { |
| fab.setImageResource(R.drawable.ic_pause_play) |
| } |
| fab.setContentDescription(fab.getResources().getString(R.string.sw_start_button)) |
| } |
| fab.setVisibility(VISIBLE) |
| } |
| |
| override fun onUpdateFab(fab: ImageView) { |
| updateFab(fab, false) |
| } |
| |
| override fun onMorphFab(fab: ImageView) { |
| // Update the fab's drawable to match the current timer state. |
| updateFab(fab, Utils.isNOrLater()) |
| // Animate the drawable. |
| AnimatorUtils.startDrawableAnimation(fab) |
| } |
| |
| override fun onUpdateFabButtons(left: Button, right: Button) { |
| val resources: Resources = getResources() |
| left.setClickable(true) |
| left.setText(R.string.sw_reset_button) |
| left.setContentDescription(resources.getString(R.string.sw_reset_button)) |
| |
| when (stopwatch.state) { |
| Stopwatch.State.RESET -> { |
| left.setVisibility(INVISIBLE) |
| right.setClickable(true) |
| right.setVisibility(INVISIBLE) |
| } |
| Stopwatch.State.RUNNING -> { |
| left.setVisibility(VISIBLE) |
| val canRecordLaps = canRecordMoreLaps() |
| right.setText(R.string.sw_lap_button) |
| right.setContentDescription(resources.getString(R.string.sw_lap_button)) |
| right.setClickable(canRecordLaps) |
| right.setVisibility(if (canRecordLaps) VISIBLE else INVISIBLE) |
| } |
| Stopwatch.State.PAUSED -> { |
| left.setVisibility(VISIBLE) |
| right.setClickable(true) |
| right.setVisibility(VISIBLE) |
| right.setText(R.string.sw_share_button) |
| right.setContentDescription(resources.getString(R.string.sw_share_button)) |
| } |
| null -> { |
| } |
| } |
| } |
| |
| /** |
| * @param color the newly installed app window color |
| */ |
| override fun onAppColorChanged(@ColorInt color: Int) { |
| mGradientItemDecoration?.updateGradientColors(color) |
| mLapsList.invalidateItemDecorations() |
| } |
| |
| /** |
| * Start the stopwatch. |
| */ |
| private fun doStart() { |
| Events.sendStopwatchEvent(R.string.action_start, R.string.label_deskclock) |
| DataModel.dataModel.startStopwatch() |
| } |
| |
| /** |
| * Pause the stopwatch. |
| */ |
| private fun doPause() { |
| Events.sendStopwatchEvent(R.string.action_pause, R.string.label_deskclock) |
| DataModel.dataModel.pauseStopwatch() |
| } |
| |
| /** |
| * Reset the stopwatch. |
| */ |
| private fun doReset() { |
| val priorState = stopwatch.state |
| Events.sendStopwatchEvent(R.string.action_reset, R.string.label_deskclock) |
| DataModel.dataModel.resetStopwatch() |
| mMainTimeText.setAlpha(1f) |
| mHundredthsTimeText.setAlpha(1f) |
| if (priorState == Stopwatch.State.RUNNING) { |
| updateFab(FabContainer.FAB_MORPH) |
| } |
| } |
| |
| /** |
| * Send stopwatch time and lap times to an external sharing application. |
| */ |
| private fun doShare() { |
| // Disable the fab buttons to avoid double-taps on the share button. |
| updateFab(FabContainer.BUTTONS_DISABLE) |
| |
| val subjects: Array<String> = getResources().getStringArray(R.array.sw_share_strings) |
| val subject = subjects[(Math.random() * subjects.size).toInt()] |
| val text = mLapsAdapter.shareText |
| |
| @SuppressLint("InlinedApi") |
| val shareIntent: Intent = Intent(Intent.ACTION_SEND) |
| .addFlags(if (Utils.isLOrLater()) { |
| Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
| } else { |
| Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET |
| }) |
| .putExtra(Intent.EXTRA_SUBJECT, subject) |
| .putExtra(Intent.EXTRA_TEXT, text) |
| .setType("text/plain") |
| |
| val context: Context = getActivity() |
| val title: String = context.getString(R.string.sw_share_button) |
| val shareChooserIntent: Intent = Intent.createChooser(shareIntent, title) |
| try { |
| context.startActivity(shareChooserIntent) |
| } catch (anfe: ActivityNotFoundException) { |
| LogUtils.e("Cannot share lap data because no suitable receiving Activity exists") |
| updateFab(FabContainer.BUTTONS_IMMEDIATE) |
| } |
| } |
| |
| /** |
| * Record and add a new lap ending now. |
| */ |
| private fun doAddLap() { |
| Events.sendStopwatchEvent(R.string.action_lap, R.string.label_deskclock) |
| |
| // Record a new lap. |
| val lap = mLapsAdapter.addLap() ?: return |
| |
| // Update button states. |
| updateFab(FabContainer.BUTTONS_IMMEDIATE) |
| if (lap.lapNumber == 1) { |
| // Child views from prior lap sets hang around and blit to the screen when adding the |
| // first lap of the subsequent lap set. Remove those superfluous children here manually |
| // to ensure they aren't seen as the first lap is drawn. |
| mLapsList.removeAllViewsInLayout() |
| if (mTime != null) { |
| // Start animating the reference lap. |
| mTime!!.update() |
| } |
| |
| // Recording the first lap transitions the UI to display the laps list. |
| showOrHideLaps(false) |
| } |
| |
| // Ensure the newly added lap is visible on screen. |
| mLapsList.scrollToPosition(0) |
| } |
| |
| /** |
| * Show or hide the list of laps. |
| */ |
| private fun showOrHideLaps(clearLaps: Boolean) { |
| val sceneRoot: ViewGroup = getView() as ViewGroup? ?: return |
| |
| TransitionManager.beginDelayedTransition(sceneRoot) |
| |
| if (clearLaps) { |
| mLapsAdapter.clearLaps() |
| } |
| |
| val lapsVisible = mLapsAdapter.getItemCount() > 0 |
| mLapsList.setVisibility(if (lapsVisible) VISIBLE else GONE) |
| |
| if (Utils.isPortrait(getActivity())) { |
| // When the lap list is visible, it includes the bottom padding. When it is absent the |
| // appropriate bottom padding must be applied to the container. |
| val res: Resources = getResources() |
| val bottom = if (lapsVisible) 0 else res.getDimensionPixelSize(R.dimen.fab_height) |
| val top: Int = sceneRoot.getPaddingTop() |
| val left: Int = sceneRoot.getPaddingLeft() |
| val right: Int = sceneRoot.getPaddingRight() |
| sceneRoot.setPadding(left, top, right, bottom) |
| } |
| } |
| |
| private fun adjustWakeLock() { |
| val appInForeground = DataModel.dataModel.isApplicationInForeground |
| if (stopwatch.isRunning && isTabSelected && appInForeground) { |
| getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) |
| } else { |
| releaseWakeLock() |
| } |
| } |
| |
| private fun releaseWakeLock() { |
| getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) |
| } |
| |
| /** |
| * Either pause or start the stopwatch based on its current state. |
| */ |
| private fun toggleStopwatchState() { |
| if (stopwatch.isRunning) { |
| doPause() |
| } else { |
| doStart() |
| } |
| } |
| |
| private val stopwatch: Stopwatch |
| get() = DataModel.dataModel.stopwatch |
| |
| private fun canRecordMoreLaps(): Boolean = DataModel.dataModel.canAddMoreLaps() |
| |
| /** |
| * Post the first runnable to update times within the UI. It will reschedule itself as needed. |
| */ |
| private fun startUpdatingTime() { |
| // Ensure only one copy of the runnable is ever scheduled by first stopping updates. |
| stopUpdatingTime() |
| mMainTimeText.post(mTimeUpdateRunnable) |
| } |
| |
| /** |
| * Remove the runnable that updates times within the UI. |
| */ |
| private fun stopUpdatingTime() { |
| mMainTimeText.removeCallbacks(mTimeUpdateRunnable) |
| } |
| |
| /** |
| * Update all time displays based on a single snapshot of the stopwatch progress. This includes |
| * the stopwatch time drawn in the circle, the current lap time and the total elapsed time in |
| * the list of laps. |
| */ |
| private fun updateTime() { |
| // Compute the total time of the stopwatch. |
| val stopwatch = stopwatch |
| val totalTime = stopwatch.totalTime |
| mStopwatchTextController.setTimeString(totalTime) |
| |
| // Update the current lap. |
| val currentLapIsVisible = mLapsLayoutManager.findFirstVisibleItemPosition() == 0 |
| if (!stopwatch.isReset && currentLapIsVisible) { |
| mLapsAdapter.updateCurrentLap(mLapsList, totalTime) |
| } |
| } |
| |
| /** |
| * Synchronize the UI state with the model data. |
| */ |
| private fun updateUI(@UpdateFabFlag updateTypes: Int) { |
| adjustWakeLock() |
| |
| // Draw the latest stopwatch and current lap times. |
| updateTime() |
| if (mTime != null) { |
| mTime!!.update() |
| } |
| val stopwatch = stopwatch |
| if (!stopwatch.isReset) { |
| startUpdatingTime() |
| } |
| |
| // Adjust the visibility of the list of laps. |
| showOrHideLaps(stopwatch.isReset) |
| |
| // Update button states. |
| updateFab(updateTypes) |
| } |
| |
| /** |
| * This runnable periodically updates times throughout the UI. It stops these updates when the |
| * stopwatch is no longer running. |
| */ |
| private inner class TimeUpdateRunnable : Runnable { |
| override fun run() { |
| val startTime = Utils.now() |
| updateTime() |
| |
| // Blink text iff the stopwatch is paused and not pressed. |
| val touchTarget: View = if (mTime != null) mTime!! else mStopwatchWrapper |
| val stopwatch = stopwatch |
| val blink = (stopwatch.isPaused && startTime % 1000 < 500 && !touchTarget.isPressed()) |
| |
| if (blink) { |
| mMainTimeText.setAlpha(0f) |
| mHundredthsTimeText.setAlpha(0f) |
| } else { |
| mMainTimeText.setAlpha(1f) |
| mHundredthsTimeText.setAlpha(1f) |
| } |
| |
| if (!stopwatch.isReset) { |
| val period = (if (stopwatch.isPaused) { |
| REDRAW_PERIOD_PAUSED |
| } else { |
| REDRAW_PERIOD_RUNNING |
| }).toLong() |
| val endTime = Utils.now() |
| val delay: Long = max(0, startTime + period - endTime).toLong() |
| mMainTimeText.postDelayed(this, delay) |
| } |
| } |
| } |
| |
| /** |
| * Acquire or release the wake lock based on the tab state. |
| */ |
| private inner class TabWatcher : TabListener { |
| override fun selectedTabChanged( |
| oldSelectedTab: UiDataModel.Tab, |
| newSelectedTab: UiDataModel.Tab |
| ) { |
| adjustWakeLock() |
| } |
| } |
| |
| /** |
| * Update the user interface in response to a stopwatch change. |
| */ |
| private inner class StopwatchWatcher : StopwatchListener { |
| override fun stopwatchUpdated(before: Stopwatch, after: Stopwatch) { |
| if (after.isReset) { |
| // Ensure the drop shadow is hidden when the stopwatch is reset. |
| setTabScrolledToTop(true) |
| if (DataModel.dataModel.isApplicationInForeground) { |
| updateUI(FabContainer.BUTTONS_IMMEDIATE) |
| } |
| return |
| } |
| if (DataModel.dataModel.isApplicationInForeground) { |
| updateUI(FabContainer.FAB_MORPH or FabContainer.BUTTONS_IMMEDIATE) |
| } |
| } |
| |
| override fun lapAdded(lap: Lap) { |
| } |
| } |
| |
| /** |
| * Toggles stopwatch state when user taps stopwatch. |
| */ |
| private inner class TimeClickListener : View.OnClickListener { |
| |
| override fun onClick(view: View?) { |
| if (stopwatch.isRunning) { |
| DataModel.dataModel.pauseStopwatch() |
| } else { |
| DataModel.dataModel.startStopwatch() |
| } |
| } |
| } |
| |
| /** |
| * Checks if the user is pressing inside of the stopwatch circle. |
| */ |
| private inner class CircleTouchListener : View.OnTouchListener { |
| |
| override fun onTouch(view: View, event: MotionEvent): Boolean { |
| val actionMasked: Int = event.getActionMasked() |
| if (actionMasked != MotionEvent.ACTION_DOWN) { |
| return false |
| } |
| val rX: Float = view.getWidth() / 2f |
| val rY: Float = (view.getHeight() - view.getPaddingBottom()) / 2f |
| val r = min(rX, rY) |
| |
| val x: Float = event.getX() - rX |
| val y: Float = event.getY() - rY |
| |
| val inCircle = (x / r.toDouble()).pow(2.0) + (y / r.toDouble()).pow(2.0) <= 1.0 |
| |
| // Consume the event if it is outside the circle |
| return !inCircle |
| } |
| } |
| |
| /** |
| * Updates the vertical scroll state of this tab in the [UiDataModel] as the user scrolls |
| * the recyclerview or when the size/position of elements within the recyclerview changes. |
| */ |
| private inner class ScrollPositionWatcher : |
| RecyclerView.OnScrollListener(), View.OnLayoutChangeListener { |
| |
| override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { |
| setTabScrolledToTop(Utils.isScrolledToTop(mLapsList)) |
| } |
| |
| override fun onLayoutChange( |
| v: View?, |
| left: Int, |
| top: Int, |
| right: Int, |
| bottom: Int, |
| oldLeft: Int, |
| oldTop: Int, |
| oldRight: Int, |
| oldBottom: Int |
| ) { |
| setTabScrolledToTop(Utils.isScrolledToTop(mLapsList)) |
| } |
| } |
| |
| /** |
| * Draws a tinting gradient over the bottom of the stopwatch laps list. This reduces the |
| * contrast between floating buttons and the laps list content. |
| */ |
| private class GradientItemDecoration internal constructor(context: Context) |
| : RecyclerView.ItemDecoration() { |
| |
| /** |
| * A reusable array of control point colors that define the gradient. It is based on the |
| * background color of the window and thus recomputed each time that color is changed. |
| */ |
| private val mGradientColors = IntArray(ALPHAS.size) |
| |
| /** The drawable that produces the tinting gradient effect of this decoration. */ |
| private val mGradient: GradientDrawable = GradientDrawable() |
| |
| /** The height of the gradient; sized relative to the fab height. */ |
| private val mGradientHeight: Int |
| |
| init { |
| mGradient.setOrientation(TOP_BOTTOM) |
| updateGradientColors(ThemeUtils.resolveColor(context, android.R.attr.windowBackground)) |
| |
| val resources: Resources = context.getResources() |
| val fabHeight: Int = resources.getDimensionPixelSize(R.dimen.fab_height) |
| mGradientHeight = (fabHeight * 1.2f).roundToInt() |
| } |
| |
| override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { |
| super.onDrawOver(c, parent, state) |
| |
| val w: Int = parent.getWidth() |
| val h: Int = parent.getHeight() |
| |
| mGradient.setBounds(0, h - mGradientHeight, w, h) |
| mGradient.draw(c) |
| } |
| |
| /** |
| * Given a `baseColor`, compute a gradient of tinted colors that define the fade |
| * effect to apply to the bottom of the lap list. |
| * |
| * @param baseColor a base color to which the gradient tint should be applied |
| */ |
| fun updateGradientColors(@ColorInt baseColor: Int) { |
| // Compute the tinted colors that form the gradient. |
| mGradientColors.indices.forEach { i -> |
| mGradientColors[i] = ColorUtils.setAlphaComponent(baseColor, ALPHAS[i]) |
| } |
| |
| // Set the gradient colors into the drawable. |
| mGradient.setColors(mGradientColors) |
| } |
| |
| companion object { |
| // 0% - 25% of gradient length -> opacity changes from 0% to 50% |
| // 25% - 90% of gradient length -> opacity changes from 50% to 100% |
| // 90% - 100% of gradient length -> opacity remains at 100% |
| private val ALPHAS = intArrayOf( |
| 0x00, // 0% |
| 0x1A, // 10% |
| 0x33, // 20% |
| 0x4D, // 30% |
| 0x66, // 40% |
| 0x80, // 50% |
| 0x89, // 53.8% |
| 0x93, // 57.6% |
| 0x9D, // 61.5% |
| 0xA7, // 65.3% |
| 0xB1, // 69.2% |
| 0xBA, // 73.0% |
| 0xC4, // 76.9% |
| 0xCE, // 80.7% |
| 0xD8, // 84.6% |
| 0xE2, // 88.4% |
| 0xEB, // 92.3% |
| 0xF5, // 96.1% |
| 0xFF, // 100% |
| 0xFF, // 100% |
| 0xFF) |
| } |
| } |
| |
| companion object { |
| /** Milliseconds between redraws while running. */ |
| private const val REDRAW_PERIOD_RUNNING = 25 |
| |
| /** Milliseconds between redraws while paused. */ |
| private const val REDRAW_PERIOD_PAUSED = 500 |
| } |
| } |