• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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