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 populateAndLayoutMenu(); 173 } 174 } 175 showForTask(TaskView taskView)176 public static TaskMenuView showForTask(TaskView taskView) { 177 BaseDraggingActivity activity = BaseDraggingActivity.fromContext(taskView.getContext()); 178 final TaskMenuView taskMenuView = (TaskMenuView) activity.getLayoutInflater().inflate( 179 R.layout.task_menu, activity.getDragLayer(), false); 180 return taskMenuView.populateAndShowForTask(taskView) ? taskMenuView : null; 181 } 182 populateAndShowForTask(TaskView taskView)183 private boolean populateAndShowForTask(TaskView taskView) { 184 if (isAttachedToWindow()) { 185 return false; 186 } 187 mActivity.getDragLayer().addView(this); 188 mTaskView = taskView; 189 populateAndLayoutMenu(); 190 post(this::animateOpen); 191 return true; 192 } 193 populateAndLayoutMenu()194 private void populateAndLayoutMenu() { 195 addMenuOptions(mTaskView); 196 orientAroundTaskView(mTaskView); 197 } 198 addMenuOptions(TaskView taskView)199 private void addMenuOptions(TaskView taskView) { 200 Drawable icon = taskView.getTask().icon.getConstantState().newDrawable(); 201 mTaskIcon.setDrawable(icon); 202 mTaskIcon.setOnClickListener(v -> close(true)); 203 mTaskName.setText(TaskUtils.getTitle(getContext(), taskView.getTask())); 204 mTaskName.setOnClickListener(v -> close(true)); 205 206 // Set the icons to match scale by listening to each other's changes 207 mMenuIconDrawable = icon instanceof FastBitmapDrawable ? (FastBitmapDrawable) icon : null; 208 taskView.getIconView().addUpdateScaleListener(mTaskViewIconScaleListener); 209 mTaskIcon.addUpdateScaleListener(mMenuIconScaleListener); 210 211 // Move the icon and text up half an icon size to lay over the TaskView 212 LinearLayout.LayoutParams params = 213 (LinearLayout.LayoutParams) mTaskIcon.getLayoutParams(); 214 params.topMargin = (int) -mThumbnailTopMargin; 215 mTaskIcon.setLayoutParams(params); 216 217 TaskOverlayFactory.getEnabledShortcuts(taskView).forEach(this::addMenuOption); 218 } 219 addMenuOption(SystemShortcut menuOption)220 private void addMenuOption(SystemShortcut menuOption) { 221 ViewGroup menuOptionView = (ViewGroup) mActivity.getLayoutInflater().inflate( 222 R.layout.task_view_menu_option, this, false); 223 menuOption.setIconAndLabelFor( 224 menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text)); 225 LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams(); 226 mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp); 227 menuOptionView.setOnClickListener(menuOption); 228 mOptionLayout.addView(menuOptionView); 229 } 230 orientAroundTaskView(TaskView taskView)231 private void orientAroundTaskView(TaskView taskView) { 232 PagedOrientationHandler orientationHandler = taskView.getPagedOrientationHandler(); 233 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 234 mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect); 235 Rect insets = mActivity.getDragLayer().getInsets(); 236 BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams(); 237 params.width = orientationHandler.getTaskMenuWidth(taskView.getThumbnail()); 238 // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start 239 params.gravity = Gravity.LEFT; 240 setLayoutParams(params); 241 setScaleX(taskView.getScaleX()); 242 setScaleY(taskView.getScaleY()); 243 mOptionLayout.setOrientation(orientationHandler 244 .getTaskMenuLayoutOrientation(mOptionLayout)); 245 setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top, 246 taskView.getPagedOrientationHandler()); 247 } 248 animateOpen()249 private void animateOpen() { 250 animateOpenOrClosed(false); 251 mIsOpen = true; 252 } 253 animateClose()254 private void animateClose() { 255 animateOpenOrClosed(true); 256 } 257 animateOpenOrClosed(boolean closing)258 private void animateOpenOrClosed(boolean closing) { 259 if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) { 260 mOpenCloseAnimator.end(); 261 } 262 mOpenCloseAnimator = new AnimatorSet(); 263 264 final Animator revealAnimator = createOpenCloseOutlineProvider() 265 .createRevealAnimator(this, closing); 266 revealAnimator.setInterpolator(Interpolators.DEACCEL); 267 mOpenCloseAnimator.play(revealAnimator); 268 mOpenCloseAnimator.play(ObjectAnimator.ofFloat(mTaskView.getThumbnail(), DIM_ALPHA, 269 closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA)); 270 mOpenCloseAnimator.addListener(new AnimationSuccessListener() { 271 @Override 272 public void onAnimationStart(Animator animation) { 273 setVisibility(VISIBLE); 274 } 275 276 @Override 277 public void onAnimationSuccess(Animator animator) { 278 if (closing) { 279 closeComplete(); 280 } 281 } 282 }); 283 mOpenCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1)); 284 mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION); 285 mOpenCloseAnimator.start(); 286 } 287 closeComplete()288 private void closeComplete() { 289 mIsOpen = false; 290 mActivity.getDragLayer().removeView(this); 291 } 292 createOpenCloseOutlineProvider()293 private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { 294 float radius = Themes.getDialogCornerRadius(getContext()); 295 Rect fromRect = new Rect(0, 0, getWidth(), 0); 296 Rect toRect = new Rect(0, 0, getWidth(), getHeight()); 297 return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect); 298 } 299 findMenuItemByText(String text)300 public View findMenuItemByText(String text) { 301 for (int i = mOptionLayout.getChildCount() - 1; i >= 0; --i) { 302 final ViewGroup menuOptionView = (ViewGroup) mOptionLayout.getChildAt(i); 303 if (text.equals(menuOptionView.<TextView>findViewById(R.id.text).getText())) { 304 return menuOptionView; 305 } 306 } 307 return null; 308 } 309 } 310