Home | History | Annotate | Download | only in views
      1 /*
      2  * Copyright (C) 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.launcher3.views;
     17 
     18 import static android.content.Context.ACCESSIBILITY_SERVICE;
     19 import static android.support.v4.graphics.ColorUtils.compositeColors;
     20 import static android.support.v4.graphics.ColorUtils.setAlphaComponent;
     21 import static android.view.MotionEvent.ACTION_DOWN;
     22 
     23 import static com.android.launcher3.LauncherState.ALL_APPS;
     24 import static com.android.launcher3.LauncherState.NORMAL;
     25 import static com.android.launcher3.anim.Interpolators.ACCEL;
     26 import static com.android.launcher3.anim.Interpolators.DEACCEL;
     27 
     28 import android.animation.Animator;
     29 import android.animation.AnimatorListenerAdapter;
     30 import android.animation.Keyframe;
     31 import android.animation.ObjectAnimator;
     32 import android.animation.PropertyValuesHolder;
     33 import android.animation.RectEvaluator;
     34 import android.content.Context;
     35 import android.graphics.Canvas;
     36 import android.graphics.Color;
     37 import android.graphics.Rect;
     38 import android.graphics.RectF;
     39 import android.graphics.drawable.Drawable;
     40 import android.os.Bundle;
     41 import android.support.annotation.NonNull;
     42 import android.support.annotation.Nullable;
     43 import android.support.v4.view.ViewCompat;
     44 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
     45 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
     46 import android.support.v4.widget.ExploreByTouchHelper;
     47 import android.util.AttributeSet;
     48 import android.util.Property;
     49 import android.view.KeyEvent;
     50 import android.view.MotionEvent;
     51 import android.view.View;
     52 import android.view.accessibility.AccessibilityManager;
     53 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
     54 
     55 import com.android.launcher3.DeviceProfile;
     56 import com.android.launcher3.Insettable;
     57 import com.android.launcher3.Launcher;
     58 import com.android.launcher3.LauncherState;
     59 import com.android.launcher3.LauncherStateManager;
     60 import com.android.launcher3.LauncherStateManager.StateListener;
     61 import com.android.launcher3.R;
     62 import com.android.launcher3.Utilities;
     63 import com.android.launcher3.uioverrides.WallpaperColorInfo;
     64 import com.android.launcher3.uioverrides.WallpaperColorInfo.OnChangeListener;
     65 import com.android.launcher3.userevent.nano.LauncherLogProto.Action;
     66 import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType;
     67 import com.android.launcher3.util.Themes;
     68 
     69 import java.util.List;
     70 
     71 /**
     72  * Simple scrim which draws a flat color
     73  */
     74 public class ScrimView extends View implements Insettable, OnChangeListener,
     75         AccessibilityStateChangeListener, StateListener {
     76 
     77     public static final Property<ScrimView, Integer> DRAG_HANDLE_ALPHA =
     78             new Property<ScrimView, Integer>(Integer.TYPE, "dragHandleAlpha") {
     79 
     80                 @Override
     81                 public Integer get(ScrimView scrimView) {
     82                     return scrimView.mDragHandleAlpha;
     83                 }
     84 
     85                 @Override
     86                 public void set(ScrimView scrimView, Integer value) {
     87                     scrimView.setDragHandleAlpha(value);
     88                 }
     89             };
     90     private static final int WALLPAPERS = R.string.wallpaper_button_text;
     91     private static final int WIDGETS = R.string.widget_button_text;
     92     private static final int SETTINGS = R.string.settings_button_text;
     93 
     94     private final Rect mTempRect = new Rect();
     95     private final int[] mTempPos = new int[2];
     96 
     97     protected final Launcher mLauncher;
     98     private final WallpaperColorInfo mWallpaperColorInfo;
     99     private final AccessibilityManager mAM;
    100     protected final int mEndScrim;
    101 
    102     protected float mMaxScrimAlpha;
    103 
    104     protected float mProgress = 1;
    105     protected int mScrimColor;
    106 
    107     protected int mCurrentFlatColor;
    108     protected int mEndFlatColor;
    109     protected int mEndFlatColorAlpha;
    110 
    111     protected final int mDragHandleSize;
    112     private final Rect mDragHandleBounds;
    113     private final RectF mHitRect = new RectF();
    114 
    115     private final AccessibilityHelper mAccessibilityHelper;
    116     @Nullable
    117     protected Drawable mDragHandle;
    118 
    119     private int mDragHandleAlpha = 255;
    120 
    121     public ScrimView(Context context, AttributeSet attrs) {
    122         super(context, attrs);
    123         mLauncher = Launcher.getLauncher(context);
    124         mWallpaperColorInfo = WallpaperColorInfo.getInstance(context);
    125         mEndScrim = Themes.getAttrColor(context, R.attr.allAppsScrimColor);
    126 
    127         mMaxScrimAlpha = 0.7f;
    128 
    129         mDragHandleSize = context.getResources()
    130                 .getDimensionPixelSize(R.dimen.vertical_drag_handle_size);
    131         mDragHandleBounds = new Rect(0, 0, mDragHandleSize, mDragHandleSize);
    132 
    133         mAccessibilityHelper = createAccessibilityHelper();
    134         ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper);
    135 
    136         mAM = (AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE);
    137         setFocusable(false);
    138     }
    139 
    140     @NonNull
    141     protected AccessibilityHelper createAccessibilityHelper() {
    142         return new AccessibilityHelper();
    143     }
    144 
    145     @Override
    146     public void setInsets(Rect insets) {
    147         updateDragHandleBounds();
    148         updateDragHandleVisibility(null);
    149     }
    150 
    151     @Override
    152     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    153         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    154         updateDragHandleBounds();
    155     }
    156 
    157     @Override
    158     protected void onAttachedToWindow() {
    159         super.onAttachedToWindow();
    160         mWallpaperColorInfo.addOnChangeListener(this);
    161         onExtractedColorsChanged(mWallpaperColorInfo);
    162 
    163         mAM.addAccessibilityStateChangeListener(this);
    164         onAccessibilityStateChanged(mAM.isEnabled());
    165     }
    166 
    167     @Override
    168     protected void onDetachedFromWindow() {
    169         super.onDetachedFromWindow();
    170         mWallpaperColorInfo.removeOnChangeListener(this);
    171         mAM.removeAccessibilityStateChangeListener(this);
    172     }
    173 
    174     @Override
    175     public boolean hasOverlappingRendering() {
    176         return false;
    177     }
    178 
    179     @Override
    180     public void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo) {
    181         mScrimColor = wallpaperColorInfo.getMainColor();
    182         mEndFlatColor = compositeColors(mEndScrim, setAlphaComponent(
    183                 mScrimColor, Math.round(mMaxScrimAlpha * 255)));
    184         mEndFlatColorAlpha = Color.alpha(mEndFlatColor);
    185         updateColors();
    186         invalidate();
    187     }
    188 
    189     public void setProgress(float progress) {
    190         if (mProgress != progress) {
    191             mProgress = progress;
    192             updateColors();
    193             updateDragHandleAlpha();
    194             invalidate();
    195         }
    196     }
    197 
    198     public void reInitUi() { }
    199 
    200     protected void updateColors() {
    201         mCurrentFlatColor = mProgress >= 1 ? 0 : setAlphaComponent(
    202                 mEndFlatColor, Math.round((1 - mProgress) * mEndFlatColorAlpha));
    203     }
    204 
    205     protected void updateDragHandleAlpha() {
    206         if (mDragHandle != null) {
    207             mDragHandle.setAlpha(mDragHandleAlpha);
    208         }
    209     }
    210 
    211     private void setDragHandleAlpha(int alpha) {
    212         if (alpha != mDragHandleAlpha) {
    213             mDragHandleAlpha = alpha;
    214             if (mDragHandle != null) {
    215                 mDragHandle.setAlpha(mDragHandleAlpha);
    216                 invalidate();
    217             }
    218         }
    219     }
    220 
    221     @Override
    222     protected void onDraw(Canvas canvas) {
    223         if (mCurrentFlatColor != 0) {
    224             canvas.drawColor(mCurrentFlatColor);
    225         }
    226         if (mDragHandle != null) {
    227             mDragHandle.draw(canvas);
    228         }
    229     }
    230 
    231     @Override
    232     public boolean onTouchEvent(MotionEvent event) {
    233         boolean value = super.onTouchEvent(event);
    234         if (!value && mDragHandle != null && event.getAction() == ACTION_DOWN
    235                 && mDragHandle.getAlpha() == 255
    236                 && mHitRect.contains(event.getX(), event.getY())) {
    237 
    238             final Drawable drawable = mDragHandle;
    239             mDragHandle = null;
    240             drawable.setBounds(mDragHandleBounds);
    241 
    242             Rect topBounds = new Rect(mDragHandleBounds);
    243             topBounds.offset(0, -mDragHandleBounds.height() / 2);
    244 
    245             Rect invalidateRegion = new Rect(mDragHandleBounds);
    246             invalidateRegion.top = topBounds.top;
    247 
    248             Keyframe frameTop = Keyframe.ofObject(0.6f, topBounds);
    249             frameTop.setInterpolator(DEACCEL);
    250             Keyframe frameBot = Keyframe.ofObject(1, mDragHandleBounds);
    251             frameBot.setInterpolator(ACCEL);
    252             PropertyValuesHolder holder = PropertyValuesHolder .ofKeyframe("bounds",
    253                     Keyframe.ofObject(0, mDragHandleBounds), frameTop, frameBot);
    254             holder.setEvaluator(new RectEvaluator());
    255 
    256             ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(drawable, holder);
    257             anim.addListener(new AnimatorListenerAdapter() {
    258                 @Override
    259                 public void onAnimationEnd(Animator animation) {
    260                     getOverlay().remove(drawable);
    261                     updateDragHandleVisibility(drawable);
    262                 }
    263             });
    264             anim.addUpdateListener((v) -> invalidate(invalidateRegion));
    265             getOverlay().add(drawable);
    266             anim.start();
    267         }
    268         return value;
    269     }
    270 
    271     protected void updateDragHandleBounds() {
    272         DeviceProfile grid = mLauncher.getDeviceProfile();
    273         final int left;
    274         final int width = getMeasuredWidth();
    275         final int top = getMeasuredHeight() - mDragHandleSize - grid.getInsets().bottom;
    276         final int topMargin;
    277 
    278         if (grid.isVerticalBarLayout()) {
    279             topMargin = grid.workspacePadding.bottom;
    280             if (grid.isSeascape()) {
    281                 left = width - grid.getInsets().right - mDragHandleSize;
    282             } else {
    283                 left = mDragHandleSize + grid.getInsets().left;
    284             }
    285         } else {
    286             left = (width - mDragHandleSize) / 2;
    287             topMargin = grid.hotseatBarSizePx;
    288         }
    289         mDragHandleBounds.offsetTo(left, top - topMargin);
    290         mHitRect.set(mDragHandleBounds);
    291         float inset = -mDragHandleSize / 2;
    292         mHitRect.inset(inset, inset);
    293 
    294         if (mDragHandle != null) {
    295             mDragHandle.setBounds(mDragHandleBounds);
    296         }
    297     }
    298 
    299     @Override
    300     public void onAccessibilityStateChanged(boolean enabled) {
    301         LauncherStateManager stateManager = mLauncher.getStateManager();
    302         stateManager.removeStateListener(this);
    303 
    304         if (enabled) {
    305             stateManager.addStateListener(this);
    306             onStateSetImmediately(mLauncher.getStateManager().getState());
    307         } else {
    308             setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
    309         }
    310         updateDragHandleVisibility(null);
    311     }
    312 
    313     private void updateDragHandleVisibility(Drawable recycle) {
    314         boolean visible = mLauncher.getDeviceProfile().isVerticalBarLayout() || mAM.isEnabled();
    315         boolean wasVisible = mDragHandle != null;
    316         if (visible != wasVisible) {
    317             if (visible) {
    318                 mDragHandle = recycle != null ? recycle :
    319                         mLauncher.getDrawable(R.drawable.drag_handle_indicator);
    320                 mDragHandle.setBounds(mDragHandleBounds);
    321 
    322                 updateDragHandleAlpha();
    323             } else {
    324                 mDragHandle = null;
    325             }
    326             invalidate();
    327         }
    328     }
    329 
    330     @Override
    331     public boolean dispatchHoverEvent(MotionEvent event) {
    332         return mAccessibilityHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
    333     }
    334 
    335     @Override
    336     public boolean dispatchKeyEvent(KeyEvent event) {
    337         return mAccessibilityHelper.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
    338     }
    339 
    340     @Override
    341     public void onFocusChanged(boolean gainFocus, int direction,
    342             Rect previouslyFocusedRect) {
    343         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
    344         mAccessibilityHelper.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
    345     }
    346 
    347     @Override
    348     public void onStateTransitionStart(LauncherState toState) {}
    349 
    350     @Override
    351     public void onStateTransitionComplete(LauncherState finalState) {
    352         onStateSetImmediately(finalState);
    353     }
    354 
    355     @Override
    356     public void onStateSetImmediately(LauncherState state) {
    357         setImportantForAccessibility(state == ALL_APPS
    358                 ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
    359                 : IMPORTANT_FOR_ACCESSIBILITY_AUTO);
    360     }
    361 
    362     protected class AccessibilityHelper extends ExploreByTouchHelper {
    363 
    364         private static final int DRAG_HANDLE_ID = 1;
    365 
    366         public AccessibilityHelper() {
    367             super(ScrimView.this);
    368         }
    369 
    370         @Override
    371         protected int getVirtualViewAt(float x, float y) {
    372             return  mDragHandleBounds.contains((int) x, (int) y)
    373                     ? DRAG_HANDLE_ID : INVALID_ID;
    374         }
    375 
    376         @Override
    377         protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
    378             virtualViewIds.add(DRAG_HANDLE_ID);
    379         }
    380 
    381         @Override
    382         protected void onPopulateNodeForVirtualView(int virtualViewId,
    383                 AccessibilityNodeInfoCompat node) {
    384             node.setContentDescription(getContext().getString(R.string.all_apps_button_label));
    385             node.setBoundsInParent(mDragHandleBounds);
    386 
    387             getLocationOnScreen(mTempPos);
    388             mTempRect.set(mDragHandleBounds);
    389             mTempRect.offset(mTempPos[0], mTempPos[1]);
    390             node.setBoundsInScreen(mTempRect);
    391 
    392             node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
    393             node.setClickable(true);
    394             node.setFocusable(true);
    395 
    396             if (mLauncher.isInState(NORMAL)) {
    397                 Context context = getContext();
    398                 if (Utilities.isWallpaperAllowed(context)) {
    399                     node.addAction(
    400                             new AccessibilityActionCompat(WALLPAPERS, context.getText(WALLPAPERS)));
    401                 }
    402                 node.addAction(new AccessibilityActionCompat(WIDGETS, context.getText(WIDGETS)));
    403                 node.addAction(new AccessibilityActionCompat(SETTINGS, context.getText(SETTINGS)));
    404             }
    405         }
    406 
    407         @Override
    408         protected boolean onPerformActionForVirtualView(
    409                 int virtualViewId, int action, Bundle arguments) {
    410             if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
    411                 mLauncher.getUserEventDispatcher().logActionOnControl(
    412                         Action.Touch.TAP, ControlType.ALL_APPS_BUTTON,
    413                         mLauncher.getStateManager().getState().containerType);
    414                 mLauncher.getStateManager().goToState(ALL_APPS);
    415                 return true;
    416             } else if (action == WALLPAPERS) {
    417                 return OptionsPopupView.startWallpaperPicker(ScrimView.this);
    418             } else if (action == WIDGETS) {
    419                 return OptionsPopupView.onWidgetsClicked(ScrimView.this);
    420             } else if (action == SETTINGS) {
    421                 return OptionsPopupView.startSettings(ScrimView.this);
    422             }
    423 
    424             return false;
    425         }
    426     }
    427 
    428     public int getDragHandleSize() {
    429         return mDragHandleSize;
    430     }
    431 }
    432