blob: a5a545af641a4467557dc32e511b489b127e293a [file] [log] [blame]
package com.android.keyguard;
import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN;
import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN;
import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.res.ResourcesCompat;
import com.android.app.animation.Interpolators;
import com.android.keyguard.dagger.KeyguardStatusViewScope;
import com.android.systemui.log.LogBuffer;
import com.android.systemui.log.core.LogLevel;
import com.android.systemui.plugins.clocks.ClockController;
import com.android.systemui.res.R;
import com.android.systemui.shared.clocks.DefaultClockController;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Switch to show plugin clock when plugin is connected, otherwise it will show default clock.
*/
@KeyguardStatusViewScope
public class KeyguardClockSwitch extends RelativeLayout {
private static final String TAG = "KeyguardClockSwitch";
public static final String MISSING_CLOCK_ID = "CLOCK_MISSING";
private static final long CLOCK_OUT_MILLIS = 133;
private static final long CLOCK_IN_MILLIS = 167;
public static final long CLOCK_IN_START_DELAY_MILLIS = 133;
private static final long STATUS_AREA_START_DELAY_MILLIS = 0;
private static final long STATUS_AREA_MOVE_UP_MILLIS = 967;
private static final long STATUS_AREA_MOVE_DOWN_MILLIS = 467;
private static final float SMARTSPACE_TRANSLATION_CENTER_MULTIPLIER = 1.4f;
private static final float SMARTSPACE_TOP_PADDING_MULTIPLIER = 2.625f;
@IntDef({LARGE, SMALL})
@Retention(RetentionPolicy.SOURCE)
public @interface ClockSize { }
public static final int LARGE = 0;
public static final int SMALL = 1;
// compensate for translation of parents subject to device screen
// In this case, the translation comes from KeyguardStatusView
public int screenOffsetYPadding = 0;
/** Returns a region for the large clock to position itself, based on the given parent. */
public static Rect getLargeClockRegion(ViewGroup parent) {
int largeClockTopMargin = parent.getResources()
.getDimensionPixelSize(
com.android.systemui.customization.R.dimen.keyguard_large_clock_top_margin);
int targetHeight = parent.getResources()
.getDimensionPixelSize(
com.android.systemui.customization.R.dimen.large_clock_text_size)
* 2;
int top = parent.getHeight() / 2 - targetHeight / 2
+ largeClockTopMargin / 2;
return new Rect(
parent.getLeft(),
top,
parent.getRight(),
top + targetHeight);
}
/** Returns a region for the small clock to position itself, based on the given parent. */
public static Rect getSmallClockRegion(ViewGroup parent) {
int targetHeight = parent.getResources()
.getDimensionPixelSize(
com.android.systemui.customization.R.dimen.small_clock_text_size);
return new Rect(
parent.getLeft(),
parent.getTop(),
parent.getRight(),
parent.getTop() + targetHeight);
}
/**
* Frame for small/large clocks
*/
private KeyguardClockFrame mSmallClockFrame;
private KeyguardClockFrame mLargeClockFrame;
private ClockController mClock;
// It's bc_smartspace_view, assigned by KeyguardClockSwitchController
// to get the top padding for translating smartspace for weather clock
private View mSmartspace;
// Smartspace in weather clock is translated by this value
// to compensate for the position invisible dateWeatherView
private int mSmartspaceTop = -1;
private KeyguardStatusAreaView mStatusArea;
private int mSmartspaceTopOffset;
private float mWeatherClockSmartspaceScaling = 1f;
private int mWeatherClockSmartspaceTranslateX = 0;
private int mWeatherClockSmartspaceTranslateY = 0;
private int mDrawAlpha = 255;
private int mStatusBarHeight = 0;
/**
* Maintain state so that a newly connected plugin can be initialized.
*/
private float mDarkAmount;
private boolean mSplitShadeCentered = false;
/**
* Indicates which clock is currently displayed - should be one of {@link ClockSize}.
* Use null to signify it is uninitialized.
*/
@ClockSize private Integer mDisplayedClockSize = null;
@VisibleForTesting AnimatorSet mClockInAnim = null;
@VisibleForTesting AnimatorSet mClockOutAnim = null;
@VisibleForTesting AnimatorSet mStatusAreaAnim = null;
private int mClockSwitchYAmount;
@VisibleForTesting boolean mChildrenAreLaidOut = false;
@VisibleForTesting boolean mAnimateOnLayout = true;
private LogBuffer mLogBuffer = null;
public KeyguardClockSwitch(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Apply dp changes on configuration change
*/
public void onConfigChanged() {
mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize(
R.dimen.keyguard_clock_switch_y_shift);
mSmartspaceTopOffset = (int) (mContext.getResources().getDimensionPixelSize(
R.dimen.keyguard_smartspace_top_offset)
* mContext.getResources().getConfiguration().fontScale
/ mContext.getResources().getDisplayMetrics().density
* SMARTSPACE_TOP_PADDING_MULTIPLIER);
mWeatherClockSmartspaceScaling = ResourcesCompat.getFloat(
mContext.getResources(), R.dimen.weather_clock_smartspace_scale);
mWeatherClockSmartspaceTranslateX = mContext.getResources().getDimensionPixelSize(
R.dimen.weather_clock_smartspace_translateX);
mWeatherClockSmartspaceTranslateY = mContext.getResources().getDimensionPixelSize(
R.dimen.weather_clock_smartspace_translateY);
mStatusBarHeight = mContext.getResources().getDimensionPixelSize(
R.dimen.status_bar_height);
updateStatusArea(/* animate= */false);
}
/** Get bc_smartspace_view from KeyguardClockSwitchController
* Use its top to decide the translation value */
public void setSmartspace(View smartspace) {
mSmartspace = smartspace;
}
/** Sets whether the large clock is being shown on a connected display. */
public void setLargeClockOnSecondaryDisplay(boolean onSecondaryDisplay) {
if (mClock != null) {
mClock.getLargeClock().getEvents().onSecondaryDisplayChanged(onSecondaryDisplay);
}
}
/**
* Enable or disable split shade specific positioning
*/
public void setSplitShadeCentered(boolean splitShadeCentered) {
if (mSplitShadeCentered != splitShadeCentered) {
mSplitShadeCentered = splitShadeCentered;
updateStatusArea(/* animate= */true);
}
}
public boolean getSplitShadeCentered() {
return mSplitShadeCentered;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mSmallClockFrame = findViewById(R.id.lockscreen_clock_view);
mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
mStatusArea = findViewById(R.id.keyguard_status_area);
onConfigChanged();
}
@Override
protected boolean onSetAlpha(int alpha) {
mDrawAlpha = alpha;
return true;
}
@Override
protected void dispatchDraw(Canvas canvas) {
KeyguardClockFrame.saveCanvasAlpha(
this, canvas, mDrawAlpha,
c -> {
super.dispatchDraw(c);
return kotlin.Unit.INSTANCE;
});
}
public void setLogBuffer(LogBuffer logBuffer) {
mLogBuffer = logBuffer;
}
public LogBuffer getLogBuffer() {
return mLogBuffer;
}
/** Returns the id of the currently rendering clock */
public String getClockId() {
if (mClock == null) {
return MISSING_CLOCK_ID;
}
return mClock.getConfig().getId();
}
void setClock(ClockController clock, int statusBarState) {
mClock = clock;
// Disconnect from existing plugin.
mSmallClockFrame.removeAllViews();
mLargeClockFrame.removeAllViews();
if (clock == null) {
if (mLogBuffer != null) {
mLogBuffer.log(TAG, LogLevel.ERROR, "No clock being shown");
}
return;
}
// Attach small and big clock views to hierarchy.
if (mLogBuffer != null) {
mLogBuffer.log(TAG, LogLevel.INFO, "Attached new clock views to switch");
}
mSmallClockFrame.addView(clock.getSmallClock().getView());
mLargeClockFrame.addView(clock.getLargeClock().getView());
updateClockTargetRegions();
updateStatusArea(/* animate= */false);
}
private void updateStatusArea(boolean animate) {
if (mDisplayedClockSize != null && mChildrenAreLaidOut) {
updateClockViews(mDisplayedClockSize == LARGE, animate);
}
}
void updateClockTargetRegions() {
if (mClock != null) {
if (mSmallClockFrame.isLaidOut()) {
Rect targetRegion = getSmallClockRegion(mSmallClockFrame);
mClock.getSmallClock().getEvents().onTargetRegionChanged(targetRegion);
}
if (mLargeClockFrame.isLaidOut()) {
Rect targetRegion = getLargeClockRegion(mLargeClockFrame);
if (mClock instanceof DefaultClockController) {
mClock.getLargeClock().getEvents().onTargetRegionChanged(
targetRegion);
} else {
mClock.getLargeClock().getEvents().onTargetRegionChanged(
new Rect(
targetRegion.left,
targetRegion.top - screenOffsetYPadding,
targetRegion.right,
targetRegion.bottom - screenOffsetYPadding));
}
}
}
}
private void updateClockViews(boolean useLargeClock, boolean animate) {
if (mLogBuffer != null) {
mLogBuffer.log(TAG, LogLevel.DEBUG, (msg) -> {
msg.setBool1(useLargeClock);
msg.setBool2(animate);
msg.setBool3(mChildrenAreLaidOut);
return kotlin.Unit.INSTANCE;
}, (msg) -> "updateClockViews"
+ "; useLargeClock=" + msg.getBool1()
+ "; animate=" + msg.getBool2()
+ "; mChildrenAreLaidOut=" + msg.getBool3());
}
if (mClockInAnim != null) mClockInAnim.cancel();
if (mClockOutAnim != null) mClockOutAnim.cancel();
if (mStatusAreaAnim != null) mStatusAreaAnim.cancel();
mClockInAnim = null;
mClockOutAnim = null;
mStatusAreaAnim = null;
View in, out;
// statusAreaYTranslation uses for the translation for both mStatusArea and mSmallClockFrame
// statusAreaClockTranslateY only uses for mStatusArea
float statusAreaYTranslation, statusAreaClockScale = 1f;
float statusAreaClockTranslateX = 0f, statusAreaClockTranslateY = 0f;
float clockInYTranslation, clockOutYTranslation;
if (useLargeClock) {
out = mSmallClockFrame;
in = mLargeClockFrame;
if (indexOfChild(in) == -1) addView(in, 0);
statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop()
+ mSmartspaceTopOffset;
// TODO: Load from clock config when less risky
if (mClock != null
&& mClock.getLargeClock().getConfig().getHasCustomWeatherDataDisplay()) {
statusAreaClockScale = mWeatherClockSmartspaceScaling;
statusAreaClockTranslateX = mWeatherClockSmartspaceTranslateX;
if (mSplitShadeCentered) {
statusAreaClockTranslateX *= SMARTSPACE_TRANSLATION_CENTER_MULTIPLIER;
}
// On large weather clock,
// top padding for time is status bar height from top of the screen.
// On small one,
// it's screenOffsetYPadding (translationY for KeyguardStatusView),
// Cause smartspace is positioned according to the smallClockFrame
// we need to translate the difference between bottom of large clock and small clock
// Also, we need to counter offset the empty date weather view, mSmartspaceTop
// mWeatherClockSmartspaceTranslateY is only for Felix
statusAreaClockTranslateY = mStatusBarHeight - 0.6F * mSmallClockFrame.getHeight()
- mSmartspaceTop - screenOffsetYPadding
- statusAreaYTranslation + mWeatherClockSmartspaceTranslateY;
}
clockInYTranslation = 0;
clockOutYTranslation = 0; // Small clock translation is handled with statusArea
} else {
in = mSmallClockFrame;
out = mLargeClockFrame;
statusAreaYTranslation = 0f;
clockInYTranslation = 0f;
clockOutYTranslation = mClockSwitchYAmount * -1f;
// Must remove in order for notifications to appear in the proper place, ideally this
// would happen after the out animation runs, but we can't guarantee that the
// nofications won't enter only after the out animation runs.
removeView(out);
}
if (!animate) {
out.setAlpha(0f);
out.setTranslationY(clockOutYTranslation);
out.setVisibility(INVISIBLE);
in.setAlpha(1f);
in.setTranslationY(clockInYTranslation);
in.setVisibility(VISIBLE);
mStatusArea.setScaleX(statusAreaClockScale);
mStatusArea.setScaleY(statusAreaClockScale);
mStatusArea.setTranslateXFromClockDesign(statusAreaClockTranslateX);
mStatusArea.setTranslateYFromClockDesign(statusAreaClockTranslateY);
mStatusArea.setTranslateYFromClockSize(statusAreaYTranslation);
mSmallClockFrame.setTranslationY(statusAreaYTranslation);
return;
}
mClockOutAnim = new AnimatorSet();
mClockOutAnim.setDuration(CLOCK_OUT_MILLIS);
mClockOutAnim.setInterpolator(Interpolators.LINEAR);
mClockOutAnim.playTogether(
ObjectAnimator.ofFloat(out, ALPHA, 0f),
ObjectAnimator.ofFloat(out, TRANSLATION_Y, clockOutYTranslation));
mClockOutAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
if (mClockOutAnim == animation) {
out.setVisibility(INVISIBLE);
mClockOutAnim = null;
}
}
});
in.setVisibility(View.VISIBLE);
mClockInAnim = new AnimatorSet();
mClockInAnim.setDuration(CLOCK_IN_MILLIS);
mClockInAnim.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
mClockInAnim.playTogether(
ObjectAnimator.ofFloat(in, ALPHA, 1f),
ObjectAnimator.ofFloat(in, TRANSLATION_Y, clockInYTranslation));
mClockInAnim.setStartDelay(CLOCK_IN_START_DELAY_MILLIS);
mClockInAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
if (mClockInAnim == animation) {
mClockInAnim = null;
}
}
});
mStatusAreaAnim = new AnimatorSet();
mStatusAreaAnim.setStartDelay(STATUS_AREA_START_DELAY_MILLIS);
mStatusAreaAnim.setDuration(
useLargeClock ? STATUS_AREA_MOVE_UP_MILLIS : STATUS_AREA_MOVE_DOWN_MILLIS);
mStatusAreaAnim.setInterpolator(Interpolators.EMPHASIZED);
mStatusAreaAnim.playTogether(
ObjectAnimator.ofFloat(mStatusArea, TRANSLATE_Y_CLOCK_SIZE.getProperty(),
statusAreaYTranslation),
ObjectAnimator.ofFloat(mSmallClockFrame, TRANSLATION_Y, statusAreaYTranslation),
ObjectAnimator.ofFloat(mStatusArea, SCALE_X, statusAreaClockScale),
ObjectAnimator.ofFloat(mStatusArea, SCALE_Y, statusAreaClockScale),
ObjectAnimator.ofFloat(mStatusArea, TRANSLATE_X_CLOCK_DESIGN.getProperty(),
statusAreaClockTranslateX),
ObjectAnimator.ofFloat(mStatusArea, TRANSLATE_Y_CLOCK_DESIGN.getProperty(),
statusAreaClockTranslateY));
mStatusAreaAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
if (mStatusAreaAnim == animation) {
mStatusAreaAnim = null;
}
}
});
mClockInAnim.start();
mClockOutAnim.start();
mStatusAreaAnim.start();
}
/**
* Display the desired clock and hide the other one
*
* @return true if desired clock appeared and false if it was already visible
*/
boolean switchToClock(@ClockSize int clockSize, boolean animate) {
if (mDisplayedClockSize != null && clockSize == mDisplayedClockSize) {
return false;
}
// let's make sure clock is changed only after all views were laid out so we can
// translate them properly
if (mChildrenAreLaidOut) {
updateClockViews(clockSize == LARGE, animate);
}
mDisplayedClockSize = clockSize;
return true;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
// TODO: b/305022530
if (mClock != null && mClock.getConfig().getId().equals("DIGITAL_CLOCK_METRO")) {
mClock.getEvents().onColorPaletteChanged(mContext.getResources());
}
if (changed) {
post(() -> updateClockTargetRegions());
}
if (mSmartspace != null && mSmartspaceTop != mSmartspace.getTop()
&& mDisplayedClockSize != null) {
mSmartspaceTop = mSmartspace.getTop();
post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout));
}
if (mDisplayedClockSize != null && !mChildrenAreLaidOut) {
post(() -> updateClockViews(mDisplayedClockSize == LARGE, mAnimateOnLayout));
}
mChildrenAreLaidOut = true;
}
public void dump(PrintWriter pw, String[] args) {
pw.println("KeyguardClockSwitch:");
pw.println(" mSmallClockFrame = " + mSmallClockFrame);
pw.println(" mSmallClockFrame.alpha = " + mSmallClockFrame.getAlpha());
pw.println(" mLargeClockFrame = " + mLargeClockFrame);
pw.println(" mLargeClockFrame.alpha = " + mLargeClockFrame.getAlpha());
pw.println(" mStatusArea = " + mStatusArea);
pw.println(" mDisplayedClockSize = " + mDisplayedClockSize);
}
}