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