Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2017 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 
     17 package com.android.launcher3.widget;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ObjectAnimator;
     22 import android.content.Context;
     23 import android.graphics.Rect;
     24 import android.util.AttributeSet;
     25 import android.view.Gravity;
     26 import android.view.LayoutInflater;
     27 import android.view.MotionEvent;
     28 import android.view.View;
     29 import android.view.ViewGroup;
     30 import android.view.animation.AnimationUtils;
     31 import android.view.animation.Interpolator;
     32 import android.widget.TextView;
     33 
     34 import com.android.launcher3.AbstractFloatingView;
     35 import com.android.launcher3.DropTarget;
     36 import com.android.launcher3.Insettable;
     37 import com.android.launcher3.ItemInfo;
     38 import com.android.launcher3.Launcher;
     39 import com.android.launcher3.LauncherAnimUtils;
     40 import com.android.launcher3.LauncherAppState;
     41 import com.android.launcher3.R;
     42 import com.android.launcher3.Utilities;
     43 import com.android.launcher3.touch.SwipeDetector;
     44 import com.android.launcher3.anim.PropertyListBuilder;
     45 import com.android.launcher3.dragndrop.DragController;
     46 import com.android.launcher3.dragndrop.DragOptions;
     47 import com.android.launcher3.graphics.GradientView;
     48 import com.android.launcher3.model.WidgetItem;
     49 import com.android.launcher3.userevent.nano.LauncherLogProto;
     50 import com.android.launcher3.util.PackageUserKey;
     51 import com.android.launcher3.util.SystemUiController;
     52 import com.android.launcher3.util.Themes;
     53 import com.android.launcher3.util.TouchController;
     54 
     55 import java.util.List;
     56 
     57 /**
     58  * Bottom sheet for the "Widgets" system shortcut in the long-press popup.
     59  */
     60 public class WidgetsBottomSheet extends AbstractFloatingView implements Insettable, TouchController,
     61         SwipeDetector.Listener, View.OnClickListener, View.OnLongClickListener,
     62         DragController.DragListener {
     63 
     64     private int mTranslationYOpen;
     65     private int mTranslationYClosed;
     66     private float mTranslationYRange;
     67 
     68     private Launcher mLauncher;
     69     private ItemInfo mOriginalItemInfo;
     70     private ObjectAnimator mOpenCloseAnimator;
     71     private Interpolator mFastOutSlowInInterpolator;
     72     private SwipeDetector.ScrollInterpolator mScrollInterpolator;
     73     private Rect mInsets;
     74     private SwipeDetector mSwipeDetector;
     75     private GradientView mGradientBackground;
     76 
     77     public WidgetsBottomSheet(Context context, AttributeSet attrs) {
     78         this(context, attrs, 0);
     79     }
     80 
     81     public WidgetsBottomSheet(Context context, AttributeSet attrs, int defStyleAttr) {
     82         super(context, attrs, defStyleAttr);
     83         setWillNotDraw(false);
     84         mLauncher = Launcher.getLauncher(context);
     85         mOpenCloseAnimator = LauncherAnimUtils.ofPropertyValuesHolder(this);
     86         mFastOutSlowInInterpolator =
     87                 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
     88         mScrollInterpolator = new SwipeDetector.ScrollInterpolator();
     89         mInsets = new Rect();
     90         mSwipeDetector = new SwipeDetector(context, this, SwipeDetector.VERTICAL);
     91         mGradientBackground = (GradientView) mLauncher.getLayoutInflater().inflate(
     92                 R.layout.gradient_bg, mLauncher.getDragLayer(), false);
     93     }
     94 
     95     @Override
     96     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
     97         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
     98         mTranslationYOpen = 0;
     99         mTranslationYClosed = getMeasuredHeight();
    100         mTranslationYRange = mTranslationYClosed - mTranslationYOpen;
    101     }
    102 
    103     public void populateAndShow(ItemInfo itemInfo) {
    104         mOriginalItemInfo = itemInfo;
    105         ((TextView) findViewById(R.id.title)).setText(getContext().getString(
    106                 R.string.widgets_bottom_sheet_title, mOriginalItemInfo.title));
    107 
    108         onWidgetsBound();
    109 
    110         mLauncher.getDragLayer().addView(mGradientBackground);
    111         mGradientBackground.setVisibility(VISIBLE);
    112         mLauncher.getDragLayer().addView(this);
    113         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
    114         setTranslationY(mTranslationYClosed);
    115         mIsOpen = false;
    116         open(true);
    117     }
    118 
    119     @Override
    120     protected void onWidgetsBound() {
    121         List<WidgetItem> widgets = mLauncher.getWidgetsForPackageUser(new PackageUserKey(
    122                 mOriginalItemInfo.getTargetComponent().getPackageName(), mOriginalItemInfo.user));
    123 
    124         ViewGroup widgetRow = (ViewGroup) findViewById(R.id.widgets);
    125         ViewGroup widgetCells = (ViewGroup) widgetRow.findViewById(R.id.widgets_cell_list);
    126 
    127         widgetCells.removeAllViews();
    128 
    129         for (int i = 0; i < widgets.size(); i++) {
    130             WidgetCell widget = addItemCell(widgetCells);
    131             widget.applyFromCellItem(widgets.get(i), LauncherAppState.getInstance(mLauncher)
    132                     .getWidgetCache());
    133             widget.ensurePreview();
    134             widget.setVisibility(View.VISIBLE);
    135             if (i < widgets.size() - 1) {
    136                 addDivider(widgetCells);
    137             }
    138         }
    139 
    140         if (widgets.size() == 1) {
    141             // If there is only one widget, we want to center it instead of left-align.
    142             WidgetsBottomSheet.LayoutParams params = (WidgetsBottomSheet.LayoutParams)
    143                     widgetRow.getLayoutParams();
    144             params.gravity = Gravity.CENTER_HORIZONTAL;
    145         } else {
    146             // Otherwise, add an empty view to the start as padding (but still scroll edge to edge).
    147             View leftPaddingView = LayoutInflater.from(getContext()).inflate(
    148                     R.layout.widget_list_divider, widgetRow, false);
    149             leftPaddingView.getLayoutParams().width = Utilities.pxFromDp(
    150                     16, getResources().getDisplayMetrics());
    151             widgetCells.addView(leftPaddingView, 0);
    152         }
    153     }
    154 
    155     private void addDivider(ViewGroup parent) {
    156         LayoutInflater.from(getContext()).inflate(R.layout.widget_list_divider, parent, true);
    157     }
    158 
    159     private WidgetCell addItemCell(ViewGroup parent) {
    160         WidgetCell widget = (WidgetCell) LayoutInflater.from(getContext()).inflate(
    161                 R.layout.widget_cell, parent, false);
    162 
    163         widget.setOnClickListener(this);
    164         widget.setOnLongClickListener(this);
    165         widget.setAnimatePreview(false);
    166 
    167         parent.addView(widget);
    168         return widget;
    169     }
    170 
    171     @Override
    172     public void onClick(View view) {
    173         mLauncher.getWidgetsView().handleClick();
    174     }
    175 
    176     @Override
    177     public boolean onLongClick(View view) {
    178         mLauncher.getDragController().addDragListener(this);
    179         return mLauncher.getWidgetsView().handleLongClick(view);
    180     }
    181 
    182     private void open(boolean animate) {
    183         if (mIsOpen || mOpenCloseAnimator.isRunning()) {
    184             return;
    185         }
    186         mIsOpen = true;
    187         boolean isSheetDark = Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark);
    188         mLauncher.getSystemUiController().updateUiState(
    189                 SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET,
    190                 isSheetDark ? SystemUiController.FLAG_DARK_NAV : SystemUiController.FLAG_LIGHT_NAV);
    191         if (animate) {
    192             mOpenCloseAnimator.setValues(new PropertyListBuilder()
    193                     .translationY(mTranslationYOpen).build());
    194             mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
    195                 @Override
    196                 public void onAnimationEnd(Animator animation) {
    197                     mSwipeDetector.finishedScrolling();
    198                 }
    199             });
    200             mOpenCloseAnimator.setInterpolator(mFastOutSlowInInterpolator);
    201             mOpenCloseAnimator.start();
    202         } else {
    203             setTranslationY(mTranslationYOpen);
    204         }
    205     }
    206 
    207     @Override
    208     protected void handleClose(boolean animate) {
    209         if (!mIsOpen || mOpenCloseAnimator.isRunning()) {
    210             return;
    211         }
    212         if (animate) {
    213             mOpenCloseAnimator.setValues(new PropertyListBuilder()
    214                     .translationY(mTranslationYClosed).build());
    215             mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
    216                 @Override
    217                 public void onAnimationEnd(Animator animation) {
    218                     mSwipeDetector.finishedScrolling();
    219                     onCloseComplete();
    220                 }
    221             });
    222             mOpenCloseAnimator.setInterpolator(mSwipeDetector.isIdleState()
    223                     ? mFastOutSlowInInterpolator : mScrollInterpolator);
    224             mOpenCloseAnimator.start();
    225         } else {
    226             setTranslationY(mTranslationYClosed);
    227             onCloseComplete();
    228         }
    229     }
    230 
    231     private void onCloseComplete() {
    232         mIsOpen = false;
    233         mLauncher.getDragLayer().removeView(mGradientBackground);
    234         mLauncher.getDragLayer().removeView(WidgetsBottomSheet.this);
    235         mLauncher.getSystemUiController().updateUiState(
    236                 SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, 0);
    237     }
    238 
    239     @Override
    240     protected boolean isOfType(@FloatingViewType int type) {
    241         return (type & TYPE_WIDGETS_BOTTOM_SHEET) != 0;
    242     }
    243 
    244     @Override
    245     public int getLogContainerType() {
    246         return LauncherLogProto.ContainerType.WIDGETS; // TODO: be more specific
    247     }
    248 
    249     /**
    250      * Returns a {@link WidgetsBottomSheet} which is already open or null
    251      */
    252     public static WidgetsBottomSheet getOpen(Launcher launcher) {
    253         return getOpenView(launcher, TYPE_WIDGETS_BOTTOM_SHEET);
    254     }
    255 
    256     @Override
    257     public void setInsets(Rect insets) {
    258         // Extend behind left, right, and bottom insets.
    259         int leftInset = insets.left - mInsets.left;
    260         int rightInset = insets.right - mInsets.right;
    261         int bottomInset = insets.bottom - mInsets.bottom;
    262         mInsets.set(insets);
    263         setPadding(getPaddingLeft() + leftInset, getPaddingTop(),
    264                 getPaddingRight() + rightInset, getPaddingBottom() + bottomInset);
    265     }
    266 
    267     /* SwipeDetector.Listener */
    268 
    269     @Override
    270     public void onDragStart(boolean start) {
    271     }
    272 
    273     @Override
    274     public boolean onDrag(float displacement, float velocity) {
    275         setTranslationY(Utilities.boundToRange(displacement, mTranslationYOpen,
    276                 mTranslationYClosed));
    277         return true;
    278     }
    279 
    280     @Override
    281     public void setTranslationY(float translationY) {
    282         super.setTranslationY(translationY);
    283         if (mGradientBackground == null) return;
    284         float p = (mTranslationYClosed - translationY) / mTranslationYRange;
    285         boolean showScrim = p <= 0;
    286         mGradientBackground.setProgress(p, showScrim);
    287     }
    288 
    289     @Override
    290     public void onDragEnd(float velocity, boolean fling) {
    291         if ((fling && velocity > 0) || getTranslationY() > (mTranslationYRange) / 2) {
    292             mScrollInterpolator.setVelocityAtZero(velocity);
    293             mOpenCloseAnimator.setDuration(SwipeDetector.calculateDuration(velocity,
    294                     (mTranslationYClosed - getTranslationY()) / mTranslationYRange));
    295             close(true);
    296         } else {
    297             mIsOpen = false;
    298             mOpenCloseAnimator.setDuration(SwipeDetector.calculateDuration(velocity,
    299                     (getTranslationY() - mTranslationYOpen) / mTranslationYRange));
    300             open(true);
    301         }
    302     }
    303 
    304     @Override
    305     public boolean onControllerTouchEvent(MotionEvent ev) {
    306         return mSwipeDetector.onTouchEvent(ev);
    307     }
    308 
    309     @Override
    310     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
    311         int directionsToDetectScroll = mSwipeDetector.isIdleState() ?
    312                 SwipeDetector.DIRECTION_NEGATIVE : 0;
    313         mSwipeDetector.setDetectableScrollConditions(
    314                 directionsToDetectScroll, false);
    315         mSwipeDetector.onTouchEvent(ev);
    316         return mSwipeDetector.isDraggingOrSettling();
    317     }
    318 
    319     /* DragListener */
    320 
    321     @Override
    322     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
    323         // A widget or custom shortcut was dragged.
    324         close(true);
    325     }
    326 
    327     @Override
    328     public void onDragEnd() {
    329     }
    330 }
    331