Add support for LS Clock to use > 1 Paint object

Updated TextInterpolator and TextAnimator to support a
Paint object for each line of text. Now we can interpolate between
the colors when animating.

Clock colors are now based on the theme (for now, wallpaper text
color). ie: first line (of vertical clock) is one color, second line is
another color.

Also adjusted Keyguard clock logic to animate on doze changes instead of
animating incrementally on setDarkAmount.

Test: atest TextInterpolatorTest TextAnimatorTest
Bug: 170228350
Change-Id: Ia62f2de48534be2007afc5a95eac46b3c44ec880
This commit is contained in:
Beverly
2020-11-16 14:29:59 -05:00
parent 06f759835a
commit 192b7c9eaa
11 changed files with 373 additions and 407 deletions

View File

@@ -69,8 +69,8 @@
android:layout_alignParentTop="true"
android:layout_marginBottom="24dp"
android:visibility="gone">
<com.android.keyguard.GradientTextClock
android:id="@+id/gradient_clock_view"
<com.android.keyguard.AnimatableClockView
android:id="@+id/animatable_clock_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="80dp"

View File

@@ -0,0 +1,81 @@
/*
* 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.keyguard;
import android.graphics.Color;
import com.android.settingslib.Utils;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.util.ViewController;
/**
* Controls the color of a GradientTextClock.
*/
public class AnimatableClockController extends ViewController<AnimatableClockView> {
private final StatusBarStateController mStatusBarStateController;
private final int[] mDozingColors = new int[] {Color.WHITE, Color.WHITE};
private int[] mLockScreenColors = new int[2];
private boolean mIsDozing;
public AnimatableClockController(
AnimatableClockView view,
StatusBarStateController statusBarStateController) {
super(view);
mStatusBarStateController = statusBarStateController;
mIsDozing = mStatusBarStateController.isDozing();
}
@Override
protected void onViewAttached() {
mStatusBarStateController.addCallback(mStatusBarStateListener);
mIsDozing = mStatusBarStateController.isDozing();
refreshTime();
initColors();
}
@Override
protected void onViewDetached() {
mStatusBarStateController.removeCallback(mStatusBarStateListener);
}
/**
* Updates the time for this view.
*/
public void refreshTime() {
mView.refreshTime();
}
private void initColors() {
mLockScreenColors[0] = Utils.getColorAttrDefaultColor(getContext(),
com.android.systemui.R.attr.wallpaperTextColor);
mLockScreenColors[1] = Utils.getColorAttrDefaultColor(getContext(),
com.android.systemui.R.attr.wallpaperTextColorSecondary);
mView.setColors(mDozingColors, mLockScreenColors);
mView.animateDoze(mIsDozing, false);
}
private final StatusBarStateController.StateListener mStatusBarStateListener =
new StatusBarStateController.StateListener() {
@Override
public void onDozingChanged(boolean isDozing) {
mIsDozing = isDozing;
mView.animateDoze(mIsDozing, true);
}
};
}

View File

