/* * Copyright (C) 2018 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.views; import static android.content.Context.ACCESSIBILITY_SERVICE; import static android.view.MotionEvent.ACTION_DOWN; import static androidx.core.graphics.ColorUtils.compositeColors; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.anim.Interpolators.ACCEL; import static com.android.launcher3.anim.Interpolators.DEACCEL; import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.Keyframe; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.RectEvaluator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.util.AttributeSet; import android.util.Property; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; import androidx.customview.widget.ExploreByTouchHelper; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Insettable; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherStateManager; import com.android.launcher3.LauncherStateManager.StateListener; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.uioverrides.WallpaperColorInfo; import com.android.launcher3.uioverrides.WallpaperColorInfo.OnChangeListener; import com.android.launcher3.userevent.nano.LauncherLogProto.Action; import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType; import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; import com.android.launcher3.util.Themes; import com.android.launcher3.widget.WidgetsFullSheet; import java.util.List; /** * Simple scrim which draws a flat color */ public class ScrimView extends View implements Insettable, OnChangeListener, AccessibilityStateChangeListener, StateListener { public static final Property DRAG_HANDLE_ALPHA = new Property(Integer.TYPE, "dragHandleAlpha") { @Override public Integer get(ScrimView scrimView) { return scrimView.mDragHandleAlpha; } @Override public void set(ScrimView scrimView, Integer value) { scrimView.setDragHandleAlpha(value); } }; private static final int WALLPAPERS = R.string.wallpaper_button_text; private static final int WIDGETS = R.string.widget_button_text; private static final int SETTINGS = R.string.settings_button_text; private static final int ALPHA_CHANNEL_COUNT = 1; private final Rect mTempRect = new Rect(); private final int[] mTempPos = new int[2]; protected final Launcher mLauncher; private final WallpaperColorInfo mWallpaperColorInfo; private final AccessibilityManager mAM; protected final int mEndScrim; protected float mMaxScrimAlpha; protected float mProgress = 1; protected int mScrimColor; protected int mCurrentFlatColor; protected int mEndFlatColor; protected int mEndFlatColorAlpha; protected final int mDragHandleSize; protected float mDragHandleOffset; private final Rect mDragHandleBounds; private final RectF mHitRect = new RectF(); private final MultiValueAlpha mMultiValueAlpha; private final AccessibilityHelper mAccessibilityHelper; @Nullable protected Drawable mDragHandle; private int mDragHandleAlpha = 255; public ScrimView(Context context, AttributeSet attrs) { super(context, attrs); mLauncher = Launcher.getLauncher(context); mWallpaperColorInfo = WallpaperColorInfo.getInstance(context); mEndScrim = Themes.getAttrColor(context, R.attr.allAppsScrimColor); mMaxScrimAlpha = 0.7f; mDragHandleSize = context.getResources() .getDimensionPixelSize(R.dimen.vertical_drag_handle_size); mDragHandleBounds = new Rect(0, 0, mDragHandleSize, mDragHandleSize); mAccessibilityHelper = createAccessibilityHelper(); ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper); mAM = (AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE); setFocusable(false); mMultiValueAlpha = new MultiValueAlpha(this, ALPHA_CHANNEL_COUNT); } public AlphaProperty getAlphaProperty(int index) { return mMultiValueAlpha.getProperty(index); } @NonNull protected AccessibilityHelper createAccessibilityHelper() { return new AccessibilityHelper(); } @Override public void setInsets(Rect insets) { updateDragHandleBounds(); updateDragHandleVisibility(null); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); updateDragHandleBounds(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mWallpaperColorInfo.addOnChangeListener(this); onExtractedColorsChanged(mWallpaperColorInfo); mAM.addAccessibilityStateChangeListener(this); onAccessibilityStateChanged(mAM.isEnabled()); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mWallpaperColorInfo.removeOnChangeListener(this); mAM.removeAccessibilityStateChangeListener(this); } @Override public boolean hasOverlappingRendering() { return false; } @Override public void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo) { mScrimColor = wallpaperColorInfo.getMainColor(); mEndFlatColor = compositeColors(mEndScrim, setColorAlphaBound( mScrimColor, Math.round(mMaxScrimAlpha * 255))); mEndFlatColorAlpha = Color.alpha(mEndFlatColor); updateColors(); invalidate(); } public void setProgress(float progress) { if (mProgress != progress) { mProgress = progress; updateColors(); updateDragHandleAlpha(); invalidate(); } } public void reInitUi() { } protected void updateColors() { mCurrentFlatColor = mProgress >= 1 ? 0 : setColorAlphaBound( mEndFlatColor, Math.round((1 - mProgress) * mEndFlatColorAlpha)); } protected void updateDragHandleAlpha() { if (mDragHandle != null) { mDragHandle.setAlpha(mDragHandleAlpha); } } private void setDragHandleAlpha(int alpha) { if (alpha != mDragHandleAlpha) { mDragHandleAlpha = alpha; if (mDragHandle != null) { mDragHandle.setAlpha(mDragHandleAlpha); invalidate(); } } } @Override protected void onDraw(Canvas canvas) { if (mCurrentFlatColor != 0) { canvas.drawColor(mCurrentFlatColor); } drawDragHandle(canvas); } protected void drawDragHandle(Canvas canvas) { if (mDragHandle != null) { canvas.translate(0, -mDragHandleOffset); mDragHandle.draw(canvas); canvas.translate(0, mDragHandleOffset); } } @Override public boolean onTouchEvent(MotionEvent event) { boolean value = super.onTouchEvent(event); if (!value && mDragHandle != null && event.getAction() == ACTION_DOWN && mDragHandle.getAlpha() == 255 && mHitRect.contains(event.getX(), event.getY())) { final Drawable drawable = mDragHandle; mDragHandle = null; Rect bounds = new Rect(mDragHandleBounds); bounds.offset(0, -(int) mDragHandleOffset); drawable.setBounds(bounds); Rect topBounds = new Rect(bounds); topBounds.offset(0, -bounds.height() / 2); Rect invalidateRegion = new Rect(bounds); invalidateRegion.top = topBounds.top; Keyframe frameTop = Keyframe.ofObject(0.6f, topBounds); frameTop.setInterpolator(DEACCEL); Keyframe frameBot = Keyframe.ofObject(1, bounds); frameBot.setInterpolator(ACCEL); PropertyValuesHolder holder = PropertyValuesHolder .ofKeyframe("bounds", Keyframe.ofObject(0, bounds), frameTop, frameBot); holder.setEvaluator(new RectEvaluator()); ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, holder); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { getOverlay().remove(drawable); updateDragHandleVisibility(drawable); } }); anim.addUpdateListener((v) -> invalidate(invalidateRegion)); getOverlay().add(drawable); anim.start(); return true; } return value; } protected void updateDragHandleBounds() { DeviceProfile grid = mLauncher.getDeviceProfile(); final int left; final int width = getMeasuredWidth(); final int top = getMeasuredHeight() - mDragHandleSize - grid.getInsets().bottom; final int topMargin; if (grid.isVerticalBarLayout()) { topMargin = grid.workspacePadding.bottom; if (grid.isSeascape()) { left = width - grid.getInsets().right - mDragHandleSize; } else { left = mDragHandleSize + grid.getInsets().left; } } else { left = (width - mDragHandleSize) / 2; topMargin = grid.hotseatBarSizePx; } mDragHandleBounds.offsetTo(left, top - topMargin); mHitRect.set(mDragHandleBounds); float inset = -mDragHandleSize / 2; mHitRect.inset(inset, inset); if (mDragHandle != null) { mDragHandle.setBounds(mDragHandleBounds); } } @Override public void onAccessibilityStateChanged(boolean enabled) { LauncherStateManager stateManager = mLauncher.getStateManager(); stateManager.removeStateListener(this); if (enabled) { stateManager.addStateListener(this); handleStateChangedComplete(stateManager.getState()); } else { setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); } updateDragHandleVisibility(null); } private void updateDragHandleVisibility(Drawable recycle) { boolean visible = mLauncher.getDeviceProfile().isVerticalBarLayout() || mAM.isEnabled(); boolean wasVisible = mDragHandle != null; if (visible != wasVisible) { if (visible) { mDragHandle = recycle != null ? recycle : mLauncher.getDrawable(R.drawable.drag_handle_indicator); mDragHandle.setBounds(mDragHandleBounds); updateDragHandleAlpha(); } else { mDragHandle = null; } invalidate(); } } @Override public boolean dispatchHoverEvent(MotionEvent event) { return mAccessibilityHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event); } @Override public boolean dispatchKeyEvent(KeyEvent event) { return mAccessibilityHelper.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); } @Override public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); mAccessibilityHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect); } @Override public void onStateTransitionStart(LauncherState toState) {} @Override public void onStateTransitionComplete(LauncherState finalState) { handleStateChangedComplete(finalState); } private void handleStateChangedComplete(LauncherState finalState) { setImportantForAccessibility(finalState == ALL_APPS ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS : IMPORTANT_FOR_ACCESSIBILITY_AUTO); } protected class AccessibilityHelper extends ExploreByTouchHelper { private static final int DRAG_HANDLE_ID = 1; public AccessibilityHelper() { super(ScrimView.this); } @Override protected int getVirtualViewAt(float x, float y) { return mDragHandleBounds.contains((int) x, (int) y) ? DRAG_HANDLE_ID : INVALID_ID; } @Override protected void getVisibleVirtualViews(List virtualViewIds) { virtualViewIds.add(DRAG_HANDLE_ID); } @Override protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfoCompat node) { node.setContentDescription(getContext().getString(R.string.all_apps_button_label)); node.setBoundsInParent(mDragHandleBounds); getLocationOnScreen(mTempPos); mTempRect.set(mDragHandleBounds); mTempRect.offset(mTempPos[0], mTempPos[1]); node.setBoundsInScreen(mTempRect); node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); node.setClickable(true); node.setFocusable(true); if (mLauncher.isInState(NORMAL)) { Context context = getContext(); if (Utilities.isWallpaperAllowed(context)) { node.addAction( new AccessibilityActionCompat(WALLPAPERS, context.getText(WALLPAPERS))); } node.addAction(new AccessibilityActionCompat(WIDGETS, context.getText(WIDGETS))); node.addAction(new AccessibilityActionCompat(SETTINGS, context.getText(SETTINGS))); } } @Override protected boolean onPerformActionForVirtualView( int virtualViewId, int action, Bundle arguments) { if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) { mLauncher.getUserEventDispatcher().logActionOnControl( Action.Touch.TAP, ControlType.ALL_APPS_BUTTON, mLauncher.getStateManager().getState().containerType); mLauncher.getStateManager().goToState(ALL_APPS); return true; } else if (action == WALLPAPERS) { return OptionsPopupView.startWallpaperPicker(ScrimView.this); } else if (action == WIDGETS) { int originalImportanceForAccessibility = getImportantForAccessibility(); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); WidgetsFullSheet widgetsFullSheet = OptionsPopupView.openWidgets(mLauncher); if (widgetsFullSheet == null) { setImportantForAccessibility(originalImportanceForAccessibility); return false; } widgetsFullSheet.addOnAttachStateChangeListener(new OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View view) {} @Override public void onViewDetachedFromWindow(View view) { setImportantForAccessibility(originalImportanceForAccessibility); widgetsFullSheet.removeOnAttachStateChangeListener(this); } }); return true; } else if (action == SETTINGS) { return OptionsPopupView.startSettings(ScrimView.this); } return false; } } public int getDragHandleSize() { return mDragHandleSize; } }