1 /* 2 * Copyright (C) 2010 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 android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 20 21 import static com.android.launcher3.LauncherState.NORMAL; 22 23 import android.animation.AnimatorSet; 24 import android.animation.FloatArrayEvaluator; 25 import android.animation.ObjectAnimator; 26 import android.animation.ValueAnimator; 27 import android.content.Context; 28 import android.content.res.ColorStateList; 29 import android.content.res.Resources; 30 import android.graphics.ColorMatrix; 31 import android.graphics.ColorMatrixColorFilter; 32 import android.graphics.Rect; 33 import android.graphics.drawable.Drawable; 34 import android.text.TextUtils; 35 import android.util.AttributeSet; 36 import android.util.Property; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.View.OnClickListener; 40 import android.view.accessibility.AccessibilityEvent; 41 import android.widget.PopupWindow; 42 import android.widget.TextView; 43 44 import com.android.launcher3.anim.Interpolators; 45 import com.android.launcher3.dragndrop.DragController; 46 import com.android.launcher3.dragndrop.DragLayer; 47 import com.android.launcher3.dragndrop.DragOptions; 48 import com.android.launcher3.dragndrop.DragView; 49 import com.android.launcher3.model.data.ItemInfo; 50 import com.android.launcher3.userevent.nano.LauncherLogProto.Target; 51 import com.android.launcher3.util.Themes; 52 import com.android.launcher3.util.Thunk; 53 54 /** 55 * Implements a DropTarget. 56 */ 57 public abstract class ButtonDropTarget extends TextView 58 implements DropTarget, DragController.DragListener, OnClickListener { 59 60 private static final Property<ButtonDropTarget, Integer> TEXT_COLOR = 61 new Property<ButtonDropTarget, Integer>(Integer.TYPE, "textColor") { 62 63 @Override 64 public Integer get(ButtonDropTarget target) { 65 return target.getTextColor(); 66 } 67 68 @Override 69 public void set(ButtonDropTarget target, Integer value) { 70 target.setTextColor(value); 71 } 72 }; 73 74 private static final int[] sTempCords = new int[2]; 75 private static final int DRAG_VIEW_DROP_DURATION = 285; 76 77 public static final int TOOLTIP_DEFAULT = 0; 78 public static final int TOOLTIP_LEFT = 1; 79 public static final int TOOLTIP_RIGHT = 2; 80 81 protected final Launcher mLauncher; 82 83 private int mBottomDragPadding; 84 protected DropTargetBar mDropTargetBar; 85 86 /** Whether this drop target is active for the current drag */ 87 protected boolean mActive; 88 /** Whether an accessible drag is in progress */ 89 private boolean mAccessibleDrag; 90 /** An item must be dragged at least this many pixels before this drop target is enabled. */ 91 private final int mDragDistanceThreshold; 92 93 /** The paint applied to the drag view on hover */ 94 protected int mHoverColor = 0; 95 96 protected CharSequence mText; 97 protected ColorStateList mOriginalTextColor; 98 protected Drawable mDrawable; 99 private boolean mTextVisible = true; 100 101 private PopupWindow mToolTip; 102 private int mToolTipLocation; 103 104 private AnimatorSet mCurrentColorAnim; 105 @Thunk ColorMatrix mSrcFilter, mDstFilter, mCurrentFilter; 106 ButtonDropTarget(Context context, AttributeSet attrs)107 public ButtonDropTarget(Context context, AttributeSet attrs) { 108 this(context, attrs, 0); 109 } 110 ButtonDropTarget(Context context, AttributeSet attrs, int defStyle)111 public ButtonDropTarget(Context context, AttributeSet attrs, int defStyle) { 112 super(context, attrs, defStyle); 113 mLauncher = Launcher.getLauncher(context); 114 115 Resources resources = getResources(); 116 mBottomDragPadding = resources.getDimensionPixelSize(R.dimen.drop_target_drag_padding); 117 mDragDistanceThreshold = resources.getDimensionPixelSize(R.dimen.drag_distanceThreshold); 118 } 119 120 @Override onFinishInflate()121 protected void onFinishInflate() { 122 super.onFinishInflate(); 123 mText = getText(); 124 mOriginalTextColor = getTextColors(); 125 setContentDescription(mText); 126 } 127 updateText(int resId)128 protected void updateText(int resId) { 129 setText(resId); 130 mText = getText(); 131 setContentDescription(mText); 132 } 133 setDrawable(int resId)134 protected void setDrawable(int resId) { 135 // We do not set the drawable in the xml as that inflates two drawables corresponding to 136 // drawableLeft and drawableStart. 137 if (mTextVisible) { 138 setCompoundDrawablesRelativeWithIntrinsicBounds(resId, 0, 0, 0); 139 mDrawable = getCompoundDrawablesRelative()[0]; 140 } else { 141 setCompoundDrawablesRelativeWithIntrinsicBounds(0, resId, 0, 0); 142 mDrawable = getCompoundDrawablesRelative()[1]; 143 } 144 } 145 setDropTargetBar(DropTargetBar dropTargetBar)146 public void setDropTargetBar(DropTargetBar dropTargetBar) { 147 mDropTargetBar = dropTargetBar; 148 } 149 hideTooltip()150 private void hideTooltip() { 151 if (mToolTip != null) { 152 mToolTip.dismiss(); 153 mToolTip = null; 154 } 155 } 156 157 @Override onDragEnter(DragObject d)158 public final void onDragEnter(DragObject d) { 159 if (!mAccessibleDrag && !mTextVisible) { 160 // Show tooltip 161 hideTooltip(); 162 163 TextView message = (TextView) LayoutInflater.from(getContext()).inflate( 164 R.layout.drop_target_tool_tip, null); 165 message.setText(mText); 166 167 mToolTip = new PopupWindow(message, WRAP_CONTENT, WRAP_CONTENT); 168 int x = 0, y = 0; 169 if (mToolTipLocation != TOOLTIP_DEFAULT) { 170 y = -getMeasuredHeight(); 171 message.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 172 if (mToolTipLocation == TOOLTIP_LEFT) { 173 x = - getMeasuredWidth() - message.getMeasuredWidth() / 2; 174 } else { 175 x = getMeasuredWidth() / 2 + message.getMeasuredWidth() / 2; 176 } 177 } 178 mToolTip.showAsDropDown(this, x, y); 179 } 180 181 d.dragView.setColor(mHoverColor); 182 animateTextColor(mHoverColor); 183 if (d.stateAnnouncer != null) { 184 d.stateAnnouncer.cancel(); 185 } 186 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 187 } 188 189 @Override onDragOver(DragObject d)190 public void onDragOver(DragObject d) { 191 // Do nothing 192 } 193 resetHoverColor()194 protected void resetHoverColor() { 195 animateTextColor(mOriginalTextColor.getDefaultColor()); 196 } 197 animateTextColor(int targetColor)198 private void animateTextColor(int targetColor) { 199 if (mCurrentColorAnim != null) { 200 mCurrentColorAnim.cancel(); 201 } 202 203 mCurrentColorAnim = new AnimatorSet(); 204 mCurrentColorAnim.setDuration(DragView.COLOR_CHANGE_DURATION); 205 206 if (mSrcFilter == null) { 207 mSrcFilter = new ColorMatrix(); 208 mDstFilter = new ColorMatrix(); 209 mCurrentFilter = new ColorMatrix(); 210 } 211 212 int defaultTextColor = mOriginalTextColor.getDefaultColor(); 213 Themes.setColorChangeOnMatrix(defaultTextColor, getTextColor(), mSrcFilter); 214 Themes.setColorChangeOnMatrix(defaultTextColor, targetColor, mDstFilter); 215 216 ValueAnimator anim1 = ValueAnimator.ofObject( 217 new FloatArrayEvaluator(mCurrentFilter.getArray()), 218 mSrcFilter.getArray(), mDstFilter.getArray()); 219 anim1.addUpdateListener((anim) -> { 220 mDrawable.setColorFilter(new ColorMatrixColorFilter(mCurrentFilter)); 221 invalidate(); 222 }); 223 224 mCurrentColorAnim.play(anim1); 225 mCurrentColorAnim.play(ObjectAnimator.ofArgb(this, TEXT_COLOR, targetColor)); 226 mCurrentColorAnim.start(); 227 } 228 229 @Override onDragExit(DragObject d)230 public final void onDragExit(DragObject d) { 231 hideTooltip(); 232 233 if (!d.dragComplete) { 234 d.dragView.setColor(0); 235 resetHoverColor(); 236 } else { 237 // Restore the hover color 238 d.dragView.setColor(mHoverColor); 239 } 240 } 241 242 @Override onDragStart(DropTarget.DragObject dragObject, DragOptions options)243 public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { 244 mActive = supportsDrop(dragObject.dragInfo); 245 mDrawable.setColorFilter(null); 246 if (mCurrentColorAnim != null) { 247 mCurrentColorAnim.cancel(); 248 mCurrentColorAnim = null; 249 } 250 setTextColor(mOriginalTextColor); 251 setVisibility(mActive ? View.VISIBLE : View.GONE); 252 253 mAccessibleDrag = options.isAccessibleDrag; 254 setOnClickListener(mAccessibleDrag ? this : null); 255 } 256 257 @Override acceptDrop(DragObject dragObject)258 public final boolean acceptDrop(DragObject dragObject) { 259 return supportsDrop(dragObject.dragInfo); 260 } 261 supportsDrop(ItemInfo info)262 protected abstract boolean supportsDrop(ItemInfo info); 263 supportsAccessibilityDrop(ItemInfo info, View view)264 public abstract boolean supportsAccessibilityDrop(ItemInfo info, View view); 265 266 @Override isDropEnabled()267 public boolean isDropEnabled() { 268 return mActive && (mAccessibleDrag || 269 mLauncher.getDragController().getDistanceDragged() >= mDragDistanceThreshold); 270 } 271 272 @Override onDragEnd()273 public void onDragEnd() { 274 mActive = false; 275 setOnClickListener(null); 276 } 277 278 /** 279 * On drop animate the dropView to the icon. 280 */ 281 @Override onDrop(final DragObject d, final DragOptions options)282 public void onDrop(final DragObject d, final DragOptions options) { 283 if (options.isFlingToDelete) { 284 // FlingAnimation handles the animation and then calls completeDrop(). 285 return; 286 } 287 final DragLayer dragLayer = mLauncher.getDragLayer(); 288 final Rect from = new Rect(); 289 dragLayer.getViewRectRelativeToSelf(d.dragView, from); 290 291 final Rect to = getIconRect(d); 292 final float scale = (float) to.width() / from.width(); 293 mDropTargetBar.deferOnDragEnd(); 294 295 Runnable onAnimationEndRunnable = () -> { 296 completeDrop(d); 297 mDropTargetBar.onDragEnd(); 298 mLauncher.getStateManager().goToState(NORMAL); 299 }; 300 301 dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f, 302 DRAG_VIEW_DROP_DURATION, 303 Interpolators.DEACCEL_2, Interpolators.LINEAR, onAnimationEndRunnable, 304 DragLayer.ANIMATION_END_DISAPPEAR, null); 305 } 306 getAccessibilityAction()307 public abstract int getAccessibilityAction(); 308 309 @Override prepareAccessibilityDrop()310 public void prepareAccessibilityDrop() { } 311 onAccessibilityDrop(View view, ItemInfo item)312 public abstract void onAccessibilityDrop(View view, ItemInfo item); 313 completeDrop(DragObject d)314 public abstract void completeDrop(DragObject d); 315 316 @Override getHitRectRelativeToDragLayer(android.graphics.Rect outRect)317 public void getHitRectRelativeToDragLayer(android.graphics.Rect outRect) { 318 super.getHitRect(outRect); 319 outRect.bottom += mBottomDragPadding; 320 321 sTempCords[0] = sTempCords[1] = 0; 322 mLauncher.getDragLayer().getDescendantCoordRelativeToSelf(this, sTempCords); 323 outRect.offsetTo(sTempCords[0], sTempCords[1]); 324 } 325 getIconRect(DragObject dragObject)326 public Rect getIconRect(DragObject dragObject) { 327 int viewWidth = dragObject.dragView.getMeasuredWidth(); 328 int viewHeight = dragObject.dragView.getMeasuredHeight(); 329 int drawableWidth = mDrawable.getIntrinsicWidth(); 330 int drawableHeight = mDrawable.getIntrinsicHeight(); 331 DragLayer dragLayer = mLauncher.getDragLayer(); 332 333 // Find the rect to animate to (the view is center aligned) 334 Rect to = new Rect(); 335 dragLayer.getViewRectRelativeToSelf(this, to); 336 337 final int width = drawableWidth; 338 final int height = drawableHeight; 339 340 final int left; 341 final int right; 342 343 if (Utilities.isRtl(getResources())) { 344 right = to.right - getPaddingRight(); 345 left = right - width; 346 } else { 347 left = to.left + getPaddingLeft(); 348 right = left + width; 349 } 350 351 final int top = to.top + (getMeasuredHeight() - height) / 2; 352 final int bottom = top + height; 353 354 to.set(left, top, right, bottom); 355 356 // Center the destination rect about the trash icon 357 final int xOffset = -(viewWidth - width) / 2; 358 final int yOffset = -(viewHeight - height) / 2; 359 to.offset(xOffset, yOffset); 360 361 return to; 362 } 363 364 @Override onClick(View v)365 public void onClick(View v) { 366 mLauncher.getAccessibilityDelegate().handleAccessibleDrop(this, null, null); 367 } 368 getTextColor()369 public int getTextColor() { 370 return getTextColors().getDefaultColor(); 371 } 372 setTextVisible(boolean isVisible)373 public void setTextVisible(boolean isVisible) { 374 CharSequence newText = isVisible ? mText : ""; 375 if (mTextVisible != isVisible || !TextUtils.equals(newText, getText())) { 376 mTextVisible = isVisible; 377 setText(newText); 378 if (mTextVisible) { 379 setCompoundDrawablesRelativeWithIntrinsicBounds(mDrawable, null, null, null); 380 } else { 381 setCompoundDrawablesRelativeWithIntrinsicBounds(null, mDrawable, null, null); 382 } 383 } 384 } 385 setToolTipLocation(int location)386 public void setToolTipLocation(int location) { 387 mToolTipLocation = location; 388 hideTooltip(); 389 } 390 isTextTruncated(int availableWidth)391 public boolean isTextTruncated(int availableWidth) { 392 availableWidth -= (getPaddingLeft() + getPaddingRight() + mDrawable.getIntrinsicWidth() 393 + getCompoundDrawablePadding()); 394 CharSequence displayedText = TextUtils.ellipsize(mText, getPaint(), availableWidth, 395 TextUtils.TruncateAt.END); 396 return !mText.equals(displayedText); 397 } 398 getDropTargetForLogging()399 public abstract Target getDropTargetForLogging(); 400 } 401