@@ -0,0 +1,158 @@
/*
* 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.keyguard;
import android.annotation.FloatRange;
import android.annotation.IntRange;
import android.content.Context;
import android.graphics.Canvas;
import android.icu.text.DateTimePatternGenerator;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.widget.TextView;
import java.util.Calendar;
import kotlin.Unit;
/**
* Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
* The time's text color is a gradient that changes its colors based on its controller.
*/
public class AnimatableClockView extends TextView {
private static final CharSequence FORMAT_12_HOUR = "hh\nmm";
private static final CharSequence FORMAT_24_HOUR = "HH\nmm";
private static final long ANIM_DURATION = 300;
private CharSequence mFormat;
private CharSequence mDescFormat;
private Calendar mTime = Calendar.getInstance();
private int[] mDozingColors;
private int[] mLockScreenColors;
private TextAnimator mTextAnimator = null;
private Runnable mOnTextAnimatorInitialized;
public AnimatableClockView(Context context) {
this(context, null, 0, 0);
}
public AnimatableClockView(Context context, AttributeSet attrs) {
this(context, attrs, 0, 0);
}
public AnimatableClockView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public AnimatableClockView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
updateTimeFormat();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
updateTimeFormat();
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
}
void refreshTime() {
mTime.setTimeInMillis(System.currentTimeMillis());
setText(DateFormat.format(mFormat, mTime));
setContentDescription(DateFormat.format(mDescFormat, mTime));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTextAnimator == null) {
mTextAnimator = new TextAnimator(
getLayout(),
() -> {
invalidate();
return Unit.INSTANCE;
},
2 /* number of lines (each can have a unique Paint) */);
if (mOnTextAnimatorInitialized != null) {
mOnTextAnimatorInitialized.run();
mOnTextAnimatorInitialized = null;
}
} else {
mTextAnimator.updateLayout(getLayout());
}
}
@Override
protected void onDraw(Canvas canvas) {
mTextAnimator.draw(canvas);
}
void setColors(int[] dozingColors, int[] lockScreenColors) {
mDozingColors = dozingColors;
mLockScreenColors = lockScreenColors;
}
void animateDoze(boolean isDozing, boolean animate) {
setTextStyle(isDozing ? 100 : 300 /* weight */,
-1,
isDozing ? mDozingColors : mLockScreenColors,
animate);
}
/**
* Set text style with an optional animation.
*
* By passing -1 to weight, the view preserves its current weight.
* By passing -1 to textSize, the view preserves its current text size.
*
* @param weight text weight.
* @param textSize font size.
* @param animate true to animate the text style change, otherwise false.
*/
private void setTextStyle(
@IntRange(from = 0, to = 1000) int weight,
@FloatRange(from = 0) float textSize,
int[] colors,
boolean animate) {
if (mTextAnimator != null) {
mTextAnimator.setTextStyle(weight, textSize, colors, animate, ANIM_DURATION, null);
} else {
// when the text animator is set, update its start values
mOnTextAnimatorInitialized =
() -> mTextAnimator.setTextStyle(
weight, textSize, colors, false, ANIM_DURATION, null);
}
}
private void updateTimeFormat() {
final boolean use24HourFormat = DateFormat.is24HourFormat(getContext());
mFormat = use24HourFormat ? FORMAT_24_HOUR : FORMAT_12_HOUR;
mDescFormat = getBestDateTimePattern(getContext(), use24HourFormat ? "Hm" : "hm");
}
private static String getBestDateTimePattern(Context context, String skeleton) {
DateTimePatternGenerator dtpg = DateTimePatternGenerator.getInstance(
context.getResources().getConfiguration().locale);
return dtpg.getBestPattern(skeleton);
}
}

View File

@@ -1,149 +0,0 @@
/*
* 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.keyguard;
import android.annotation.FloatRange;
import android.annotation.IntRange;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.widget.TextClock;
import kotlin.Unit;
/**
* Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
* The time's text color is a gradient that changes its colors based on its controller.
*/
public class GradientTextClock extends TextClock {
private int[] mGradientColors;
private float[] mPositions;
private TextAnimator mTextAnimator = null;
public GradientTextClock(Context context) {
this(context, null, 0, 0);
}
public GradientTextClock(Context context, AttributeSet attrs) {
this(context, attrs, 0, 0);
}
public GradientTextClock(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public GradientTextClock(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
addOnLayoutChangeListener(mOnLayoutChangeListener);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeOnLayoutChangeListener(mOnLayoutChangeListener);
}
@Override
public void refreshTime() {
super.refreshTime();
}
@Override
public void setFormat12Hour(CharSequence format) {
super.setFormat12Hour(FORMAT_12);
}
@Override
public void setFormat24Hour(CharSequence format) {
super.setFormat24Hour(FORMAT_24);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTextAnimator == null) {
mTextAnimator = new TextAnimator(getLayout(), () -> {
invalidate();
return Unit.INSTANCE;
});
} else {
mTextAnimator.updateLayout(getLayout());
}
}
@Override
protected void onDraw(Canvas canvas) {
mTextAnimator.draw(canvas);
}
public void setGradientColors(int[] colors) {
mGradientColors = colors;
updatePaint();
}
public void setColorPositions(float[] positions) {
mPositions = positions;
}
/**
* Set text style with animation.
*
* By passing -1 to weight, the view preserve the current weight.
* By passing -1 to textSize, the view preserve the current text size.
*
* @param weight text weight.
* @param textSize font size.
* @param animate true for changing text style with animation, otherwise false.
*/
public void setTextStyle(
@IntRange(from = 0, to = 1000) int weight,
@FloatRange(from = 0) float textSize,
boolean animate) {
if (mTextAnimator != null) {
mTextAnimator.setTextStyle(weight, textSize, animate, -1, null);
}
}
private void updatePaint() {
Shader shader = new LinearGradient(
getX(), getY(), getX(), getMeasuredHeight() + getY(), mGradientColors, mPositions,
Shader.TileMode.REPEAT);
getPaint().setShader(shader);
if (mTextAnimator != null) {
mTextAnimator.setShader(shader);
}
}
private final OnLayoutChangeListener mOnLayoutChangeListener =
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if (bottom != oldBottom || top != oldTop) {
updatePaint();
}
};
public static final CharSequence FORMAT_12 = "hh\nmm";
public static final CharSequence FORMAT_24 = "HH\nmm";
}

