android P/Q/R/S 9/10/11/12 multi task gesture animation OverviewInputConsumer - Section 2
hi, multitasking gesture analyzes OtherActivity. In this section, we will analyze the source code and situation analysis of multitasking when the desktop itself is the foreground.
First, let's take a look at the two processes of multitasking on the native aosp:
Key phenomena:[
[introductory course, practical course, cross process project, input project]( https://ke.qq.com/course/package/51285?tuin=7d4eb354)
ps needs to study in-depth framework courses and course discounts
Please join qq group: 422901085 (get demo source code)
1. Slide your fingers slowly, and the workspace as a whole will also slide slowly
This process is the process of drawing on the bottom of our fingers. It is better to use a self drawn figure to show some:
That is to say, when the bottom of the finger is scratched up, the whole workspace will be scratched up slowly
2. When the upper stroke reaches a certain critical value, there is a direct animation process to enter the multi task
This process is relatively simple and easy to understand. There is no need to add additional pictures to explain a multi task interface
Key source code analysis:
1. workspace slowly up the process
First, the global touch listener listens to touch events. In this case, the OverviewInputConsumer listens to touch events
The specific path is in the following classes of the Launcher Code:
com/android/quickstep/inputconsumers/OverviewInputConsumer.java
public OverviewInputConsumer(T activity, @Nullable InputMonitorCompat inputMonitor, boolean startingInActivityBounds) { mActivity = activity; mInputMonitor = inputMonitor; mStartingInActivityBounds = startingInActivityBounds; mTarget = activity.getDragLayer(); if (startingInActivityBounds) { mEventReceiver = mTarget::dispatchTouchEvent; mProxyTouch = true; } else { // Only proxy touches to controllers if we are starting touch from nav bar. mEventReceiver = mTarget::proxyTouchEvent;//proxyTouchEvent is the key processing point here mTarget.getLocationOnScreen(mLocationOnScreen); mProxyTouch = mTarget.prepareProxyEventStarting(); } } @Override public int getType() { return TYPE_OVERVIEW; } @Override public boolean allowInterceptByParent() { return !mTargetHandledTouch; } @Override public void onMotionEvent(MotionEvent ev) { if (!mProxyTouch) { return; } int flags = ev.getEdgeFlags(); if (!mStartingInActivityBounds) { ev.setEdgeFlags(flags | Utilities.EDGE_NAV_BAR); } ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]); boolean handled = mEventReceiver.test(ev);//This will trigger the call to proxyTouchEvent ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]); ev.setEdgeFlags(flags); //ellipsis } //ellipsis }
After the onMotionEvent method is called, the proxyTouchEvent method is called to process
packages/apps/Trebuchet/src/com/android/launcher3/views/BaseDragLayer.java
/** * Proxies the touch events to the gesture handlers */ public boolean proxyTouchEvent(MotionEvent ev) { boolean handled; if (mProxyTouchController != null) {//Must be null at first handled = mProxyTouchController.onControllerTouchEvent(ev); } else { mProxyTouchController = findControllerToHandleTouch(ev);//Need to traverse to find the appropriate TouchController handled = mProxyTouchController != null; } int action = ev.getAction(); if (action == ACTION_UP || action == ACTION_CANCEL) { mProxyTouchController = null; mTouchDispatchState &= ~TOUCH_DISPATCHING_PROXY; } return handled; } //Here's a look at the findControllerToHandleTouch method private TouchController findControllerToHandleTouch(MotionEvent ev) { AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity); if (topView != null && topView.onControllerInterceptTouchEvent(ev)) { return topView; } for (TouchController controller : mControllers) { //Traverse to see which TouchController will be interested in this event and return who if (controller.onControllerInterceptTouchEvent(ev)) { return controller; } } return null; }
Here we mControllers are actually
packages/apps/Trebuchet/go/quickstep/src/com/android/launcher3/uioverrides/RecentsUiFactory.java
public static TouchController[] createTouchControllers(Launcher launcher) { ArrayList<TouchController> list = new ArrayList<>(); list.add(launcher.getDragController()); if (launcher.getDeviceProfile().isVerticalBarLayout()) { list.add(new LandscapeStatesTouchController(launcher)); list.add(new LandscapeEdgeSwipeController(launcher)); } else { boolean allowDragToOverview = SysUINavigationMode.INSTANCE.get(launcher) .getMode().hasGestures; list.add(new PortraitStatesTouchController(launcher, allowDragToOverview)); } if (FeatureFlags.PULL_DOWN_STATUS_BAR && Utilities.IS_DEBUG_DEVICE && !launcher.getDeviceProfile().isMultiWindowMode && !launcher.getDeviceProfile().isVerticalBarLayout()) { list.add(new StatusBarTouchController(launcher)); } return list.toArray(new TouchController[list.size()]); }
It can be seen here that we actually have a portaitstatestouchcontroller, so we consider entering its processing. However, the portaitstatestouchcontroller itself does not, but the AbstractStateChangeTouchController does:
packages/apps/Trebuchet/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java
@Override public final boolean onControllerInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { mNoIntercept = !canInterceptTouch(ev); if (mNoIntercept) { return false; } // Now figure out which direction scroll events the controller will start // calling the callbacks. final int directionsToDetectScroll; boolean ignoreSlopWhenSettling = false; if (mCurrentAnimation != null) { directionsToDetectScroll = SingleAxisSwipeDetector.DIRECTION_BOTH; ignoreSlopWhenSettling = true; } else { directionsToDetectScroll = getSwipeDirection(); if (directionsToDetectScroll == 0) { mNoIntercept = true; return false; } } mDetector.setDetectableScrollConditions( directionsToDetectScroll, ignoreSlopWhenSettling); } if (mNoIntercept) { return false; } onControllerTouchEvent(ev);//Here processing is the key return mDetector.isDraggingOrSettling();//Returns whether it is in the DRAGGING state } @Override public final boolean onControllerTouchEvent(MotionEvent ev) { return mDetector.onTouchEvent(ev);//onTouchEvent of mDetector called }
The final processing will call mDetector Ontouchevent, where mDetector is BaseSwipeDetector:
com/android/launcher3/touch/BaseSwipeDetector.java
The BaseSwipeDetector is the key point of the real touch event logic
public boolean onTouchEvent(MotionEvent ev) { int actionMasked = ev.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN && mVelocityTracker != null) { mVelocityTracker.clear(); } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); switch (actionMasked) { case MotionEvent.ACTION_DOWN://At first, down only did some initialization related work and recorded coordinates mActivePointerId = ev.getPointerId(0); mDownPos.set(ev.getX(), ev.getY()); mLastPos.set(mDownPos); mLastDisplacement.set(0, 0); mDisplacement.set(0, 0); if (mState == ScrollState.SETTLING && mIgnoreSlopWhenSettling) { setState(ScrollState.DRAGGING); } break; //Omitted part case MotionEvent.ACTION_MOVE://Start moving int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == INVALID_POINTER_ID) { break; } mDisplacement.set(ev.getX(pointerIndex) - mDownPos.x, ev.getY(pointerIndex) - mDownPos.y);//Record moving point if (mIsRtl) { mDisplacement.x = -mDisplacement.x; } // handle state and listener calls. if (mState != ScrollState.DRAGGING && shouldScrollStart(mDisplacement)) {//Start to see if it is possible to trigger the driving setState(ScrollState.DRAGGING); } if (mState == ScrollState.DRAGGING) { reportDragging(ev);//If you are already in DRAGGING and start sliding, you need to call reportDragging } mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: // These are synthetic events and there is no need to update internal values. if (mState == ScrollState.DRAGGING) { setState(ScrollState.SETTLING);//When lifted, it becomes the completed state } mVelocityTracker.recycle(); mVelocityTracker = null; break; default: break; } return true; }
Here is the key method setState:
private void setState(ScrollState newState) { if (newState == ScrollState.DRAGGING) {//Set to DRAGGING initializeDragging(); if (mState == ScrollState.IDLE) { reportDragStart(false /* recatch */);//Call reportDragStart } else if (mState == ScrollState.SETTLING) { reportDragStart(true /* recatch */); } } if (newState == ScrollState.SETTLING) { reportDragEnd();//Drag end } mState = newState; } private void reportDragStart(boolean recatch) { reportDragStartInternal(recatch); }
reportDragStartInternal is actually a method implemented by the subclass SingleAxisSwipeDetector:
com/android/launcher3/touch/SingleAxisSwipeDetector.java
@Override protected void reportDragStartInternal(boolean recatch) { mListener.onDragStart(!recatch); }
There's a transfer here
com/android/launcher3/touch/AbstractStateChangeTouchController.java
@Override public void onDragStart(boolean start) { mStartState = mLauncher.getStateManager().getState(); mIsLogContainerSet = false; if (mCurrentAnimation == null) { mFromState = mStartState; mToState = null; cancelAnimationControllers(); reinitCurrentAnimation(false, mDetector.wasInitialTouchPositive());//Start to initialize the animation, which is complex, mainly to set the Workspace animation mDisplacementShift = 0; } else { mCurrentAnimation.pause(); mStartProgress = mCurrentAnimation.getProgressFraction(); mAtomicAnimAutoPlayInfo = null; if (mAtomicComponentsController != null) { mAtomicComponentsController.pause(); } } mCanBlockFling = mFromState == NORMAL; mFlingBlockCheck.unblockFling(); }
The above analysis has completed the DragStart situation. What about the finger sliding process?
Then go back to reportDragging in onTouchEvent of BaseSwipeDetector
if (mState == ScrollState.DRAGGING) { reportDragging(ev); //If you are already in DRAGGING and start sliding, you need to call reportDragging }
Look here
private void reportDragging(MotionEvent event) { if (mDisplacement != mLastDisplacement) { if (DBG) { Log.d(TAG, String.format("onDrag disp=%s", mDisplacement)); } mLastDisplacement.set(mDisplacement); sTempPoint.set(mDisplacement.x - mSubtractDisplacement.x, mDisplacement.y - mSubtractDisplacement.y);//Transfer the specific sliding distance, and finally convert it to progress reportDraggingInternal(sTempPoint, event); } }
Here we go back to the reportDraggingInternal(sTempPoint, event) method, which calls the SingleAxisSwipeDetector
reportDraggingInternal:
@Override protected void reportDraggingInternal(PointF displacement, MotionEvent event) { mListener.onDrag(mDir.extractDirection(displacement), event); }
Here we call com/android/launcher3/touch/abstractstatechangetouchcontroller java
@Override public boolean onDrag(float displacement, MotionEvent ev) { if (!mIsLogContainerSet) { if (mStartState == ALL_APPS) { mStartContainerType = LauncherLogProto.ContainerType.ALLAPPS; } else if (mStartState == NORMAL) { mStartContainerType = getLogContainerTypeForNormalState(ev); } else if (mStartState == OVERVIEW) { mStartContainerType = LauncherLogProto.ContainerType.TASKSWITCHER; } mIsLogContainerSet = true; } return onDrag(displacement);//Finally, the corresponding overloaded method onDrag will be called } @Override public boolean onDrag(float displacement) { float deltaProgress = mProgressMultiplier * (displacement - mDisplacementShift); float progress = deltaProgress + mStartProgress; updateProgress(progress);//Here is to update the progress boolean isDragTowardPositive = mSwipeDirection.isPositive( displacement - mDisplacementShift); if (progress <= 0) { if (reinitCurrentAnimation(false, isDragTowardPositive)) { mDisplacementShift = displacement; if (mCanBlockFling) { mFlingBlockCheck.blockFling(); } } } else if (progress >= 1) { if (reinitCurrentAnimation(true, isDragTowardPositive)) { mDisplacementShift = displacement; if (mCanBlockFling) { mFlingBlockCheck.blockFling(); } } } else { mFlingBlockCheck.onEvent(); } return true; } protected void updateProgress(float fraction) { mCurrentAnimation.setPlayFraction(fraction);//In fact, the setPlayFraction of mCurrentAnimation animation is set here if (mAtomicComponentsController != null) { // Make sure we don't divide by 0, and have at least a small runway. float start = Math.min(mAtomicComponentsStartProgress, 0.9f); mAtomicComponentsController.setPlayFraction((fraction - start) / (1 - start)); } maybeUpdateAtomicAnim(mFromState, mToState, fraction); }
Mcurrentanimation setPlayFraction will finally call the setPlayFraction method of AnimatorPlaybackController
com/android/launcher3/anim/AnimatorPlaybackController.java
@Override public void setPlayFraction(float fraction) { mCurrentFraction = fraction; // Let the animator report the progress but don't apply the progress to child // animations if it has been cancelled. if (mTargetCancelled) { return; } long playPos = clampDuration(fraction); for (ValueAnimator anim : mChildAnimations) { anim.setCurrentPlayTime(Math.min(playPos, anim.getDuration())); } }
The mChildAnimations animation set is set by reinitCurrentAnimation in the previous onDragStart. The set contains several animations, including the mobile animation of the workspace. It will be traversed here, and then the animation time can be set to complete the animation of the workspace..
Does it feel good? If we write code, are we sure that we can directly call the ui to set some properties of the View at this time
2. Start the animation to enter multi task after the threshold value of progress is reached
Before analyzing updateProgress, there was also a maybeUpdateAtomicAnim method
private void maybeUpdateAtomicAnim(LauncherState fromState, LauncherState toState, float progress) { if (!goingBetweenNormalAndOverview(fromState, toState)) { return; } float threshold = toState == OVERVIEW ? ATOMIC_OVERVIEW_ANIM_THRESHOLD : 1f - ATOMIC_OVERVIEW_ANIM_THRESHOLD; boolean passedThreshold = progress >= threshold;//Judge whether the progress has reached the threshold progress requirements if (passedThreshold != mPassedOverviewAtomicThreshold) {//Arrival progress LauncherState atomicFromState = passedThreshold ? fromState: toState; LauncherState atomicToState = passedThreshold ? toState : fromState; mPassedOverviewAtomicThreshold = passedThreshold; if (mAtomicAnim != null) { mAtomicAnim.cancel(); } mAtomicAnim = createAtomicAnimForState(atomicFromState, atomicToState, ATOMIC_DURATION);//Create an atomic animation from NORMAL -- overviews //Omitted part mAtomicAnim.start();//Turn on atomic animation mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); } }