Files
lawnchair/src/com/android/launcher3/allapps/WorkUtilityView.java
Brandon Dayauon fcb892328b Add unit testing to workkUtilityView
Testing inflate and visibility states (with both flag states)

bug: 361589193
Test: manually screenshot: https://screenshot.googleplex.com/45HDsyfQfNbgjiE
Flag: com.android.launcher3.work_scheduler_in_work_profile
Change-Id: I57cff03fe3b362e304f52984a2699f8d5b6e653e
2024-12-02 10:32:56 -08:00

408 lines
16 KiB
Java

/*
* 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.launcher3.allapps;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.graphics.Insets;
import androidx.core.view.WindowInsetsCompat;
import com.android.app.animation.Interpolators;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedPropertySetter;
import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
import com.android.launcher3.model.StringCache;
import com.android.launcher3.views.ActivityContext;
import java.util.ArrayList;
/**
* Work profile utility ViewGroup that is shown at the bottom of AllApps work tab
*/
public class WorkUtilityView extends LinearLayout implements Insettable,
KeyboardInsetAnimationCallback.KeyboardInsetListener {
private static final int TEXT_EXPAND_OPACITY_DURATION = 300;
private static final int TEXT_COLLAPSE_OPACITY_DURATION = 50;
private static final int EXPAND_COLLAPSE_DURATION = 300;
private static final int TEXT_ALPHA_EXPAND_DELAY = 80;
private static final int TEXT_ALPHA_COLLAPSE_DELAY = 0;
private static final int WORK_SCHEDULER_OPACITY_DURATION =
(int) (EXPAND_COLLAPSE_DURATION * 0.75f);
private static final int FLAG_FADE_ONGOING = 1 << 1;
private static final int FLAG_TRANSLATION_ONGOING = 1 << 2;
private static final int FLAG_IS_EXPAND = 1 << 3;
private static final int SCROLL_THRESHOLD_DP = 10;
private static final float WORK_SCHEDULER_SCALE_MIN = 0.25f;
private static final float WORK_SCHEDULER_SCALE_MAX = 1f;
private final Rect mInsets = new Rect();
private final Rect mImeInsets = new Rect();
private int mFlags;
private final ActivityContext mActivityContext;
private final Context mContext;
private final int mTextMarginStart;
private final int mTextMarginEnd;
private final int mIconMarginStart;
private final String mWorkSchedulerIntentAction;
// Threshold when user scrolls up/down to determine when should button extend/collapse
private final int mScrollThreshold;
private ValueAnimator mPauseFABAnim;
private TextView mPauseText;
private ImageView mWorkIcon;
private ImageButton mSchedulerButton;
public WorkUtilityView(@NonNull Context context) {
this(context, null, 0);
}
public WorkUtilityView(@NonNull Context context, @NonNull AttributeSet attrs) {
this(context, attrs, 0);
}
public WorkUtilityView(@NonNull Context context, @NonNull AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
mScrollThreshold = Utilities.dpToPx(SCROLL_THRESHOLD_DP);
mActivityContext = ActivityContext.lookupContext(getContext());
mTextMarginStart = mContext.getResources().getDimensionPixelSize(
R.dimen.work_fab_text_start_margin);
mTextMarginEnd = mContext.getResources().getDimensionPixelSize(
R.dimen.work_fab_text_end_margin);
mIconMarginStart = mContext.getResources().getDimensionPixelSize(
R.dimen.work_fab_icon_start_margin_expanded);
mWorkSchedulerIntentAction = mContext.getResources().getString(
R.string.work_profile_scheduler_intent);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mPauseText = findViewById(R.id.pause_text);
mWorkIcon = findViewById(R.id.work_icon);
mSchedulerButton = findViewById(R.id.work_scheduler);
setSelected(true);
KeyboardInsetAnimationCallback keyboardInsetAnimationCallback =
new KeyboardInsetAnimationCallback(this);
setWindowInsetsAnimationCallback(keyboardInsetAnimationCallback);
// Expand is the default state upon initialization.
addFlag(FLAG_IS_EXPAND);
setInsets(mActivityContext.getDeviceProfile().getInsets());
updateStringFromCache();
mSchedulerButton.setVisibility(GONE);
mSchedulerButton.setOnClickListener(null);
if (shouldUseScheduler()) {
mSchedulerButton.setVisibility(VISIBLE);
mSchedulerButton.setOnClickListener(view ->
mContext.startActivity(new Intent(mWorkSchedulerIntentAction)));
}
}
@Override
public void setInsets(Rect insets) {
mInsets.set(insets);
updateTranslationY();
MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
if (lp != null) {
int bottomMargin = getResources().getDimensionPixelSize(R.dimen.work_fab_margin_bottom);
DeviceProfile dp = ActivityContext.lookupContext(getContext()).getDeviceProfile();
if (mActivityContext.getAppsView().isSearchBarFloating()) {
bottomMargin += dp.hotseatQsbHeight;
}
if (!dp.isGestureMode && dp.isTaskbarPresent) {
bottomMargin += dp.taskbarHeight;
}
lp.bottomMargin = bottomMargin;
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
boolean isRtl = Utilities.isRtl(getResources());
int shift = mActivityContext.getDeviceProfile().getAllAppsIconStartMargin(mContext);
setTranslationX(isRtl ? shift : -shift);
}
@Override
public boolean isEnabled() {
return super.isEnabled() && getVisibility() == VISIBLE;
}
public void animateVisibility(boolean visible) {
clearAnimation();
if (visible) {
addFlag(FLAG_FADE_ONGOING);
setVisibility(VISIBLE);
extend();
animate().alpha(1).withEndAction(() -> removeFlag(FLAG_FADE_ONGOING)).start();
} else if (getVisibility() != GONE) {
addFlag(FLAG_FADE_ONGOING);
animate().alpha(0).withEndAction(() -> {
removeFlag(FLAG_FADE_ONGOING);
setVisibility(GONE);
}).start();
}
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
WindowInsetsCompat windowInsetsCompat =
WindowInsetsCompat.toWindowInsetsCompat(insets, this);
if (windowInsetsCompat.isVisible(WindowInsetsCompat.Type.ime())) {
setInsets(mImeInsets, windowInsetsCompat.getInsets(WindowInsetsCompat.Type.ime()));
shrink();
} else {
mImeInsets.setEmpty();
extend();
}
updateTranslationY();
return super.onApplyWindowInsets(insets);
}
void updateTranslationY() {
setTranslationY(-mImeInsets.bottom);
}
@Override
public void setTranslationY(float translationY) {
// Always translate at least enough for nav bar insets.
super.setTranslationY(Math.min(translationY, -mInsets.bottom));
}
private ValueAnimator animateSchedulerScale(boolean isExpanding) {
float scaleFrom = isExpanding ? WORK_SCHEDULER_SCALE_MIN : WORK_SCHEDULER_SCALE_MAX;
float scaleTo = isExpanding ? WORK_SCHEDULER_SCALE_MAX : WORK_SCHEDULER_SCALE_MIN;
ValueAnimator schedulerScaleAnim = ObjectAnimator.ofFloat(scaleFrom, scaleTo);
schedulerScaleAnim.setDuration(EXPAND_COLLAPSE_DURATION);
schedulerScaleAnim.setInterpolator(Interpolators.STANDARD);
schedulerScaleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float scale = (float) valueAnimator.getAnimatedValue();
mSchedulerButton.setScaleX(scale);
mSchedulerButton.setScaleY(scale);
}
});
schedulerScaleAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
if (isExpanding) {
mSchedulerButton.setVisibility(VISIBLE);
}
}
@Override
public void onAnimationEnd(Animator animation) {
if (!isExpanding) {
mSchedulerButton.setVisibility(GONE);
}
}
});
return schedulerScaleAnim;
}
private ValueAnimator animateSchedulerAlpha(boolean isExpanding) {
float alphaFrom = isExpanding ? 0 : 1;
float alphaTo = isExpanding ? 1 : 0;
ValueAnimator schedulerAlphaAnim = ObjectAnimator.ofFloat(alphaFrom, alphaTo);
schedulerAlphaAnim.setDuration(WORK_SCHEDULER_OPACITY_DURATION);
schedulerAlphaAnim.setStartDelay(isExpanding ? 0 :
EXPAND_COLLAPSE_DURATION - WORK_SCHEDULER_OPACITY_DURATION);
schedulerAlphaAnim.setInterpolator(Interpolators.STANDARD);
schedulerAlphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mSchedulerButton.setAlpha((float) valueAnimator.getAnimatedValue());
}
});
return schedulerAlphaAnim;
}
private void animateWorkUtilityViews(boolean isExpanding) {
if (!shouldAnimate(isExpanding)) {
return;
}
AnimatorSet animatorSet = new AnimatedPropertySetter().buildAnim();
mPauseText.measure(0,0);
int currentWidth = mPauseText.getWidth();
int fullWidth = mPauseText.getMeasuredWidth();
float from = isExpanding ? 0 : currentWidth;
float to = isExpanding ? fullWidth : 0;
mPauseFABAnim = ObjectAnimator.ofFloat(from, to);
mPauseFABAnim.setDuration(EXPAND_COLLAPSE_DURATION);
mPauseFABAnim.setInterpolator(Interpolators.STANDARD);
mPauseFABAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float translation = (float) valueAnimator.getAnimatedValue();
float translationFraction = translation / fullWidth;
ViewGroup.MarginLayoutParams textViewLayoutParams =
(ViewGroup.MarginLayoutParams) mPauseText.getLayoutParams();
textViewLayoutParams.width = (int) translation;
textViewLayoutParams.setMarginStart((int) (mTextMarginStart * translationFraction));
textViewLayoutParams.setMarginEnd((int) (mTextMarginEnd * translationFraction));
mPauseText.setLayoutParams(textViewLayoutParams);
ViewGroup.MarginLayoutParams iconLayoutParams =
(ViewGroup.MarginLayoutParams) mWorkIcon.getLayoutParams();
iconLayoutParams.setMarginStart((int) (mIconMarginStart * translationFraction));
mWorkIcon.setLayoutParams(iconLayoutParams);
}
});
mPauseFABAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
if (isExpanding) {
addFlag(FLAG_IS_EXPAND);
} else {
mPauseText.setVisibility(GONE);
removeFlag(FLAG_IS_EXPAND);
}
mPauseText.setHorizontallyScrolling(false);
mPauseText.setEllipsize(TextUtils.TruncateAt.END);
}
@Override
public void onAnimationStart(Animator animator) {
mPauseText.setHorizontallyScrolling(true);
mPauseText.setVisibility(VISIBLE);
mPauseText.setEllipsize(null);
}
});
ArrayList<Animator> animatorList = new ArrayList<>();
animatorList.add(mPauseFABAnim);
animatorList.add(updatePauseTextAlpha(isExpanding));
if (shouldUseScheduler()) {
animatorList.add(animateSchedulerScale(isExpanding));
animatorList.add(animateSchedulerAlpha(isExpanding));
}
animatorSet.playTogether(animatorList);
animatorSet.start();
}
private ValueAnimator updatePauseTextAlpha(boolean expand) {
float from = expand ? 0 : 1;
float to = expand ? 1 : 0;
ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
alphaAnim.setDuration(expand ? TEXT_EXPAND_OPACITY_DURATION
: TEXT_COLLAPSE_OPACITY_DURATION);
alphaAnim.setStartDelay(expand ? TEXT_ALPHA_EXPAND_DELAY : TEXT_ALPHA_COLLAPSE_DELAY);
alphaAnim.setInterpolator(Interpolators.LINEAR);
alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mPauseText.setAlpha((float) valueAnimator.getAnimatedValue());
}
});
return alphaAnim;
}
private void setInsets(Rect rect, Insets insets) {
rect.set(insets.left, insets.top, insets.right, insets.bottom);
}
public Rect getImeInsets() {
return mImeInsets;
}
@Override
public void onTranslationStart() {
addFlag(FLAG_TRANSLATION_ONGOING);
}
@Override
public void onTranslationEnd() {
removeFlag(FLAG_TRANSLATION_ONGOING);
}
private void addFlag(int flag) {
mFlags |= flag;
}
private void removeFlag(int flag) {
mFlags &= ~flag;
}
private boolean containsFlag(int flag) {
return (mFlags & flag) == flag;
}
public void extend() {
animateWorkUtilityViews(true);
}
public void shrink() {
animateWorkUtilityViews(false);
}
/**
* Determines if the button should animate based on current state. It should animate the button
* only if it is not in the same state it is animating to.
*/
private boolean shouldAnimate(boolean expanding) {
return expanding != containsFlag(FLAG_IS_EXPAND)
&& (mPauseFABAnim == null || !mPauseFABAnim.isRunning());
}
public int getScrollThreshold() {
return mScrollThreshold;
}
public void updateStringFromCache(){
StringCache cache = mActivityContext.getStringCache();
if (cache != null) {
mPauseText.setText(cache.workProfilePauseButton);
}
}
@VisibleForTesting
boolean shouldUseScheduler() {
return Flags.workSchedulerInWorkProfile() && !mWorkSchedulerIntentAction.isEmpty();
}
@VisibleForTesting
ImageButton getSchedulerButton() {
return mSchedulerButton;
}
}