1 /*
2  * Copyright (C) 2011 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;
18 
19 import static com.android.launcher3.ButtonDropTarget.TOOLTIP_DEFAULT;
20 import static com.android.launcher3.ButtonDropTarget.TOOLTIP_LEFT;
21 import static com.android.launcher3.ButtonDropTarget.TOOLTIP_RIGHT;
22 import static com.android.launcher3.anim.AlphaUpdateListener.updateVisibility;
23 
24 import android.animation.TimeInterpolator;
25 import android.content.Context;
26 import android.graphics.Rect;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.util.TypedValue;
30 import android.view.Gravity;
31 import android.view.View;
32 import android.view.ViewDebug;
33 import android.view.ViewPropertyAnimator;
34 import android.widget.FrameLayout;
35 
36 import androidx.annotation.NonNull;
37 
38 import com.android.launcher3.anim.Interpolators;
39 import com.android.launcher3.dragndrop.DragController;
40 import com.android.launcher3.dragndrop.DragController.DragListener;
41 import com.android.launcher3.dragndrop.DragOptions;
42 import com.android.launcher3.testing.TestProtocol;
43 
44 /*
45  * The top bar containing various drop targets: Delete/App Info/Uninstall.
46  */
47 public class DropTargetBar extends FrameLayout
48         implements DragListener, Insettable {
49 
50     protected static final int DEFAULT_DRAG_FADE_DURATION = 175;
51     protected static final TimeInterpolator DEFAULT_INTERPOLATOR = Interpolators.ACCEL;
52 
53     private final Runnable mFadeAnimationEndRunnable =
54             () -> updateVisibility(DropTargetBar.this);
55 
56     @ViewDebug.ExportedProperty(category = "launcher")
57     protected boolean mDeferOnDragEnd;
58 
59     @ViewDebug.ExportedProperty(category = "launcher")
60     protected boolean mVisible = false;
61 
62     private ButtonDropTarget[] mDropTargets;
63     private ViewPropertyAnimator mCurrentAnimation;
64 
65     private boolean mIsVertical = true;
66 
DropTargetBar(Context context, AttributeSet attrs)67     public DropTargetBar(Context context, AttributeSet attrs) {
68         super(context, attrs);
69     }
70 
DropTargetBar(Context context, AttributeSet attrs, int defStyle)71     public DropTargetBar(Context context, AttributeSet attrs, int defStyle) {
72         super(context, attrs, defStyle);
73     }
74 
75     @Override
onFinishInflate()76     protected void onFinishInflate() {
77         super.onFinishInflate();
78         mDropTargets = new ButtonDropTarget[getChildCount()];
79         for (int i = 0; i < mDropTargets.length; i++) {
80             mDropTargets[i] = (ButtonDropTarget) getChildAt(i);
81             mDropTargets[i].setDropTargetBar(this);
82         }
83     }
84 
85     @Override
setInsets(Rect insets)86     public void setInsets(Rect insets) {
87         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
88         DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
89         mIsVertical = grid.isVerticalBarLayout();
90 
91         lp.leftMargin = insets.left;
92         lp.topMargin = insets.top;
93         lp.bottomMargin = insets.bottom;
94         lp.rightMargin = insets.right;
95         int tooltipLocation = TOOLTIP_DEFAULT;
96 
97         if (grid.isVerticalBarLayout()) {
98             lp.width = grid.dropTargetBarSizePx;
99             lp.height = grid.availableHeightPx - 2 * grid.edgeMarginPx;
100             lp.gravity = grid.isSeascape() ? Gravity.RIGHT : Gravity.LEFT;
101             tooltipLocation = grid.isSeascape() ? TOOLTIP_LEFT : TOOLTIP_RIGHT;
102         } else {
103             int gap;
104             if (grid.isTablet) {
105                 // XXX: If the icon size changes across orientations, we will have to take
106                 //      that into account here too.
107                 gap = ((grid.widthPx - 2 * grid.edgeMarginPx
108                         - (grid.inv.numColumns * grid.cellWidthPx))
109                         / (2 * (grid.inv.numColumns + 1)))
110                         + grid.edgeMarginPx;
111             } else {
112                 gap = getContext().getResources()
113                         .getDimensionPixelSize(R.dimen.drop_target_bar_margin_horizontal);
114             }
115             lp.width = grid.availableWidthPx - 2 * gap;
116 
117             lp.topMargin += grid.edgeMarginPx;
118             lp.height = grid.dropTargetBarSizePx;
119             lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
120         }
121         setLayoutParams(lp);
122         for (ButtonDropTarget button : mDropTargets) {
123             button.setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.dropTargetTextSizePx);
124             button.setToolTipLocation(tooltipLocation);
125         }
126     }
127 
setup(DragController dragController)128     public void setup(DragController dragController) {
129         dragController.addDragListener(this);
130         for (int i = 0; i < mDropTargets.length; i++) {
131             dragController.addDragListener(mDropTargets[i]);
132             dragController.addDropTarget(mDropTargets[i]);
133         }
134     }
135 
136     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)137     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
138         int width = MeasureSpec.getSize(widthMeasureSpec);
139         int height = MeasureSpec.getSize(heightMeasureSpec);
140 
141         int visibleCount = getVisibleButtonsCount();
142         if (visibleCount == 0) {
143             // do nothing
144         } else if (mIsVertical) {
145             int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
146             int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
147 
148             for (ButtonDropTarget button : mDropTargets) {
149                 if (button.getVisibility() != GONE) {
150                     button.setTextVisible(false);
151                     button.measure(widthSpec, heightSpec);
152                 }
153             }
154         } else {
155             int availableWidth = width / visibleCount;
156             boolean textVisible = true;
157             for (ButtonDropTarget buttons : mDropTargets) {
158                 if (buttons.getVisibility() != GONE) {
159                     textVisible = textVisible && !buttons.isTextTruncated(availableWidth);
160                 }
161             }
162 
163             int widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST);
164             int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
165             for (ButtonDropTarget button : mDropTargets) {
166                 if (button.getVisibility() != GONE) {
167                     button.setTextVisible(textVisible);
168                     button.measure(widthSpec, heightSpec);
169                 }
170             }
171         }
172         setMeasuredDimension(width, height);
173     }
174 
175     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)176     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
177         int visibleCount = getVisibleButtonsCount();
178         if (visibleCount == 0) {
179             // do nothing
180         } else if (mIsVertical) {
181             int gap = getResources().getDimensionPixelSize(R.dimen.drop_target_vertical_gap);
182             int start = gap;
183             int end;
184 
185             for (ButtonDropTarget button : mDropTargets) {
186                 if (button.getVisibility() != GONE) {
187                     end = start + button.getMeasuredHeight();
188                     button.layout(0, start, button.getMeasuredWidth(), end);
189                     start = end + gap;
190                 }
191             }
192         } else {
193             int frameSize = (right - left) / visibleCount;
194 
195             int start = frameSize / 2;
196             int halfWidth;
197             for (ButtonDropTarget button : mDropTargets) {
198                 if (button.getVisibility() != GONE) {
199                     halfWidth = button.getMeasuredWidth() / 2;
200                     button.layout(start - halfWidth, 0,
201                             start + halfWidth, button.getMeasuredHeight());
202                     start = start + frameSize;
203                 }
204             }
205         }
206     }
207 
getVisibleButtonsCount()208     private int getVisibleButtonsCount() {
209         int visibleCount = 0;
210         for (ButtonDropTarget buttons : mDropTargets) {
211             if (buttons.getVisibility() != GONE) {
212                 visibleCount++;
213             }
214         }
215         return visibleCount;
216     }
217 
animateToVisibility(boolean isVisible)218     public void animateToVisibility(boolean isVisible) {
219         if (TestProtocol.sDebugTracing) {
220             Log.d(TestProtocol.NO_DROP_TARGET, "8");
221         }
222         if (mVisible != isVisible) {
223             mVisible = isVisible;
224 
225             // Cancel any existing animation
226             if (mCurrentAnimation != null) {
227                 mCurrentAnimation.cancel();
228                 mCurrentAnimation = null;
229             }
230 
231             float finalAlpha = mVisible ? 1 : 0;
232             if (Float.compare(getAlpha(), finalAlpha) != 0) {
233                 setVisibility(View.VISIBLE);
234                 mCurrentAnimation = animate().alpha(finalAlpha)
235                         .setInterpolator(DEFAULT_INTERPOLATOR)
236                         .setDuration(DEFAULT_DRAG_FADE_DURATION)
237                         .withEndAction(mFadeAnimationEndRunnable);
238             }
239 
240         }
241     }
242 
243     /*
244      * DragController.DragListener implementation
245      */
246     @Override
onDragStart(DropTarget.DragObject dragObject, DragOptions options)247     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
248         if (TestProtocol.sDebugTracing) {
249             Log.d(TestProtocol.NO_DROP_TARGET, "7");
250         }
251         animateToVisibility(true);
252     }
253 
254     /**
255      * This is called to defer hiding the delete drop target until the drop animation has completed,
256      * instead of hiding immediately when the drag has ended.
257      */
deferOnDragEnd()258     protected void deferOnDragEnd() {
259         mDeferOnDragEnd = true;
260     }
261 
262     @Override
onDragEnd()263     public void onDragEnd() {
264         if (!mDeferOnDragEnd) {
265             animateToVisibility(false);
266         } else {
267             mDeferOnDragEnd = false;
268         }
269     }
270 
getDropTargets()271     public ButtonDropTarget[] getDropTargets() {
272         return mDropTargets;
273     }
274 
275     @Override
onVisibilityChanged(@onNull View changedView, int visibility)276     protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
277         super.onVisibilityChanged(changedView, visibility);
278         if (TestProtocol.sDebugTracing) {
279             if (visibility == VISIBLE) {
280                 Log.d(TestProtocol.NO_DROP_TARGET, "9");
281             } else {
282                 Log.d(TestProtocol.NO_DROP_TARGET, "Hiding drop target", new Exception());
283             }
284         }
285     }
286 }
287