Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 418683f5 authored by Nick Chameyev's avatar Nick Chameyev
Browse files

Fold to AOD animation: part 2, add fold to aod controller

Adds animation that animates AOD content
from left to right when folding a foldable
device.

There are still a couple of issue that should be
addressed in the follow up CLs:
* Navigation bar handle sometimes displayed on AOD
* Showing keyguard when folding from unlocked state
sometimes takes too much time so the screen blocker
times out which leads to flickering

Bug: 202844967
Test: fold from lockscreen, fold from unlocked device,
 aod enabled/disabled, gesture nav enabled/disabled
Test: screen off animation when unlocked
Test: screen off animation when locked
Test: screen off animation when timed out
Test: fingerprint unlock from aod
Change-Id: Ie44f82119ac770ce34faecc60402044d4109dddf
parent 07ae9943
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -168,6 +168,12 @@ public class AnimatableClockController extends ViewController<AnimatableClockVie
        if (!mIsDozing) mView.animateAppearOnLockscreen();
    }

    /** Animate the clock appearance when a foldable device goes from fully-open/half-open state to
     * fully folded state and it goes to sleep (always on display screen) */
    public void animateFoldAppear() {
        mView.animateFoldAppear();
    }

    /**
     * Updates the time for the view.
     */
+0 −315
Original line number Diff line number Diff line
/*
 * 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.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.widget.TextView;

import com.android.systemui.R;

import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;

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 DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm";
    private static final CharSequence DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm";
    private static final long DOZE_ANIM_DURATION = 300;
    private static final long APPEAR_ANIM_DURATION = 350;
    private static final long CHARGE_ANIM_DURATION_PHASE_0 = 500;
    private static final long CHARGE_ANIM_DURATION_PHASE_1 = 1000;

    private final Calendar mTime = Calendar.getInstance();

    private final int mDozingWeight;
    private final int mLockScreenWeight;
    private CharSequence mFormat;
    private CharSequence mDescFormat;
    private int mDozingColor;
    private int mLockScreenColor;
    private float mLineSpacingScale = 1f;
    private int mChargeAnimationDelay = 0;

    private TextAnimator mTextAnimator = null;
    private Runnable mOnTextAnimatorInitialized;

    private boolean mIsSingleLine;

    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);
        TypedArray ta = context.obtainStyledAttributes(
                attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes);
        try {
            mDozingWeight = ta.getInt(R.styleable.AnimatableClockView_dozeWeight, 100);
            mLockScreenWeight = ta.getInt(R.styleable.AnimatableClockView_lockScreenWeight, 300);
            mChargeAnimationDelay = ta.getInt(
                    R.styleable.AnimatableClockView_chargeAnimationDelay, 200);
        } finally {
            ta.recycle();
        }

        ta = context.obtainStyledAttributes(
                attrs, android.R.styleable.TextView, defStyleAttr, defStyleRes);
        try {
            mIsSingleLine = ta.getBoolean(android.R.styleable.TextView_singleLine, false);
        } finally {
            ta.recycle();
        }

        refreshFormat();
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        refreshFormat();
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();
    }

    int getDozingWeight() {
        if (useBoldedVersion()) {
            return mDozingWeight + 100;
        }
        return mDozingWeight;
    }

    int getLockScreenWeight() {
        if (useBoldedVersion()) {
            return mLockScreenWeight + 100;
        }
        return mLockScreenWeight;
    }

    /**
     * Whether to use a bolded version based on the user specified fontWeightAdjustment.
     */
    boolean useBoldedVersion() {
        // "Bold text" fontWeightAdjustment is 300.
        return getResources().getConfiguration().fontWeightAdjustment > 100;
    }

    void refreshTime() {
        mTime.setTimeInMillis(System.currentTimeMillis());
        setText(DateFormat.format(mFormat, mTime));
        setContentDescription(DateFormat.format(mDescFormat, mTime));
    }

    void onTimeZoneChanged(TimeZone timeZone) {
        mTime.setTimeZone(timeZone);
        refreshFormat();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if (mTextAnimator == null) {
            mTextAnimator = new TextAnimator(
                    getLayout(),
                    () -> {
                        invalidate();
                        return Unit.INSTANCE;
                    });
            if (mOnTextAnimatorInitialized != null) {
                mOnTextAnimatorInitialized.run();
                mOnTextAnimatorInitialized = null;
            }
        } else {
            mTextAnimator.updateLayout(getLayout());
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mTextAnimator.draw(canvas);
    }

    void setLineSpacingScale(float scale) {
        mLineSpacingScale = scale;
        setLineSpacing(0, mLineSpacingScale);
    }

    void setColors(int dozingColor, int lockScreenColor) {
        mDozingColor = dozingColor;
        mLockScreenColor = lockScreenColor;
    }

    void animateAppearOnLockscreen() {
        if (mTextAnimator == null) {
            return;
        }

        setTextStyle(
                getDozingWeight(),
                -1 /* text size, no update */,
                mLockScreenColor,
                false /* animate */,
                0 /* duration */,
                0 /* delay */,
                null /* onAnimationEnd */);

        setTextStyle(
                getLockScreenWeight(),
                -1 /* text size, no update */,
                mLockScreenColor,
                true, /* animate */
                APPEAR_ANIM_DURATION,
                0 /* delay */,
                null /* onAnimationEnd */);
    }

    void animateCharge(DozeStateGetter dozeStateGetter) {
        if (mTextAnimator == null || mTextAnimator.isRunning()) {
            // Skip charge animation if dozing animation is already playing.
            return;
        }
        Runnable startAnimPhase2 = () -> setTextStyle(
                dozeStateGetter.isDozing() ? getDozingWeight() : getLockScreenWeight() /* weight */,
                -1,
                null,
                true /* animate */,
                CHARGE_ANIM_DURATION_PHASE_1,
                0 /* delay */,
                null /* onAnimationEnd */);
        setTextStyle(dozeStateGetter.isDozing()
                        ? getLockScreenWeight()
                        : getDozingWeight()/* weight */,
                -1,
                null,
                true /* animate */,
                CHARGE_ANIM_DURATION_PHASE_0,
                mChargeAnimationDelay,
                startAnimPhase2);
    }

    void animateDoze(boolean isDozing, boolean animate) {
        setTextStyle(isDozing ? getDozingWeight() : getLockScreenWeight() /* weight */,
                -1,
                isDozing ? mDozingColor : mLockScreenColor,
                animate,
                DOZE_ANIM_DURATION,
                0 /* delay */,
                null /* onAnimationEnd */);
    }

    /**
     * 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,
            Integer color,
            boolean animate,
            long duration,
            long delay,
            Runnable onAnimationEnd) {
        if (mTextAnimator != null) {
            mTextAnimator.setTextStyle(weight, textSize, color, animate, duration, null,
                    delay, onAnimationEnd);
        } else {
            // when the text animator is set, update its start values
            mOnTextAnimatorInitialized =
                    () -> mTextAnimator.setTextStyle(
                            weight, textSize, color, false, duration, null,
                            delay, onAnimationEnd);
        }
    }

    void refreshFormat() {
        Patterns.update(mContext);

        final boolean use24HourFormat = DateFormat.is24HourFormat(getContext());
        if (mIsSingleLine && use24HourFormat) {
            mFormat = Patterns.sClockView24;
        } else if (!mIsSingleLine && use24HourFormat) {
            mFormat = DOUBLE_LINE_FORMAT_24_HOUR;
        } else if (mIsSingleLine && !use24HourFormat) {
            mFormat = Patterns.sClockView12;
        } else {
            mFormat = DOUBLE_LINE_FORMAT_12_HOUR;
        }

        mDescFormat = use24HourFormat ? Patterns.sClockView24 : Patterns.sClockView12;
        refreshTime();
    }

    // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
    // This is an optimization to ensure we only recompute the patterns when the inputs change.
    private static final class Patterns {
        static String sClockView12;
        static String sClockView24;
        static String sCacheKey;

        static void update(Context context) {
            final Locale locale = Locale.getDefault();
            final Resources res = context.getResources();
            final String clockView12Skel = res.getString(R.string.clock_12hr_format);
            final String clockView24Skel = res.getString(R.string.clock_24hr_format);
            final String key = locale.toString() + clockView12Skel + clockView24Skel;
            if (key.equals(sCacheKey)) return;
            sClockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel);

            // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton
            // format.  The following code removes the AM/PM indicator if we didn't want it.
            if (!clockView12Skel.contains("a")) {
                sClockView12 = sClockView12.replaceAll("a", "").trim();
            }
            sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel);
            sCacheKey = key;
        }
    }

    interface DozeStateGetter {
        boolean isDozing();
    }
}
+370 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.animation.TimeInterpolator
import android.annotation.ColorInt
import android.annotation.FloatRange
import android.annotation.IntRange
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.text.format.DateFormat
import android.util.AttributeSet
import android.widget.TextView
import com.android.systemui.R
import com.android.systemui.animation.Interpolators
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone

/**
 * 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.
 */
class AnimatableClockView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0,
    defStyleRes: Int = 0
) : TextView(context, attrs, defStyleAttr, defStyleRes) {

    private val time = Calendar.getInstance()

    private val dozingWeightInternal: Int
    private val lockScreenWeightInternal: Int
    private val isSingleLineInternal: Boolean

    private var format: CharSequence? = null
    private var descFormat: CharSequence? = null

    @ColorInt
    private var dozingColor = 0

    @ColorInt
    private var lockScreenColor = 0

    private var lineSpacingScale = 1f
    private val chargeAnimationDelay: Int
    private var textAnimator: TextAnimator? = null
    private var onTextAnimatorInitialized: Runnable? = null

    val dozingWeight: Int
        get() = if (useBoldedVersion()) dozingWeightInternal + 100 else dozingWeightInternal

    val lockScreenWeight: Int
        get() = if (useBoldedVersion()) lockScreenWeightInternal + 100 else lockScreenWeightInternal

    init {
        val animatableClockViewAttributes = context.obtainStyledAttributes(
            attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes
        )

        try {
            dozingWeightInternal = animatableClockViewAttributes.getInt(
                R.styleable.AnimatableClockView_dozeWeight,
                100
            )
            lockScreenWeightInternal = animatableClockViewAttributes.getInt(
                R.styleable.AnimatableClockView_lockScreenWeight,
                300
            )
            chargeAnimationDelay = animatableClockViewAttributes.getInt(
                R.styleable.AnimatableClockView_chargeAnimationDelay, 200
            )
        } finally {
            animatableClockViewAttributes.recycle()
        }

        val textViewAttributes = context.obtainStyledAttributes(
            attrs, android.R.styleable.TextView,
            defStyleAttr, defStyleRes
        )

        isSingleLineInternal =
            try {
                textViewAttributes.getBoolean(android.R.styleable.TextView_singleLine, false)
            } finally {
                textViewAttributes.recycle()
            }

        refreshFormat()
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        refreshFormat()
    }

    /**
     * Whether to use a bolded version based on the user specified fontWeightAdjustment.
     */
    fun useBoldedVersion(): Boolean {
        // "Bold text" fontWeightAdjustment is 300.
        return resources.configuration.fontWeightAdjustment > 100
    }

    fun refreshTime() {
        time.timeInMillis = System.currentTimeMillis()
        text = DateFormat.format(format, time)
        contentDescription = DateFormat.format(descFormat, time)
    }

    fun onTimeZoneChanged(timeZone: TimeZone?) {
        time.timeZone = timeZone
        refreshFormat()
    }

    @SuppressLint("DrawAllocation")
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val animator = textAnimator
        if (animator == null) {
            textAnimator = TextAnimator(layout) { invalidate() }
            onTextAnimatorInitialized?.run()
            onTextAnimatorInitialized = null
        } else {
            animator.updateLayout(layout)
        }
    }

    override fun onDraw(canvas: Canvas) {
        textAnimator?.draw(canvas)
    }

    fun setLineSpacingScale(scale: Float) {
        lineSpacingScale = scale
        setLineSpacing(0f, lineSpacingScale)
    }

    fun setColors(@ColorInt dozingColor: Int, lockScreenColor: Int) {
        this.dozingColor = dozingColor
        this.lockScreenColor = lockScreenColor
    }

    fun animateAppearOnLockscreen() {
        if (textAnimator == null) {
            return
        }
        setTextStyle(
            weight = dozingWeight,
            textSize = -1f,
            color = lockScreenColor,
            animate = false,
            duration = 0,
            delay = 0,
            onAnimationEnd = null
        )
        setTextStyle(
            weight = lockScreenWeight,
            textSize = -1f,
            color = lockScreenColor,
            animate = true,
            duration = APPEAR_ANIM_DURATION,
            delay = 0,
            onAnimationEnd = null
        )
    }

    fun animateFoldAppear() {
        if (textAnimator == null) {
            return
        }
        setTextStyle(
            weight = lockScreenWeightInternal,
            textSize = -1f,
            color = lockScreenColor,
            animate = false,
            duration = 0,
            delay = 0,
            onAnimationEnd = null
        )
        setTextStyle(
            weight = dozingWeightInternal,
            textSize = -1f,
            color = dozingColor,
            animate = true,
            interpolator = Interpolators.EMPHASIZED_DECELERATE,
            duration = StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD.toLong(),
            delay = 0,
            onAnimationEnd = null
        )
    }

    fun animateCharge(dozeStateGetter: DozeStateGetter) {
        if (textAnimator == null || textAnimator!!.isRunning()) {
            // Skip charge animation if dozing animation is already playing.
            return
        }
        val startAnimPhase2 = Runnable {
            setTextStyle(
                weight = if (dozeStateGetter.isDozing) dozingWeight else lockScreenWeight,
                textSize = -1f,
                color = null,
                animate = true,
                duration = CHARGE_ANIM_DURATION_PHASE_1,
                delay = 0,
                onAnimationEnd = null
            )
        }
        setTextStyle(
            weight = if (dozeStateGetter.isDozing) lockScreenWeight else dozingWeight,
            textSize = -1f,
            color = null,
            animate = true,
            duration = CHARGE_ANIM_DURATION_PHASE_0,
            delay = chargeAnimationDelay.toLong(),
            onAnimationEnd = startAnimPhase2
        )
    }

    fun animateDoze(isDozing: Boolean, animate: Boolean) {
        setTextStyle(
            weight = if (isDozing) dozingWeight else lockScreenWeight,
            textSize = -1f,
            color = if (isDozing) dozingColor else lockScreenColor,
            animate = animate,
            duration = DOZE_ANIM_DURATION,
            delay = 0,
            onAnimationEnd = null
        )
    }

    /**
     * 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 fun setTextStyle(
        @IntRange(from = 0, to = 1000) weight: Int,
        @FloatRange(from = 0.0) textSize: Float,
        color: Int?,
        animate: Boolean,
        interpolator: TimeInterpolator?,
        duration: Long,
        delay: Long,
        onAnimationEnd: Runnable?
    ) {
        if (textAnimator != null) {
            textAnimator?.setTextStyle(
                weight = weight,
                textSize = textSize,
                color = color,
                animate = animate,
                duration = duration,
                interpolator = interpolator,
                delay = delay,
                onAnimationEnd = onAnimationEnd
            )
        } else {
            // when the text animator is set, update its start values
            onTextAnimatorInitialized = Runnable {
                textAnimator?.setTextStyle(
                    weight = weight,
                    textSize = textSize,
                    color = color,
                    animate = false,
                    duration = duration,
                    interpolator = interpolator,
                    delay = delay,
                    onAnimationEnd = onAnimationEnd
                )
            }
        }
    }

    private fun setTextStyle(
        @IntRange(from = 0, to = 1000) weight: Int,
        @FloatRange(from = 0.0) textSize: Float,
        color: Int?,
        animate: Boolean,
        duration: Long,
        delay: Long,
        onAnimationEnd: Runnable?
    ) {
        setTextStyle(
            weight = weight,
            textSize = textSize,
            color = color,
            animate = animate,
            interpolator = null,
            duration = duration,
            delay = delay,
            onAnimationEnd = onAnimationEnd
        )
    }

    fun refreshFormat() {
        Patterns.update(context)
        val use24HourFormat = DateFormat.is24HourFormat(context)

        format = when {
            isSingleLineInternal && use24HourFormat -> Patterns.sClockView24
            !isSingleLineInternal && use24HourFormat -> DOUBLE_LINE_FORMAT_24_HOUR
            isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
            else -> DOUBLE_LINE_FORMAT_12_HOUR
        }

        descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12

        refreshTime()
    }

    // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
    // This is an optimization to ensure we only recompute the patterns when the inputs change.
    private object Patterns {
        var sClockView12: String? = null
        var sClockView24: String? = null
        var sCacheKey: String? = null

        fun update(context: Context) {
            val locale = Locale.getDefault()
            val res = context.resources
            val clockView12Skel = res.getString(R.string.clock_12hr_format)
            val clockView24Skel = res.getString(R.string.clock_24hr_format)
            val key = locale.toString() + clockView12Skel + clockView24Skel
            if (key == sCacheKey) return

            val clockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel)
            sClockView12 = clockView12

            // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton
            // format.  The following code removes the AM/PM indicator if we didn't want it.
            if (!clockView12Skel.contains("a")) {
                sClockView12 = clockView12.replace("a".toRegex(), "").trim { it <= ' ' }
            }
            sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel)
            sCacheKey = key
        }
    }

    interface DozeStateGetter {
        val isDozing: Boolean
    }
}

private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm"
private const val DOZE_ANIM_DURATION: Long = 300
private const val APPEAR_ANIM_DURATION: Long = 350
private const val CHARGE_ANIM_DURATION_PHASE_0: Long = 500
private const val CHARGE_ANIM_DURATION_PHASE_1: Long = 1000
+16 −4

File changed.

Preview size limit exceeded, changes collapsed.

+11 −4
Original line number Diff line number Diff line
@@ -292,17 +292,24 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS
     * Set which clock should be displayed on the keyguard. The other one will be automatically
     * hidden.
     */
    public void displayClock(@KeyguardClockSwitch.ClockSize int clockSize) {
    public void displayClock(@KeyguardClockSwitch.ClockSize int clockSize, boolean animate) {
        if (!mCanShowDoubleLineClock && clockSize == KeyguardClockSwitch.LARGE) {
            return;
        }

        boolean appeared = mView.switchToClock(clockSize);
        if (appeared && clockSize == LARGE) {
        boolean appeared = mView.switchToClock(clockSize, animate);
        if (animate && appeared && clockSize == LARGE) {
            mLargeClockViewController.animateAppear();
        }
    }

    public void animateFoldToAod() {
        if (mClockViewController != null) {
            mClockViewController.animateFoldAppear();
            mLargeClockViewController.animateFoldAppear();
        }
    }

    /**
     * If we're presenting a custom clock of just the default one.
     */
@@ -445,7 +452,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS
            Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, 1) != 0;

        if (!mCanShowDoubleLineClock) {
            mUiExecutor.execute(() -> displayClock(KeyguardClockSwitch.SMALL));
            mUiExecutor.execute(() -> displayClock(KeyguardClockSwitch.SMALL, /* animate */ true));
        }
    }
}
Loading