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 17 package com.android.quickstep.views; 18 19 import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorSet; 23 import android.animation.ObjectAnimator; 24 import android.content.Context; 25 import android.graphics.Rect; 26 import android.graphics.drawable.Drawable; 27 import android.util.AttributeSet; 28 import android.view.Gravity; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.LinearLayout; 33 import android.widget.TextView; 34 35 import com.android.launcher3.AbstractFloatingView; 36 import com.android.launcher3.BaseDraggingActivity; 37 import com.android.launcher3.FastBitmapDrawable; 38 import com.android.launcher3.R; 39 import com.android.launcher3.anim.AnimationSuccessListener; 40 import com.android.launcher3.anim.Interpolators; 41 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; 42 import com.android.launcher3.popup.SystemShortcut; 43 import com.android.launcher3.touch.PagedOrientationHandler; 44 import com.android.launcher3.util.Themes; 45 import com.android.launcher3.views.BaseDragLayer; 46 import com.android.quickstep.TaskOverlayFactory; 47 import com.android.quickstep.TaskUtils; 48 import com.android.quickstep.views.IconView.OnScaleUpdateListener; 49 50 /** 51 * Contains options for a recent task when long-pressing its icon. 52 */ 53 public class TaskMenuView extends AbstractFloatingView { 54 55 private static final Rect sTempRect = new Rect(); 56 57 private final OnScaleUpdateListener mTaskViewIconScaleListener = new OnScaleUpdateListener() { 58 @Override 59 public void onScaleUpdate(float scale) { 60 final Drawable drawable = mTaskIcon.getDrawable(); 61 if (drawable instanceof FastBitmapDrawable) { 62 if (scale != ((FastBitmapDrawable) drawable).getScale()) { 63 mMenuIconDrawable.setScale(scale); 64 } 65 } 66 } 67 }; 68 69 private final OnScaleUpdateListener mMenuIconScaleListener = new OnScaleUpdateListener() { 70 @Override 71 public void onScaleUpdate(float scale) { 72 final Drawable taskViewDrawable = mTaskView.getIconView().getDrawable(); 73 if (taskViewDrawable instanceof FastBitmapDrawable) { 74 final float currentScale = ((FastBitmapDrawable) taskViewDrawable).getScale(); 75 if (currentScale != scale) { 76 ((FastBitmapDrawable) taskViewDrawable).setScale(scale); 77 } 78 } 79 } 80 }; 81 82 private static final int REVEAL_OPEN_DURATION = 150; 83 private static final int REVEAL_CLOSE_DURATION = 100; 84 85 private final float mThumbnailTopMargin; 86 private BaseDraggingActivity mActivity; 87 private TextView mTaskName; 88 private IconView mTaskIcon; 89 private AnimatorSet mOpenCloseAnimator; 90 private TaskView mTaskView; 91 private LinearLayout mOptionLayout; 92 private FastBitmapDrawable mMenuIconDrawable; 93 TaskMenuView(Context context, AttributeSet attrs)94 public TaskMenuView(Context context, AttributeSet attrs) { 95 this(context, attrs, 0); 96 } 97 TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr)98 public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) { 99 super(context, attrs, defStyleAttr); 100 101 mActivity = BaseDraggingActivity.fromContext(context); 102 mThumbnailTopMargin = getResources().getDimension(R.dimen.task_thumbnail_top_margin); 103 } 104 105 @Override onFinishInflate()106 protected void onFinishInflate() { 107 super.onFinishInflate(); 108 mTaskName = findViewById(R.id.task_name); 109 mTaskIcon = findViewById(R.id.task_icon); 110 mOptionLayout = findViewById(R.id.menu_option_layout); 111 } 112 113 @Override onControllerInterceptTouchEvent(MotionEvent ev)114 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 115 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 116 BaseDragLayer dl = mActivity.getDragLayer(); 117 if (!dl.isEventOverView(this, ev)) { 118 // TODO: log this once we have a new container type for it? 119 close(true); 120 return true; 121 } 122 } 123 return false; 124 } 125 126 @Override handleClose(boolean animate)127 protected void handleClose(boolean animate) { 128 if (animate) { 129 animateClose(); 130 } else { 131 closeComplete(); 132 } 133 } 134 135 @Override logActionCommand(int command)136 public void logActionCommand(int command) { 137 // TODO 138 } 139 140 @Override onDetachedFromWindow()141 protected void onDetachedFromWindow() { 142 super.onDetachedFromWindow(); 143 144 // Remove all scale listeners when menu is removed 145 mTaskView.getIconView().removeUpdateScaleListener(mTaskViewIconScaleListener); 146 mTaskIcon.removeUpdateScaleListener(mMenuIconScaleListener); 147 } 148 149 @Override isOfType(int type)150 protected boolean isOfType(int type) { 151 return (type & TYPE_TASK_MENU) != 0; 152 } 153 setPosition(float x, float y, PagedOrientationHandler pagedOrientationHandler)154 public void setPosition(float x, float y, PagedOrientationHandler pagedOrientationHandler) { 155 float adjustedY = y + mThumbnailTopMargin; 156 // Changing pivot to make computations easier 157 // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set, 158 // which would render the X and Y position set here incorrect 159 setPivotX(0); 160 setPivotY(0); 161 setRotation(pagedOrientationHandler.getDegreesRotated()); 162 setX(pagedOrientationHandler.getTaskMenuX(x, mTaskView.getThumbnail())); 163 setY(pagedOrientationHandler.getTaskMenuY(adjustedY, mTaskView.getThumbnail())); 164 } 165 onRotationChanged()166 public void onRotationChanged() { 167 if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) { 168 mOpenCloseAnimator.end(); 169 } 170 if (mIsOpen) { 171 mOptionLayout.removeAllViews(); 172 if (!populateAndLayoutMenu()) { 173 close(false); 174 } 175 } 176 } 177 showForTask(TaskView taskView)178 public static TaskMenuView showForTask(TaskView taskView) { 179 BaseDraggingActivity activity = BaseDraggingActivity.fromContext(taskView.getContext()); 180 final TaskMenuView taskMenuView = (TaskMenuView) activity.getLayoutInflater().inflate( 181 R.layout.task_menu, activity.getDragLayer(), false); 182 return taskMenuView.populateAndShowForTask(taskView) ? taskMenuView : null; 183 } 184 populateAndShowForTask(TaskView taskView)185 private boolean populateAndShowForTask(TaskView taskView) { 186 if (isAttachedToWindow()) { 187 return false; 188 } 189 mActivity.getDragLayer().addView(this); 190 mTaskView = taskView; 191 if (!populateAndLayoutMenu()) { 192 return false; 193 } 194 post(this::animateOpen); 195 return true; 196 } 197 198 /** @return true if successfully able to populate task view menu, false otherwise */ populateAndLayoutMenu()199 private boolean populateAndLayoutMenu() { 200 if (mTaskView.getTask().icon == null) { 201 // Icon may not be loaded 202 return false; 203 } 204 addMenuOptions(mTaskView); 205 orientAroundTaskView(mTaskView); 206 return true; 207 } 208 addMenuOptions(TaskView taskView)209 private void addMenuOptions(TaskView taskView) { 210 Drawable icon = taskView.getTask().icon.getConstantState().newDrawable(); 211 mTaskIcon.setDrawable(icon); 212 mTaskIcon.setOnClickListener(v -> close(true)); 213 mTaskName.setText(TaskUtils.getTitle(getContext(), taskView.getTask())); 214 mTaskName.setOnClickListener(v -> close(true)); 215 216 // Set the icons to match scale by listening to each other's changes 217 mMenuIconDrawable = icon instanceof FastBitmapDrawable ? (FastBitmapDrawable) icon : null; 218 taskView.getIconView().addUpdateScaleListener(mTaskViewIconScaleListener); 219 mTaskIcon.addUpdateScaleListener(mMenuIconScaleListener); 220 221 // Move the icon and text up half an icon size to lay over the TaskView 222 LinearLayout.LayoutParams params = 223 (LinearLayout.LayoutParams) mTaskIcon.getLayoutParams(); 224 params.topMargin = (int) -mThumbnailTopMargin; 225 mTaskIcon.setLayoutParams(params); 226 227 TaskOverlayFactory.getEnabledShortcuts(taskView).forEach(this::addMenuOption); 228 } 229 addMenuOption(SystemShortcut menuOption)230 private void addMenuOption(SystemShortcut menuOption) { 231 ViewGroup menuOptionView = (ViewGroup) mActivity.getLayoutInflater().inflate( 232 R.layout.task_view_menu_option, this, false); 233 menuOption.setIconAndLabelFor( 234 menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text)); 235 LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams(); 236 mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp); 237 menuOptionView.setOnClickListener(menuOption); 238 mOptionLayout.addView(menuOptionView); 239 } 240 orientAroundTaskView(TaskView taskView)241 private void orientAroundTaskView(TaskView taskView) { 242 PagedOrientationHandler orientationHandler = taskView.getPagedOrientationHandler(); 243 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 244 mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect); 245 Rect insets = mActivity.getDragLayer().getInsets(); 246 BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams(); 247 params.width = orientationHandler.getTaskMenuWidth(taskView.getThumbnail()); 248 // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start 249 params.gravity = Gravity.LEFT; 250 setLayoutParams(params); 251 setScaleX(taskView.getScaleX()); 252 setScaleY(taskView.getScaleY()); 253 boolean canActivityRotate = taskView.getRecentsView() 254 .mOrientationState.isRecentsActivityRotationAllowed(); 255 mOptionLayout.setOrientation(orientationHandler 256 .getTaskMenuLayoutOrientation(canActivityRotate, mOptionLayout)); 257 setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top, 258 taskView.getPagedOrientationHandler()); 259 } 260 animateOpen()261 private void animateOpen() { 262 animateOpenOrClosed(false); 263 mIsOpen = true; 264 } 265 animateClose()266 private void animateClose() { 267 animateOpenOrClosed(true); 268 } 269 animateOpenOrClosed(boolean closing)270 private void animateOpenOrClosed(boolean closing) { 271 if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) { 272 mOpenCloseAnimator.end(); 273 } 274 mOpenCloseAnimator = new AnimatorSet(); 275 276 final Animator revealAnimator = createOpenCloseOutlineProvider() 277 .createRevealAnimator(this, closing); 278 revealAnimator.setInterpolator(Interpolators.DEACCEL); 279 mOpenCloseAnimator.play(revealAnimator); 280 mOpenCloseAnimator.play(ObjectAnimator.ofFloat(mTaskView.getThumbnail(), DIM_ALPHA, 281 closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA)); 282 mOpenCloseAnimator.addListener(new AnimationSuccessListener() { 283 @Override 284 public void onAnimationStart(Animator animation) { 285 setVisibility(VISIBLE); 286 } 287 288 @Override 289 public void onAnimationSuccess(Animator animator) { 290 if (closing) { 291 closeComplete(); 292 } 293 } 294 }); 295 mOpenCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1)); 296 mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION); 297 mOpenCloseAnimator.start(); 298 } 299 closeComplete()300 private void closeComplete() { 301 mIsOpen = false; 302 mActivity.getDragLayer().removeView(this); 303 } 304 createOpenCloseOutlineProvider()305 private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { 306 float radius = Themes.getDialogCornerRadius(getContext()); 307 Rect fromRect = new Rect(0, 0, getWidth(), 0); 308 Rect toRect = new Rect(0, 0, getWidth(), getHeight()); 309 return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect); 310 } 311 findMenuItemByText(String text)312 public View findMenuItemByText(String text) { 313 for (int i = mOptionLayout.getChildCount() - 1; i >= 0; --i) { 314 final ViewGroup menuOptionView = (ViewGroup) mOptionLayout.getChildAt(i); 315 if (text.equals(menuOptionView.<TextView>findViewById(R.id.text).getText())) { 316 return menuOptionView; 317 } 318 } 319 return null; 320 } 321 } 322