1 /* <lambda>null2 * Copyright (C) 2025 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.uioverrides.touchcontrollers 18 19 import android.content.Context 20 import android.graphics.Rect 21 import android.view.MotionEvent 22 import com.android.app.animation.Interpolators.ZOOM_IN 23 import com.android.launcher3.AbstractFloatingView 24 import com.android.launcher3.LauncherAnimUtils 25 import com.android.launcher3.Utilities.EDGE_NAV_BAR 26 import com.android.launcher3.Utilities.boundToRange 27 import com.android.launcher3.Utilities.debugLog 28 import com.android.launcher3.Utilities.isRtl 29 import com.android.launcher3.anim.AnimatorPlaybackController 30 import com.android.launcher3.touch.BaseSwipeDetector 31 import com.android.launcher3.touch.SingleAxisSwipeDetector 32 import com.android.launcher3.util.DisplayController 33 import com.android.launcher3.util.FlingBlockCheck 34 import com.android.launcher3.util.TouchController 35 import com.android.quickstep.views.RecentsView 36 import com.android.quickstep.views.RecentsViewContainer 37 import com.android.quickstep.views.TaskView 38 import kotlin.math.abs 39 40 /** Touch controller which handles dragging task view cards for launch. */ 41 class TaskViewLaunchTouchController<CONTAINER>( 42 private val container: CONTAINER, 43 private val taskViewRecentsTouchContext: TaskViewRecentsTouchContext, 44 ) : TouchController, SingleAxisSwipeDetector.Listener where 45 CONTAINER : Context, 46 CONTAINER : RecentsViewContainer { 47 private val tempRect = Rect() 48 private val flingBlockCheck = FlingBlockCheck() 49 private val recentsView: RecentsView<*, *> = container.getOverviewPanel() 50 private val detector: SingleAxisSwipeDetector = 51 SingleAxisSwipeDetector( 52 container as Context, 53 this, 54 recentsView.pagedOrientationHandler.upDownSwipeDirection, 55 ) 56 private val isRtl = isRtl(container.resources) 57 private val downDirection = recentsView.pagedOrientationHandler.getDownDirection(isRtl) 58 59 private var taskBeingDragged: TaskView? = null 60 private var launchEndDisplacement: Float = 0f 61 private var playbackController: AnimatorPlaybackController? = null 62 private var verticalFactor: Int = 0 63 64 private fun canTaskLaunchTaskView(taskView: TaskView?) = 65 taskView != null && 66 taskView === recentsView.currentPageTaskView && 67 DisplayController.getNavigationMode(container).hasGestures && 68 (!recentsView.showAsGrid() || taskView.isLargeTile) && 69 recentsView.isTaskInExpectedScrollPosition(taskView) 70 71 private fun canInterceptTouch(ev: MotionEvent): Boolean = 72 when { 73 // Don't intercept swipes on the nav bar, as user might be trying to go home during a 74 // task dismiss animation. 75 (ev.edgeFlags and EDGE_NAV_BAR) != 0 -> { 76 debugLog(TAG, "Not intercepting edge swipe on nav bar.") 77 false 78 } 79 80 // Floating views that a TouchController should not try to intercept touches from. 81 AbstractFloatingView.getTopOpenViewWithType( 82 container, 83 AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT, 84 ) != null -> { 85 debugLog(TAG, "Not intercepting, open floating view blocking touch.") 86 false 87 } 88 89 // Disable swiping if the task overlay is modal. 90 taskViewRecentsTouchContext.isRecentsModal -> { 91 debugLog(TAG, "Not intercepting touch in modal overlay.") 92 false 93 } 94 95 else -> 96 taskViewRecentsTouchContext.isRecentsInteractive.also { isRecentsInteractive -> 97 if (!isRecentsInteractive) { 98 debugLog(TAG, "Not intercepting touch, recents not interactive.") 99 } 100 } 101 } 102 103 override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean { 104 if ( 105 (ev.action == MotionEvent.ACTION_UP || ev.action == MotionEvent.ACTION_CANCEL) && 106 playbackController == null 107 ) { 108 clearState() 109 } 110 if (ev.action == MotionEvent.ACTION_DOWN) { 111 if (!onActionDown(ev)) { 112 clearState() 113 return false 114 } 115 } 116 onControllerTouchEvent(ev) 117 val downDirectionIsNegative = downDirection == SingleAxisSwipeDetector.DIRECTION_NEGATIVE 118 val wasInitialTouchDown = 119 (downDirectionIsNegative && !detector.wasInitialTouchPositive()) || 120 (!downDirectionIsNegative && detector.wasInitialTouchPositive()) 121 return detector.isDraggingState && wasInitialTouchDown 122 } 123 124 override fun onControllerTouchEvent(ev: MotionEvent) = detector.onTouchEvent(ev) 125 126 private fun onActionDown(ev: MotionEvent): Boolean { 127 if (!canInterceptTouch(ev)) { 128 return false 129 } 130 taskBeingDragged = 131 recentsView.taskViews 132 .firstOrNull { 133 recentsView.isTaskViewVisible(it) && container.dragLayer.isEventOverView(it, ev) 134 } 135 ?.also { 136 verticalFactor = 137 recentsView.pagedOrientationHandler.getTaskDragDisplacementFactor(isRtl) 138 } 139 if (!canTaskLaunchTaskView(taskBeingDragged)) { 140 debugLog(TAG, "Not intercepting touch, task cannot be launched.") 141 return false 142 } 143 detector.setDetectableScrollConditions(downDirection, /* ignoreSlop= */ false) 144 return true 145 } 146 147 override fun onDragStart(start: Boolean, startDisplacement: Float) { 148 val taskBeingDragged = taskBeingDragged ?: return 149 debugLog(TAG, "Handling touch event.") 150 151 val secondaryLayerDimension: Int = 152 recentsView.pagedOrientationHandler.getSecondaryDimension(container.getDragLayer()) 153 val maxDuration = 2L * secondaryLayerDimension 154 recentsView.clearPendingAnimation() 155 val pendingAnimation = 156 recentsView.createTaskLaunchAnimation(taskBeingDragged, maxDuration, ZOOM_IN) 157 // Since the thumbnail is what is filling the screen, based the end displacement on it. 158 taskBeingDragged.getThumbnailBounds(tempRect, /* relativeToDragLayer= */ true) 159 launchEndDisplacement = 160 recentsView.pagedOrientationHandler 161 .getTaskLaunchLength(secondaryLayerDimension, tempRect) 162 .toFloat() * verticalFactor 163 playbackController = 164 pendingAnimation.createPlaybackController()?.apply { 165 taskViewRecentsTouchContext.onUserControlledAnimationCreated(this) 166 dispatchOnStart() 167 } 168 } 169 170 override fun onDrag(displacement: Float): Boolean { 171 playbackController?.setPlayFraction( 172 boundToRange(displacement / launchEndDisplacement, 0f, 1f) 173 ) 174 return true 175 } 176 177 override fun onDragEnd(velocity: Float) { 178 val playbackController = playbackController ?: return 179 180 val isBeyondLaunchThreshold = 181 abs(playbackController.progressFraction) > abs(LAUNCH_THRESHOLD_FRACTION) 182 val velocityIsNegative = !recentsView.pagedOrientationHandler.isGoingUp(velocity, isRtl) 183 val isFlingingTowardsLaunch = detector.isFling(velocity) && velocityIsNegative 184 val isFlingingTowardsRestState = detector.isFling(velocity) && !velocityIsNegative 185 val isLaunching = 186 isFlingingTowardsLaunch || (isBeyondLaunchThreshold && !isFlingingTowardsRestState) 187 188 val progress = playbackController.progressFraction 189 var animationDuration = 190 BaseSwipeDetector.calculateDuration( 191 velocity, 192 if (isLaunching) (1 - progress) else progress, 193 ) 194 if (detector.isFling(velocity) && flingBlockCheck.isBlocked && !isLaunching) { 195 animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity).toLong() 196 } 197 198 playbackController.setEndAction(this::clearState) 199 playbackController.startWithVelocity( 200 container, 201 isLaunching, 202 velocity, 203 launchEndDisplacement, 204 animationDuration, 205 ) 206 } 207 208 private fun clearState() { 209 detector.finishedScrolling() 210 detector.setDetectableScrollConditions(0, false) 211 taskBeingDragged = null 212 playbackController = null 213 } 214 215 companion object { 216 private const val TAG = "TaskViewLaunchTouchController" 217 private const val LAUNCH_THRESHOLD_FRACTION: Float = 0.5f 218 } 219 } 220