mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-19 10:48:19 +00:00
Merge "Optimizing some layouts in taskview" into ub-launcher3-qt-dev
am: 552b3fe811
Change-Id: Iddee5000935e8070bce412bb0cce889a39c79617
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
<com.android.quickstep.hints.ProactiveHintsContainer
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:gravity="center_horizontal">
|
||||
</com.android.quickstep.hints.ProactiveHintsContainer>
|
||||
@@ -18,9 +18,11 @@ package com.android.quickstep.views;
|
||||
|
||||
import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
|
||||
|
||||
import static com.android.launcher3.Utilities.prefixTextWithIcon;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.ActivityOptions;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.LauncherApps;
|
||||
import android.content.pm.LauncherApps.AppUsageLimit;
|
||||
@@ -28,16 +30,16 @@ import android.icu.text.MeasureFormat;
|
||||
import android.icu.text.MeasureFormat.FormatWidth;
|
||||
import android.icu.util.Measure;
|
||||
import android.icu.util.MeasureUnit;
|
||||
import android.os.Build;
|
||||
import android.os.UserHandle;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.android.launcher3.BaseActivity;
|
||||
import com.android.launcher3.BaseDraggingActivity;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.Utilities;
|
||||
import com.android.launcher3.userevent.nano.LauncherLogProto;
|
||||
@@ -46,45 +48,72 @@ import com.android.systemui.shared.recents.model.Task;
|
||||
import java.time.Duration;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class DigitalWellBeingToast extends LinearLayout {
|
||||
@TargetApi(Build.VERSION_CODES.Q)
|
||||
public final class DigitalWellBeingToast {
|
||||
static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
|
||||
static final int MINUTE_MS = 60000;
|
||||
private final LauncherApps mLauncherApps;
|
||||
|
||||
public interface InitializeCallback {
|
||||
void call(String contentDescription);
|
||||
}
|
||||
|
||||
private static final String TAG = DigitalWellBeingToast.class.getSimpleName();
|
||||
|
||||
private final BaseDraggingActivity mActivity;
|
||||
private final TaskView mTaskView;
|
||||
private final LauncherApps mLauncherApps;
|
||||
|
||||
private Task mTask;
|
||||
private TextView mText;
|
||||
private boolean mHasLimit;
|
||||
private long mAppRemainingTimeMs;
|
||||
|
||||
public DigitalWellBeingToast(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setLayoutDirection(Utilities.isRtl(getResources()) ?
|
||||
View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
|
||||
setOnClickListener((view) -> openAppUsageSettings());
|
||||
mLauncherApps = context.getSystemService(LauncherApps.class);
|
||||
public DigitalWellBeingToast(BaseDraggingActivity activity, TaskView taskView) {
|
||||
mActivity = activity;
|
||||
mTaskView = taskView;
|
||||
mLauncherApps = activity.getSystemService(LauncherApps.class);
|
||||
}
|
||||
|
||||
public TextView getTextView() {
|
||||
return mText;
|
||||
private void setTaskFooter(View view) {
|
||||
View oldFooter = mTaskView.setFooter(TaskView.INDEX_DIGITAL_WELLBEING_TOAST, view);
|
||||
if (oldFooter != null) {
|
||||
oldFooter.setOnClickListener(null);
|
||||
mActivity.getViewCache().recycleView(R.layout.digital_wellbeing_toast, oldFooter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
||||
mText = findViewById(R.id.digital_well_being_remaining_time);
|
||||
private void setNoLimit() {
|
||||
mHasLimit = false;
|
||||
mTaskView.setContentDescription(mTask.titleDescription);
|
||||
setTaskFooter(null);
|
||||
mAppRemainingTimeMs = 0;
|
||||
}
|
||||
|
||||
public void initialize(Task task, InitializeCallback callback) {
|
||||
private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
|
||||
mAppRemainingTimeMs = appRemainingTimeMs;
|
||||
mHasLimit = true;
|
||||
TextView toast = mActivity.getViewCache().getView(R.layout.digital_wellbeing_toast,
|
||||
mActivity, mTaskView);
|
||||
toast.setText(prefixTextWithIcon(mActivity, R.drawable.ic_hourglass_top, getText()));
|
||||
toast.setOnClickListener(this::openAppUsageSettings);
|
||||
setTaskFooter(toast);
|
||||
|
||||
mTaskView.setContentDescription(
|
||||
getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
|
||||
RecentsView rv = mTaskView.getRecentsView();
|
||||
if (rv != null) {
|
||||
rv.onDigitalWellbeingToastShown();
|
||||
}
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return getText(mAppRemainingTimeMs);
|
||||
}
|
||||
|
||||
public boolean hasLimit() {
|
||||
return mHasLimit;
|
||||
}
|
||||
|
||||
public void initialize(Task task) {
|
||||
mTask = task;
|
||||
|
||||
if (task.key.userId != UserHandle.myUserId()) {
|
||||
setVisibility(GONE);
|
||||
callback.call(task.titleDescription);
|
||||
setNoLimit();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,16 +127,12 @@ public final class DigitalWellBeingToast extends LinearLayout {
|
||||
final long appRemainingTimeMs =
|
||||
usageLimit != null ? usageLimit.getUsageRemaining() : -1;
|
||||
|
||||
post(() -> {
|
||||
mTaskView.post(() -> {
|
||||
if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
|
||||
setVisibility(GONE);
|
||||
setNoLimit();
|
||||
} else {
|
||||
setVisibility(VISIBLE);
|
||||
mText.setText(getText(appRemainingTimeMs));
|
||||
setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
|
||||
}
|
||||
|
||||
callback.call(getContentDescriptionForTask(
|
||||
task, appUsageLimitTimeMs, appRemainingTimeMs));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -146,7 +171,7 @@ public final class DigitalWellBeingToast extends LinearLayout {
|
||||
|
||||
// Use a specific string for usage less than one minute but non-zero.
|
||||
if (duration.compareTo(Duration.ZERO) > 0) {
|
||||
return getResources().getString(durationLessThanOneMinuteStringId);
|
||||
return mActivity.getString(durationLessThanOneMinuteStringId);
|
||||
}
|
||||
|
||||
// Otherwise, return 0-minute string.
|
||||
@@ -176,24 +201,24 @@ public final class DigitalWellBeingToast extends LinearLayout {
|
||||
}
|
||||
|
||||
private String getText(long remainingTime) {
|
||||
return getResources().getString(
|
||||
return mActivity.getString(
|
||||
R.string.time_left_for_app,
|
||||
getRoundedUpToMinuteReadableDuration(remainingTime));
|
||||
}
|
||||
|
||||
public void openAppUsageSettings() {
|
||||
public void openAppUsageSettings(View view) {
|
||||
final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
|
||||
.putExtra(Intent.EXTRA_PACKAGE_NAME,
|
||||
mTask.getTopComponent().getPackageName()).addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
try {
|
||||
final BaseActivity activity = BaseActivity.fromContext(getContext());
|
||||
final BaseActivity activity = BaseActivity.fromContext(view.getContext());
|
||||
final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
|
||||
this, 0, 0,
|
||||
getWidth(), getHeight());
|
||||
view, 0, 0,
|
||||
view.getWidth(), view.getHeight());
|
||||
activity.startActivity(intent, options.toBundle());
|
||||
activity.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP,
|
||||
LauncherLogProto.ControlType.APP_USAGE_SETTINGS, this);
|
||||
LauncherLogProto.ControlType.APP_USAGE_SETTINGS, view);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.e(TAG, "Failed to open app usage settings for task "
|
||||
+ mTask.getTopComponent().getPackageName(), e);
|
||||
@@ -203,7 +228,7 @@ public final class DigitalWellBeingToast extends LinearLayout {
|
||||
private String getContentDescriptionForTask(
|
||||
Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
|
||||
return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ?
|
||||
getResources().getString(
|
||||
mActivity.getString(
|
||||
R.string.task_contents_description_with_remaining_time,
|
||||
task.titleDescription,
|
||||
getText(appRemainingTimeMs)) :
|
||||
|
||||
@@ -351,6 +351,9 @@ public abstract class RecentsView<T extends BaseActivity> extends PagedView impl
|
||||
.getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
|
||||
setWillNotDraw(false);
|
||||
updateEmptyMessage();
|
||||
|
||||
// Initialize quickstep specific cache params here, as this is constructed only once
|
||||
mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
|
||||
}
|
||||
|
||||
public OverScroller getScroller() {
|
||||
|
||||
@@ -27,6 +27,8 @@ import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.TimeInterpolator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.animation.ValueAnimator.AnimatorUpdateListener;
|
||||
import android.app.ActivityOptions;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
@@ -39,6 +41,7 @@ import android.os.Handler;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.FloatProperty;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
@@ -145,14 +148,12 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
};
|
||||
|
||||
private final TaskOutlineProvider mOutlineProvider;
|
||||
private final FooterOutlineProvider mFooterOutlineProvider;
|
||||
|
||||
private Task mTask;
|
||||
private TaskThumbnailView mSnapshotView;
|
||||
private TaskMenuView mMenuView;
|
||||
private IconView mIconView;
|
||||
private View mTaskFooterContainer;
|
||||
private DigitalWellBeingToast mDigitalWellBeingToast;
|
||||
private final DigitalWellBeingToast mDigitalWellBeingToast;
|
||||
private float mCurveScale;
|
||||
private float mFullscreenProgress;
|
||||
private final FullscreenDrawParams mCurrentFullscreenParams;
|
||||
@@ -171,6 +172,14 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
private TaskThumbnailCache.ThumbnailLoadRequest mThumbnailLoadRequest;
|
||||
private TaskIconCache.IconLoadRequest mIconLoadRequest;
|
||||
|
||||
// Order in which the footers appear. Lower order appear below higher order.
|
||||
public static final int INDEX_DIGITAL_WELLBEING_TOAST = 0;
|
||||
public static final int INDEX_PROACTIVE_SUGGEST = 1;
|
||||
private final FooterWrapper[] mFooters = new FooterWrapper[2];
|
||||
private float mFooterVerticalOffset = 0;
|
||||
private float mFooterAlpha = 1;
|
||||
private int mStackHeight;
|
||||
|
||||
public TaskView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
@@ -208,8 +217,9 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
mCornerRadius = TaskCornerRadius.get(context);
|
||||
mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources());
|
||||
mCurrentFullscreenParams = new FullscreenDrawParams(mCornerRadius);
|
||||
mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);
|
||||
|
||||
mOutlineProvider = new TaskOutlineProvider(getResources(), mCurrentFullscreenParams);
|
||||
mFooterOutlineProvider = new FooterOutlineProvider(mCurrentFullscreenParams);
|
||||
setOutlineProvider(mOutlineProvider);
|
||||
}
|
||||
|
||||
@@ -218,10 +228,6 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
super.onFinishInflate();
|
||||
mSnapshotView = findViewById(R.id.snapshot);
|
||||
mIconView = findViewById(R.id.icon);
|
||||
mDigitalWellBeingToast = findViewById(R.id.digital_well_being_toast);
|
||||
mTaskFooterContainer = findViewById(R.id.task_footer_container);
|
||||
mTaskFooterContainer.setOutlineProvider(mFooterOutlineProvider);
|
||||
mTaskFooterContainer.setClipToOutline(true);
|
||||
}
|
||||
|
||||
public TaskMenuView getMenuView() {
|
||||
@@ -357,15 +363,7 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
|
||||
getRecentsView().updateLiveTileIcon(task.icon);
|
||||
}
|
||||
mDigitalWellBeingToast.initialize(
|
||||
mTask,
|
||||
contentDescription -> {
|
||||
setContentDescription(contentDescription);
|
||||
if (mDigitalWellBeingToast.getVisibility() == VISIBLE
|
||||
&& getRecentsView() != null) {
|
||||
getRecentsView().onDigitalWellbeingToastShown();
|
||||
}
|
||||
});
|
||||
mDigitalWellBeingToast.initialize(mTask);
|
||||
});
|
||||
} else {
|
||||
mSnapshotView.setThumbnail(null, null);
|
||||
@@ -424,14 +422,12 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
mIconView.setScaleX(scale);
|
||||
mIconView.setScaleY(scale);
|
||||
|
||||
int footerVerticalOffset = (int) (mTaskFooterContainer.getHeight() * (1.0f - scale));
|
||||
mTaskFooterContainer.setTranslationY(
|
||||
mCurrentFullscreenParams.mCurrentDrawnInsets.bottom +
|
||||
mCurrentFullscreenParams.mCurrentDrawnInsets.top +
|
||||
footerVerticalOffset);
|
||||
mFooterOutlineProvider.setFullscreenDrawParams(
|
||||
mCurrentFullscreenParams, footerVerticalOffset);
|
||||
mTaskFooterContainer.invalidateOutline();
|
||||
mFooterVerticalOffset = 1.0f - scale;
|
||||
for (FooterWrapper footer : mFooters) {
|
||||
if (footer != null) {
|
||||
footer.updateFooterOffset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setIconScaleAnimStartProgress(float startProgress) {
|
||||
@@ -505,8 +501,13 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA);
|
||||
setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation));
|
||||
|
||||
float fade = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation, 0f, 1f);
|
||||
mTaskFooterContainer.setAlpha(fade);
|
||||
mFooterAlpha = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation, 0f, 1f);
|
||||
for (FooterWrapper footer : mFooters) {
|
||||
if (footer != null) {
|
||||
footer.mView.setAlpha(mFooterAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
if (mMenuView != null) {
|
||||
mMenuView.setPosition(getX() - getRecentsView().getScrollX(), getY());
|
||||
mMenuView.setScaleX(getScaleX());
|
||||
@@ -514,6 +515,56 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the footer at the specific index and returns the previously set footer.
|
||||
*/
|
||||
public View setFooter(int index, View view) {
|
||||
View oldFooter = null;
|
||||
|
||||
// If the footer are is already collapsed, do not animate entry
|
||||
boolean shouldAnimateEntry = mFooterVerticalOffset <= 0;
|
||||
|
||||
if (mFooters[index] != null) {
|
||||
oldFooter = mFooters[index].mView;
|
||||
mFooters[index].release();
|
||||
removeView(oldFooter);
|
||||
|
||||
// If we are replacing an existing footer, do not animate entry
|
||||
shouldAnimateEntry = false;
|
||||
}
|
||||
if (view != null) {
|
||||
int indexToAdd = getChildCount();
|
||||
for (int i = index - 1; i >= 0; i--) {
|
||||
if (mFooters[i] != null) {
|
||||
indexToAdd = indexOfChild(mFooters[i].mView);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
addView(view, indexToAdd);
|
||||
((LayoutParams) view.getLayoutParams()).gravity =
|
||||
Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
|
||||
view.setAlpha(mFooterAlpha);
|
||||
mFooters[index] = new FooterWrapper(view);
|
||||
if (shouldAnimateEntry) {
|
||||
mFooters[index].animateEntry();
|
||||
}
|
||||
} else {
|
||||
mFooters[index] = null;
|
||||
}
|
||||
|
||||
mStackHeight = 0;
|
||||
for (FooterWrapper footer : mFooters) {
|
||||
if (footer != null) {
|
||||
footer.setVerticalShift(mStackHeight);
|
||||
mStackHeight += footer.mExpectedHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return oldFooter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
@@ -523,6 +574,18 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(0, 0, getWidth(), getHeight());
|
||||
setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT);
|
||||
}
|
||||
|
||||
mStackHeight = 0;
|
||||
for (FooterWrapper footer : mFooters) {
|
||||
if (footer != null) {
|
||||
mStackHeight += footer.mView.getHeight();
|
||||
}
|
||||
}
|
||||
for (FooterWrapper footer : mFooters) {
|
||||
if (footer != null) {
|
||||
footer.updateFooterOffset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static float getCurveScaleForInterpolation(float linearInterpolation) {
|
||||
@@ -581,26 +644,74 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FooterOutlineProvider extends ViewOutlineProvider {
|
||||
private class FooterWrapper extends ViewOutlineProvider {
|
||||
|
||||
private FullscreenDrawParams mFullscreenDrawParams;
|
||||
private int mVerticalOffset;
|
||||
private final Rect mOutlineRect = new Rect();
|
||||
final View mView;
|
||||
final ViewOutlineProvider mOldOutlineProvider;
|
||||
final ViewOutlineProvider mDelegate;
|
||||
|
||||
FooterOutlineProvider(FullscreenDrawParams params) {
|
||||
mFullscreenDrawParams = params;
|
||||
final int mExpectedHeight;
|
||||
final int mOldPaddingBottom;
|
||||
|
||||
int mAnimationOffset = 0;
|
||||
int mEntryAnimationOffset = 0;
|
||||
|
||||
public FooterWrapper(View view) {
|
||||
mView = view;
|
||||
mOldOutlineProvider = view.getOutlineProvider();
|
||||
mDelegate = mOldOutlineProvider == null
|
||||
? ViewOutlineProvider.BACKGROUND : mOldOutlineProvider;
|
||||
|
||||
int h = view.getLayoutParams().height;
|
||||
if (h > 0) {
|
||||
mExpectedHeight = h;
|
||||
} else {
|
||||
int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST);
|
||||
view.measure(m, m);
|
||||
mExpectedHeight = view.getMeasuredHeight();
|
||||
}
|
||||
mOldPaddingBottom = view.getPaddingBottom();
|
||||
|
||||
if (mOldOutlineProvider != null) {
|
||||
view.setOutlineProvider(this);
|
||||
view.setClipToOutline(true);
|
||||
}
|
||||
}
|
||||
|
||||
void setFullscreenDrawParams(FullscreenDrawParams params, int verticalOffset) {
|
||||
mFullscreenDrawParams = params;
|
||||
mVerticalOffset = verticalOffset;
|
||||
public void setVerticalShift(int shift) {
|
||||
mView.setPadding(mView.getPaddingLeft(), mView.getPaddingTop(),
|
||||
mView.getPaddingRight(), mOldPaddingBottom + shift);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline) {
|
||||
mOutlineRect.set(0, 0, view.getWidth(), view.getHeight());
|
||||
mOutlineRect.offset(0, -mVerticalOffset);
|
||||
outline.setRoundRect(mOutlineRect, mFullscreenDrawParams.mCurrentDrawnCornerRadius);
|
||||
mDelegate.getOutline(view, outline);
|
||||
outline.offset(0, -mAnimationOffset - mEntryAnimationOffset);
|
||||
}
|
||||
|
||||
void updateFooterOffset() {
|
||||
mAnimationOffset = Math.round(mStackHeight * mFooterVerticalOffset);
|
||||
mView.setTranslationY(mAnimationOffset + mEntryAnimationOffset
|
||||
+ mCurrentFullscreenParams.mCurrentDrawnInsets.bottom
|
||||
+ mCurrentFullscreenParams.mCurrentDrawnInsets.top);
|
||||
mView.invalidateOutline();
|
||||
}
|
||||
|
||||
void release() {
|
||||
mView.setOutlineProvider(mOldOutlineProvider);
|
||||
setVerticalShift(0);
|
||||
}
|
||||
|
||||
void animateEntry() {
|
||||
ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
|
||||
animator.addUpdateListener(anim -> {
|
||||
float factor = 1 - anim.getAnimatedFraction();
|
||||
int totalShift = mExpectedHeight + mView.getPaddingBottom() - mOldPaddingBottom;
|
||||
mEntryAnimationOffset = Math.round(factor * totalShift);
|
||||
updateFooterOffset();
|
||||
});
|
||||
animator.setDuration(100);
|
||||
animator.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,7 +735,7 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
}
|
||||
}
|
||||
|
||||
if (mDigitalWellBeingToast.getVisibility() == VISIBLE) {
|
||||
if (mDigitalWellBeingToast.hasLimit()) {
|
||||
info.addAction(
|
||||
new AccessibilityNodeInfo.AccessibilityAction(
|
||||
R.string.accessibility_app_usage_settings,
|
||||
@@ -648,7 +759,7 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
|
||||
}
|
||||
|
||||
if (action == R.string.accessibility_app_usage_settings) {
|
||||
mDigitalWellBeingToast.openAppUsageSettings();
|
||||
mDigitalWellBeingToast.openAppUsageSettings(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
27
quickstep/res/layout/digital_wellbeing_toast.xml
Normal file
27
quickstep/res/layout/digital_wellbeing_toast.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2019 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.
|
||||
-->
|
||||
<TextView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/bg_wellbeing_toast"
|
||||
android:fontFamily="sans-serif"
|
||||
android:forceHasOverlappingRendering="false"
|
||||
android:gravity="center"
|
||||
android:importantForAccessibility="noHideDescendants"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"/>
|
||||
@@ -25,7 +25,7 @@
|
||||
android:id="@+id/snapshot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="@dimen/task_thumbnail_top_margin" />
|
||||
android:layout_marginTop="@dimen/task_thumbnail_top_margin"/>
|
||||
|
||||
<com.android.quickstep.views.IconView
|
||||
android:id="@+id/icon"
|
||||
@@ -33,47 +33,5 @@
|
||||
android:layout_height="@dimen/task_thumbnail_icon_size"
|
||||
android:layout_gravity="top|center_horizontal"
|
||||
android:focusable="false"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/task_footer_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:animateLayoutChanges="true"
|
||||
android:forceHasOverlappingRendering="true"
|
||||
android:layout_gravity="bottom|center_horizontal">
|
||||
<FrameLayout
|
||||
android:id="@+id/proactive_suggest_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
<com.android.quickstep.views.DigitalWellBeingToast
|
||||
android:id="@+id/digital_well_being_toast"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:importantForAccessibility="noHideDescendants"
|
||||
android:background="@drawable/bg_wellbeing_toast"
|
||||
android:gravity="center"
|
||||
android:visibility="gone">
|
||||
<ImageView
|
||||
android:id="@+id/digital_well_being_hourglass"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@drawable/ic_hourglass_top"
|
||||
/>
|
||||
<TextView
|
||||
android:id="@+id/digital_well_being_remaining_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/white"
|
||||
android:gravity="center_vertical"
|
||||
/>
|
||||
</com.android.quickstep.views.DigitalWellBeingToast>
|
||||
</LinearLayout>
|
||||
android:importantForAccessibility="no"/>
|
||||
</com.android.quickstep.views.TaskView>
|
||||
@@ -51,15 +51,15 @@ public class DigitalWellBeingToastTest extends AbstractQuickStepTest {
|
||||
mLauncher.pressHome();
|
||||
final DigitalWellBeingToast toast = getToast();
|
||||
|
||||
assertTrue("Toast is not visible", toast.isShown());
|
||||
assertEquals("Toast text: ", "5 minutes left today", toast.getTextView().getText());
|
||||
assertTrue("Toast is not visible", toast.hasLimit());
|
||||
assertEquals("Toast text: ", "5 minutes left today", toast.getText());
|
||||
|
||||
// Unset time limit for app.
|
||||
runWithShellPermission(
|
||||
() -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
|
||||
|
||||
mLauncher.pressHome();
|
||||
assertFalse("Toast is visible", getToast().isShown());
|
||||
assertFalse("Toast is visible", getToast().hasLimit());
|
||||
} finally {
|
||||
runWithShellPermission(
|
||||
() -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
|
||||
|
||||
@@ -69,6 +69,7 @@ import com.android.launcher3.compat.ShortcutConfigActivityInfo;
|
||||
import com.android.launcher3.config.FeatureFlags;
|
||||
import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
|
||||
import com.android.launcher3.graphics.RotationMode;
|
||||
import com.android.launcher3.graphics.TintedDrawableSpan;
|
||||
import com.android.launcher3.icons.LauncherIcons;
|
||||
import com.android.launcher3.shortcuts.DeepShortcutManager;
|
||||
import com.android.launcher3.shortcuts.ShortcutKey;
|
||||
@@ -562,6 +563,20 @@ public final class Utilities {
|
||||
return spanned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixes a text with the provided icon
|
||||
*/
|
||||
public static CharSequence prefixTextWithIcon(Context context, int iconRes, CharSequence msg) {
|
||||
// Update the hint to contain the icon.
|
||||
// Prefix the original hint with two spaces. The first space gets replaced by the icon
|
||||
// using span. The second space is used for a singe space character between the hint
|
||||
// and the icon.
|
||||
SpannableString spanned = new SpannableString(" " + msg);
|
||||
spanned.setSpan(new TintedDrawableSpan(context, iconRes),
|
||||
0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
|
||||
return spanned;
|
||||
}
|
||||
|
||||
public static SharedPreferences getPrefs(Context context) {
|
||||
return context.getSharedPreferences(
|
||||
LauncherFiles.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE);
|
||||
|
||||
@@ -20,6 +20,7 @@ import static android.view.View.MeasureSpec.getSize;
|
||||
import static android.view.View.MeasureSpec.makeMeasureSpec;
|
||||
|
||||
import static com.android.launcher3.LauncherState.ALL_APPS_HEADER;
|
||||
import static com.android.launcher3.Utilities.prefixTextWithIcon;
|
||||
import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
|
||||
|
||||
import android.content.Context;
|
||||
@@ -40,6 +41,7 @@ import com.android.launcher3.ExtendedEditText;
|
||||
import com.android.launcher3.Insettable;
|
||||
import com.android.launcher3.Launcher;
|
||||
import com.android.launcher3.R;
|
||||
import com.android.launcher3.Utilities;
|
||||
import com.android.launcher3.allapps.AllAppsContainerView;
|
||||
import com.android.launcher3.allapps.AllAppsStore;
|
||||
import com.android.launcher3.allapps.AlphabeticalAppsList;
|
||||
@@ -89,14 +91,7 @@ public class AppsSearchContainerLayout extends ExtendedEditText
|
||||
mFixedTranslationY = getTranslationY();
|
||||
mMarginTopAdjusting = mFixedTranslationY - getPaddingTop();
|
||||
|
||||
// Update the hint to contain the icon.
|
||||
// Prefix the original hint with two spaces. The first space gets replaced by the icon
|
||||
// using span. The second space is used for a singe space character between the hint
|
||||
// and the icon.
|
||||
SpannableString spanned = new SpannableString(" " + getHint());
|
||||
spanned.setSpan(new TintedDrawableSpan(getContext(), R.drawable.ic_allapps_search),
|
||||
0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
|
||||
setHint(spanned);
|
||||
setHint(prefixTextWithIcon(getContext(), R.drawable.ic_allapps_search, getHint()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -32,7 +32,7 @@ public class TintedDrawableSpan extends DynamicDrawableSpan {
|
||||
|
||||
public TintedDrawableSpan(Context context, int resourceId) {
|
||||
super(ALIGN_BOTTOM);
|
||||
mDrawable = context.getDrawable(resourceId);
|
||||
mDrawable = context.getDrawable(resourceId).mutate();
|
||||
mOldTint = 0;
|
||||
mDrawable.setTint(0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user