View File

@@ -72,11 +72,6 @@ public class KeyguardClockSwitch extends RelativeLayout {
*/
private TextClock mClockViewBold;
/**
* Gradient clock for usage when mode != KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL.
*/
private TimeBasedColorsClockController mNewLockscreenClockViewController;
/**
* Frame for clock when mode != KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL.
*/
@@ -157,7 +152,6 @@ public class KeyguardClockSwitch extends RelativeLayout {
setPaddingRelative(startEndPadding, 0, startEndPadding, 0);
mSmallClockFrame.setVisibility(GONE);
mNewLockscreenClockFrame.setVisibility(VISIBLE);
mNewLockscreenClockViewController.init();
statusAreaLP.removeRule(RelativeLayout.BELOW);
statusAreaLP.addRule(RelativeLayout.LEFT_OF, R.id.new_lockscreen_clock_view);
@@ -181,8 +175,6 @@ public class KeyguardClockSwitch extends RelativeLayout {
mClockView = findViewById(R.id.default_clock_view);
mClockViewBold = findViewById(R.id.default_clock_view_bold);
mNewLockscreenClockFrame = findViewById(R.id.new_lockscreen_clock_view);
mNewLockscreenClockViewController =
new TimeBasedColorsClockController(findViewById(R.id.gradient_clock_view));
mSmallClockFrame = findViewById(R.id.clock_view);
mKeyguardStatusArea = findViewById(R.id.keyguard_status_area);
}
@@ -305,7 +297,6 @@ public class KeyguardClockSwitch extends RelativeLayout {
if (mClockPlugin != null) {
mClockPlugin.setDarkAmount(darkAmount);
}
mNewLockscreenClockViewController.setDarkAmount(darkAmount);
updateBigClockAlpha();
}
@@ -356,7 +347,6 @@ public class KeyguardClockSwitch extends RelativeLayout {
* Refresh the time of the clock, due to either time tick broadcast or doze time tick alarm.
*/
public void refresh() {
mNewLockscreenClockViewController.refreshTime(System.currentTimeMillis());
mClockView.refreshTime();
mClockViewBold.refreshTime();
if (mClockPlugin != null) {

View File

@@ -55,7 +55,12 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS
private final ClockManager mClockManager;
private final KeyguardSliceViewController mKeyguardSliceViewController;
private final NotificationIconAreaController mNotificationIconAreaController;
private FrameLayout mNewLockscreenClockFrame;
/**
* Gradient clock for usage when mode != KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL.
*/
private AnimatableClockController mNewLockScreenClockViewController;
private FrameLayout mNewLockScreenClockFrame;
private int mLockScreenMode = KeyguardUpdateMonitor.LOCK_SCREEN_MODE_NORMAL;
@@ -119,7 +124,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS
mColorExtractor.addOnColorsChangedListener(mColorsListener);
mView.updateColors(getGradientColors());
updateAodIcons();
mNewLockscreenClockFrame = mView.findViewById(R.id.new_lockscreen_clock_view);
mNewLockScreenClockFrame = mView.findViewById(R.id.new_lockscreen_clock_view);
}
@Override
@@ -182,6 +187,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS
* Refresh clock. Called in response to TIME_TICK broadcasts.
*/
void refresh() {
if (mNewLockScreenClockViewController != null) {
mNewLockScreenClockViewController.refreshTime();
}
mView.refresh();
}
@@ -192,8 +201,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS
*/
void updatePosition(int x, AnimationProperties props, boolean animate) {
x = Math.abs(x);
if (mNewLockscreenClockFrame != null) {
PropertyAnimator.setProperty(mNewLockscreenClockFrame, AnimatableProperty.TRANSLATION_X,
if (mNewLockScreenClockFrame != null) {
PropertyAnimator.setProperty(mNewLockScreenClockFrame, AnimatableProperty.TRANSLATION_X,
-x, props, animate);
}
mKeyguardSliceViewController.updatePosition(x, props, animate);
@@ -205,6 +214,17 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS
*/
void updateLockScreenMode(int mode) {
mLockScreenMode = mode;
if (mode == KeyguardUpdateMonitor.LOCK_SCREEN_MODE_LAYOUT_1) {
if (mNewLockScreenClockViewController == null) {
mNewLockScreenClockViewController =
new AnimatableClockController(
mView.findViewById(R.id.animatable_clock_view),
mStatusBarStateController);
mNewLockScreenClockViewController.init();
}
} else {
mNewLockScreenClockViewController = null;
}
mView.updateLockScreenMode(mLockScreenMode);
updateAodIcons();
}

View File

@@ -21,11 +21,10 @@ import android.animation.AnimatorListenerAdapter
import android.animation.TimeInterpolator
import android.animation.ValueAnimator
import android.graphics.Canvas
import android.graphics.Shader
import android.text.Layout
private const val TAG_WGHT = "wght"
private const val DEFAULT_ANIMATION_DURATION: Long = 1000
private const val DEFAULT_ANIMATION_DURATION: Long = 300
/**
* This class provides text animation between two styles.
@@ -53,9 +52,13 @@ private const val DEFAULT_ANIMATION_DURATION: Long = 1000
* </code>
* </pre>
*/
class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) {
class TextAnimator(
layout: Layout,
private val invalidateCallback: () -> Unit,
private val numLines: Int = 1
) {
// Following two members are for mutable for testing purposes.
internal var textInterpolator: TextInterpolator = TextInterpolator(layout)
internal var textInterpolator: TextInterpolator = TextInterpolator(layout, numLines)
internal var animator: ValueAnimator = ValueAnimator.ofFloat(1f).apply {
duration = DEFAULT_ANIMATION_DURATION
addUpdateListener {
@@ -72,19 +75,6 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) {
textInterpolator.layout = layout
}
var shader: Shader
get() = textInterpolator.basePaint.shader.also {
require(it === textInterpolator.targetPaint.shader) {
"base and target paint has different shader. Usually shader is not interpolatable."
}
}
set(value) {
textInterpolator.basePaint.shader = value
textInterpolator.targetPaint.shader = value
// Shader doesn't change the text layout, so no need to call onTargetPaintModified or
// onBasePaintModified
}
fun draw(c: Canvas) = textInterpolator.draw(c)
/**
@@ -97,6 +87,8 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) {
*
* @param weight an optional text weight.
* @param textSize an optional font size.
* @param colors an optional colors array that must be the same size as numLines passed to
* the TextInterpolator
* @param animate an optional boolean indicating true for showing style transition as animation,
* false for immediate style transition. True by default.
* @param duration an optional animation duration in milliseconds. This is ignored if animate is
@@ -107,6 +99,7 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) {
fun setTextStyle(
weight: Int = -1,
textSize: Float = -1f,
colors: IntArray? = null,
animate: Boolean = true,
duration: Long = -1L,
interpolator: TimeInterpolator? = null
@@ -117,10 +110,21 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) {
}
if (textSize >= 0) {
textInterpolator.targetPaint.textSize = textSize
for (targetPaint in textInterpolator.targetPaint)
targetPaint.textSize = textSize
}
if (weight >= 0) {
textInterpolator.targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight"
for (targetPaint in textInterpolator.targetPaint)
targetPaint.fontVariationSettings = "'$TAG_WGHT' $weight"
}
if (colors != null) {
require(colors.size == textInterpolator.targetPaint.size) {
"colors size (${colors.size}) must be the same size as" +
" targetPaints size (${textInterpolator.targetPaint.size})," +
" which was initialized as numLines ($numLines)"
}
for ((index, targetPaint) in textInterpolator.targetPaint.withIndex())
targetPaint.color = colors[index]
}
textInterpolator.onTargetPaintModified()
@@ -138,4 +142,4 @@ class TextAnimator(layout: Layout, private val invalidateCallback: () -> Unit) {
textInterpolator.rebase()
}
}
}
}

View File

@@ -22,21 +22,28 @@ import android.graphics.text.PositionedGlyphs
import android.graphics.text.TextRunShaper
import android.text.Layout
import android.util.MathUtils
import com.android.internal.graphics.ColorUtils
import java.lang.Math.max
/**
* Provide text style linear interpolation for plain text.
*/
class TextInterpolator(layout: Layout) {
class TextInterpolator(
layout: Layout,
lines: Int = 1
) {
/**
* Returns base paint used for interpolation.
*
* Once you modified the style parameters, you have to call reshapeText to recalculate base text
* layout.
*
* @return a paint object.
* @return an array list of paint objects representing one paint per line of text. If this
* list has a smaller size than the number of lines, all extra lines will use the Paint an
* index 0.
*/
val basePaint = Paint(layout.paint)
val basePaint = createDefaultPaint(layout.paint, lines)
/**
* Returns target paint used for interpolation.
@@ -44,9 +51,18 @@ class TextInterpolator(layout: Layout) {
* Once you modified the style parameters, you have to call reshapeText to recalculate target
* text layout.
*
* @return a paint object
* @return an array list of paint objects representing one paint per line of text. If this
* list has a smaller size than the number of lines, all extra lines will use the Paint an
* index 0.
*/
val targetPaint = Paint(layout.paint)
val targetPaint = createDefaultPaint(layout.paint, lines)
private fun createDefaultPaint(paint: Paint, lines: Int): ArrayList<Paint> {
val paintList = ArrayList<Paint>()
for (i in 0 until lines)
paintList.add(Paint(paint))
return paintList
}
/**
* A class represents a single font run.
@@ -78,7 +94,7 @@ class TextInterpolator(layout: Layout) {
private val fontInterpolator = FontInterpolator()
// Recycling object for glyph drawing. Will be extended for the longest font run if needed.
private val tmpDrawPaint = Paint()
private val tmpDrawPaints = ArrayList<Paint>()
private var tmpPositionArray = FloatArray(20)
/**
@@ -115,8 +131,8 @@ class TextInterpolator(layout: Layout) {
/**
* Recalculate internal text layout for interpolation.
*
* Whenever you modifies target paint, you have to call this method to recalculate internal text
* layout used for interpolation.
* Whenever the target paint is modified, call this method to recalculate internal
* text layout used for interpolation.
*/
fun onTargetPaintModified() {
updatePositionsAndFonts(shapeText(layout, targetPaint), updateBase = false)
@@ -125,8 +141,8 @@ class TextInterpolator(layout: Layout) {
/**
* Recalculate internal text layout for interpolation.
*
* Whenever you modifies base paint, you have to call this method to recalculate internal text
* layout used for interpolation.
* Whenever the base paint is modified, call this method to recalculate internal
* text layout used for interpolation.
*/
fun onBasePaintModified() {
updatePositionsAndFonts(shapeText(layout, basePaint), updateBase = true)
@@ -192,10 +208,10 @@ class TextInterpolator(layout: Layout) {
if (progress == 0f) {
return
} else if (progress == 1f) {
basePaint.set(targetPaint)
updatePaint(basePaint, targetPaint)
} else {
lerp(basePaint, targetPaint, progress, tmpDrawPaint)
basePaint.set(tmpDrawPaint)
lerp(basePaint, targetPaint, progress, tmpDrawPaints)
updatePaint(basePaint, tmpDrawPaints)
}
lines.forEach { line ->
@@ -211,13 +227,21 @@ class TextInterpolator(layout: Layout) {
progress = 0f
}
companion object {
fun updatePaint(toUpdate: ArrayList<Paint>, newValues: ArrayList<Paint>) {
toUpdate.clear()
for (paint in newValues)
toUpdate.add(Paint(paint))
}
}
/**
* Draws interpolated text at the given progress.
*
* @param canvas a canvas.
*/
fun draw(canvas: Canvas) {
lerp(basePaint, targetPaint, progress, tmpDrawPaint)
lerp(basePaint, targetPaint, progress, tmpDrawPaints)
lines.forEachIndexed { lineNo, line ->
canvas.save()
try {
@@ -226,7 +250,10 @@ class TextInterpolator(layout: Layout) {
canvas.translate(origin, layout.getLineBaseline(lineNo).toFloat())
line.fontRuns.forEach { run ->
drawFontRun(canvas, line, run, tmpDrawPaint)
if (lineNo >= tmpDrawPaints.size)
drawFontRun(canvas, line, run, tmpDrawPaints[0])
else
drawFontRun(canvas, line, run, tmpDrawPaints[lineNo])
}
} finally {
canvas.restore()
@@ -384,15 +411,25 @@ class TextInterpolator(layout: Layout) {
}
// Linear interpolate the paint.
private fun lerp(from: Paint, to: Paint, t: Float, out: Paint) {
// Currently only font size is interpolated.
private fun lerp(
from: ArrayList<Paint>,
to: ArrayList<Paint>,
progress: Float,
out: ArrayList<Paint>
) {
out.clear()
// Currently only font size & colors are interpolated.
// TODO(172943390): Add other interpolation or support custom interpolator.
out.set(from)
out.textSize = MathUtils.lerp(from.textSize, to.textSize, t)
for (index in from.indices) {
val paint = Paint(from[index])
paint.textSize = MathUtils.lerp(from[index].textSize, to[index].textSize, progress)
paint.color = ColorUtils.blendARGB(from[index].color, to[index].color, progress)
out.add(paint)
}
}
// Shape the text and stores the result to out argument.
private fun shapeText(layout: Layout, paint: Paint): List<PositionedGlyphs> {
private fun shapeText(layout: Layout, paints: ArrayList<Paint>): List<PositionedGlyphs> {
val out = mutableListOf<PositionedGlyphs>()
for (lineNo in 0 until layout.lineCount) { // Shape all lines.
val lineStart = layout.getLineStart(lineNo)
@@ -403,7 +440,7 @@ class TextInterpolator(layout: Layout) {
lineStart, count, // shape context = shape range.
0f, 0f, // the layout offset. Not changed.
layout.getParagraphDirection(lineNo) == Layout.DIR_RIGHT_TO_LEFT,
paint)) // Use given paint instead of layout's paint for style interpolation.
paints[lineNo])) // Use given paint instead of layout's for style interpolation.
}
return out
}
@@ -414,4 +451,4 @@ private fun Layout.getDrawOrigin(lineNo: Int) =
getLineLeft(lineNo)
} else {
getLineRight(lineNo)
}
}

View File

@@ -1,176 +0,0 @@
/*
* 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.keyguard;
import android.util.MathUtils;
import com.android.internal.graphics.ColorUtils;
import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.util.ViewController;
import java.util.Calendar;
import java.util.GregorianCalendar;
/**
* Changes the color of the text clock based on the time of day.
*/
public class TimeBasedColorsClockController extends ViewController<GradientTextClock> {
private final int[] mGradientColors = new int[3];
private final float[] mPositions = new float[3];
/**
* 0 = fully awake
* between 0 and 1 = transitioning between awake and doze
* 1 = fully in doze
*/
private float mDarkAmount = 0f;
public TimeBasedColorsClockController(GradientTextClock view) {
super(view);
}
@Override
protected void onViewAttached() {
refreshTime(System.currentTimeMillis());
}
@Override
protected void onViewDetached() {
}
/**
* Updates the time for this view. Also updates any color changes.
*/
public void refreshTime(long timeInMillis) {
updateColors(timeInMillis);
updatePositions(timeInMillis);
mView.refreshTime();
}
/**
* Set the amount (ratio) that the device has transitioned to doze.
*
* @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake.
*/
public void setDarkAmount(float darkAmount) {
mDarkAmount = darkAmount;
refreshTime(System.currentTimeMillis());
int weight = (int) MathUtils.lerp(200, 400, 1f - darkAmount);
mView.setTextStyle(weight, -1 /* unchange text size */, true);
}
private int getTimeIndex(long timeInMillis) {
Calendar now = getCalendar(timeInMillis);
int hour = now.get(Calendar.HOUR_OF_DAY); // 0 - 23
if (hour < mTimes[0]) {
return mTimes.length - 1;
}
for (int i = 1; i < mTimes.length; i++) {
if (hour < mTimes[i]) {
return i - 1;
}
}
return mTimes.length - 1;
}
private void updateColors(long timeInMillis) {
final int index = getTimeIndex(timeInMillis);
final int wallpaperTextColor =
Utils.getColorAttrDefaultColor(mView.getContext(), R.attr.wallpaperTextColor);
for (int i = 0; i < mGradientColors.length; i++) {
// wallpaperTextColor on LS when mDarkAmount = 0f
// full color on AOD when mDarkAmount = 1f
mGradientColors[i] =
ColorUtils.blendARGB(wallpaperTextColor, COLORS[index][i], mDarkAmount);
}
mView.setGradientColors(mGradientColors);
}
private void updatePositions(long timeInMillis) {
Calendar now = getCalendar(timeInMillis);
final int index = getTimeIndex(timeInMillis);
final Calendar startTime = new GregorianCalendar();
startTime.setTimeInMillis(now.getTimeInMillis());
startTime.set(Calendar.HOUR_OF_DAY, mTimes[index]);
if (startTime.getTimeInMillis() > now.getTimeInMillis()) {
// start should be earlier than 'now'
startTime.add(Calendar.DATE, -1);
}
final Calendar endTime = new GregorianCalendar();
endTime.setTimeInMillis(now.getTimeInMillis());
if (index == mTimes.length - 1) {
endTime.set(Calendar.HOUR_OF_DAY, mTimes[0]);
endTime.add(Calendar.DATE, 1); // end time is tomorrow
} else {
endTime.set(Calendar.HOUR_OF_DAY, mTimes[index + 1]);
}
long totalTimeInThisColorGradient = endTime.getTimeInMillis() - startTime.getTimeInMillis();
long timeIntoThisColorGradient = now.getTimeInMillis() - startTime.getTimeInMillis();
float percentageWithinGradient =
(float) timeIntoThisColorGradient / (float) totalTimeInThisColorGradient;
for (int i = 0; i < mPositions.length; i++) {
// currently hard-coded .3 movement of gradient
mPositions[i] = POSITIONS[index][i] - (.3f * percentageWithinGradient);
}
mView.setColorPositions(mPositions);
}
private Calendar getCalendar(long timeInMillis) {
Calendar now = new GregorianCalendar();
now.setTimeInMillis(timeInMillis);
return now;
}
private static final int[] SUNRISE = new int[] {0xFF6F75AA, 0xFFAFF0FF, 0xFFFFDEBF};
private static final int[] DAY = new int[] {0xFF9BD8FB, 0xFFD7F5FF, 0xFFFFF278};
private static final int[] NIGHT = new int[] {0xFF333D5E, 0xFFC5A1D6, 0xFF907359};
private static final float[] SUNRISE_START_POSITIONS = new float[] {.3f, .5f, .8f};
private static final float[] DAY_START_POSITIONS = new float[] {.4f, .8f, 1f};
private static final float[] NIGHT_START_POSITIONS = new float[] {.25f, .5f, .8f};
// TODO (b/170228350): use TwilightManager to set sunrise/sunset times
private final int mSunriseTime = 6; // 6am
private final int mDaytime = 9; // 9 am
private final int mNightTime = 19; // 7pm
private int[] mTimes = new int[] {
mSunriseTime,
mDaytime,
mNightTime
};
private static final int[][] COLORS = new int[][] {
SUNRISE,
DAY,
NIGHT
};
private static final float[][] POSITIONS = new float[][] {
SUNRISE_START_POSITIONS,
DAY_START_POSITIONS,
NIGHT_START_POSITIONS
};
}

View File

@@ -35,9 +35,9 @@ import org.mockito.Mockito.verify
import kotlin.math.ceil
private val PAINT = TextPaint().apply {
private val PAINT = arrayListOf(TextPaint().apply {
textSize = 32f
}
})
@RunWith(AndroidTestingRunner::class)
@SmallTest
@@ -50,10 +50,10 @@ class TextAnimatorTest : SysuiTestCase() {
@Test
fun testAnimationStarted() {
val layout = makeLayout("Hello, World", PAINT)
val layout = makeLayout("Hello, World", PAINT[0])
val valueAnimator = mock(ValueAnimator::class.java)
val textInterpolator = mock(TextInterpolator::class.java)
val paint = mock(Paint::class.java)
val paint = arrayListOf(mock(Paint::class.java))
`when`(textInterpolator.targetPaint).thenReturn(paint)
val textAnimator = TextAnimator(layout, {}).apply {
@@ -82,10 +82,10 @@ class TextAnimatorTest : SysuiTestCase() {
@Test
fun testAnimationNotStarted() {
val layout = makeLayout("Hello, World", PAINT)
val layout = makeLayout("Hello, World", PAINT[0])
val valueAnimator = mock(ValueAnimator::class.java)
val textInterpolator = mock(TextInterpolator::class.java)
val paint = mock(Paint::class.java)
val paint = arrayListOf(mock(Paint::class.java))
`when`(textInterpolator.targetPaint).thenReturn(paint)
val textAnimator = TextAnimator(layout, {}).apply {

View File

@@ -18,6 +18,7 @@ package com.android.keyguard
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.testing.AndroidTestingRunner
import android.text.Layout
import android.text.StaticLayout
@@ -37,13 +38,13 @@ private val PAINT = TextPaint().apply {
textSize = 32f
}
private val START_PAINT = TextPaint(PAINT).apply {
private val START_PAINT = arrayListOf<Paint>(TextPaint(PAINT).apply {
fontVariationSettings = "'wght' 400"
}
})
private val END_PAINT = TextPaint(PAINT).apply {
private val END_PAINT = arrayListOf<Paint>(TextPaint(PAINT).apply {
fontVariationSettings = "'wght' 700"
}
})
@RunWith(AndroidTestingRunner::class)
@SmallTest
@@ -59,16 +60,16 @@ class TextInterpolatorTest : SysuiTestCase() {
val layout = makeLayout(TEXT, PAINT)
val interp = TextInterpolator(layout)
interp.basePaint.set(START_PAINT)
TextInterpolator.updatePaint(interp.basePaint, START_PAINT)
interp.onBasePaintModified()
interp.targetPaint.set(END_PAINT)
TextInterpolator.updatePaint(interp.targetPaint, END_PAINT)
interp.onTargetPaintModified()
// Just after created TextInterpolator, it should have 0 progress.
assertThat(interp.progress).isEqualTo(0f)
val actual = interp.toBitmap(BMP_WIDTH, BMP_HEIGHT)
val expected = makeLayout(TEXT, START_PAINT).toBitmap(BMP_WIDTH, BMP_HEIGHT)
val expected = makeLayout(TEXT, START_PAINT[0] as TextPaint).toBitmap(BMP_WIDTH, BMP_HEIGHT)
assertThat(expected.sameAs(actual)).isTrue()
}
@@ -78,15 +79,15 @@ class TextInterpolatorTest : SysuiTestCase() {
val layout = makeLayout(TEXT, PAINT)
val interp = TextInterpolator(layout)
interp.basePaint.set(START_PAINT)
TextInterpolator.updatePaint(interp.basePaint, START_PAINT)
interp.onBasePaintModified()
interp.targetPaint.set(END_PAINT)
TextInterpolator.updatePaint(interp.targetPaint, END_PAINT)
interp.onTargetPaintModified()
interp.progress = 1f
val actual = interp.toBitmap(BMP_WIDTH, BMP_HEIGHT)
val expected = makeLayout(TEXT, END_PAINT).toBitmap(BMP_WIDTH, BMP_HEIGHT)
val expected = makeLayout(TEXT, END_PAINT[0] as TextPaint).toBitmap(BMP_WIDTH, BMP_HEIGHT)
assertThat(expected.sameAs(actual)).isTrue()
}
@@ -96,10 +97,10 @@ class TextInterpolatorTest : SysuiTestCase() {
val layout = makeLayout(TEXT, PAINT)
val interp = TextInterpolator(layout)
interp.basePaint.set(START_PAINT)
TextInterpolator.updatePaint(interp.basePaint, START_PAINT)
interp.onBasePaintModified()
interp.targetPaint.set(END_PAINT)
TextInterpolator.updatePaint(interp.targetPaint, END_PAINT)
interp.onTargetPaintModified()
// We cannot expect exact text layout of the middle position since we don't use text shaping
@@ -107,10 +108,10 @@ class TextInterpolatorTest : SysuiTestCase() {
// end state.
interp.progress = 0.5f
val actual = interp.toBitmap(BMP_WIDTH, BMP_HEIGHT)
assertThat(actual.sameAs(makeLayout(TEXT, START_PAINT).toBitmap(BMP_WIDTH, BMP_HEIGHT)))
.isFalse()
assertThat(actual.sameAs(makeLayout(TEXT, END_PAINT).toBitmap(BMP_WIDTH, BMP_HEIGHT)))
.isFalse()
assertThat(actual.sameAs(makeLayout(TEXT, START_PAINT[0] as TextPaint)
.toBitmap(BMP_WIDTH, BMP_HEIGHT))).isFalse()
assertThat(actual.sameAs(makeLayout(TEXT, END_PAINT[0] as TextPaint)
.toBitmap(BMP_WIDTH, BMP_HEIGHT))).isFalse()
}
@Test
@@ -118,10 +119,10 @@ class TextInterpolatorTest : SysuiTestCase() {
val layout = makeLayout(TEXT, PAINT)
val interp = TextInterpolator(layout)
interp.basePaint.set(START_PAINT)
TextInterpolator.updatePaint(interp.basePaint, START_PAINT)
interp.onBasePaintModified()
interp.targetPaint.set(END_PAINT)
TextInterpolator.updatePaint(interp.targetPaint, END_PAINT)
interp.onTargetPaintModified()
interp.progress = 0.5f