| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.quickstep; |
| |
| import static android.view.MotionEvent.ACTION_CANCEL; |
| import static android.view.MotionEvent.ACTION_DOWN; |
| import static android.view.MotionEvent.ACTION_MOVE; |
| import static android.view.MotionEvent.ACTION_POINTER_DOWN; |
| import static android.view.MotionEvent.ACTION_UP; |
| |
| import android.content.res.Resources; |
| import android.graphics.Point; |
| import android.graphics.RectF; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| import android.view.Surface; |
| |
| import com.android.launcher3.R; |
| import com.android.launcher3.ResourceUtils; |
| import com.android.launcher3.util.DisplayController.Info; |
| |
| import java.io.PrintWriter; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| /** |
| * Maintains state for supporting nav bars and tracking their gestures in multiple orientations. |
| * See {@link OrientationRectF#applyTransformToRotation(MotionEvent, int, boolean)} for |
| * transformation of MotionEvents from one orientation's coordinate space to another's. |
| * |
| * This class only supports single touch/pointer gesture tracking for touches started in a supported |
| * nav bar region. |
| */ |
| class OrientationTouchTransformer { |
| |
| private static class CurrentDisplay { |
| public Point size; |
| public int rotation; |
| |
| CurrentDisplay() { |
| this.size = new Point(0, 0); |
| this.rotation = 0; |
| } |
| |
| CurrentDisplay(Point size, int rotation) { |
| this.size = size; |
| this.rotation = rotation; |
| } |
| |
| @Override |
| public String toString() { |
| return "CurrentDisplay:" |
| + " rotation: " + rotation |
| + " size: " + size; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| |
| CurrentDisplay display = (CurrentDisplay) o; |
| if (rotation != display.rotation) return false; |
| |
| return Objects.equals(size, display.size); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(size, rotation); |
| } |
| }; |
| |
| private static final String TAG = "OrientationTouchTransformer"; |
| private static final boolean DEBUG = false; |
| |
| private static final int QUICKSTEP_ROTATION_UNINITIALIZED = -1; |
| |
| private final Map<CurrentDisplay, OrientationRectF> mSwipeTouchRegions = |
| new HashMap<CurrentDisplay, OrientationRectF>(); |
| private final RectF mAssistantLeftRegion = new RectF(); |
| private final RectF mAssistantRightRegion = new RectF(); |
| private final RectF mOneHandedModeRegion = new RectF(); |
| private CurrentDisplay mCurrentDisplay = new CurrentDisplay(); |
| private int mNavBarGesturalHeight; |
| private final int mNavBarLargerGesturalHeight; |
| private boolean mEnableMultipleRegions; |
| private Resources mResources; |
| private OrientationRectF mLastRectTouched; |
| /** |
| * The rotation of the last touched nav bar, whether that be through the last region the user |
| * touched down on or valid rotation user turned their device to. |
| * Note this is different than |
| * {@link #mQuickStepStartingRotation} as it always updates its value on every touch whereas |
| * mQuickstepStartingRotation only updates when device rotation matches touch rotation. |
| */ |
| private int mActiveTouchRotation; |
| private SysUINavigationMode.Mode mMode; |
| private QuickStepContractInfo mContractInfo; |
| |
| /** |
| * Represents if we're currently in a swipe "session" of sorts. If value is |
| * QUICKSTEP_ROTATION_UNINITIALIZED, then user has not tapped on an active nav region. |
| * Otherwise it will be the rotation of the display when the user first interacted with the |
| * active nav bar region. |
| * The "session" ends when {@link #enableMultipleRegions(boolean, Info)} is |
| * called - usually from a timeout or if user starts interacting w/ the foreground app. |
| * |
| * This is different than {@link #mLastRectTouched} as it can get reset by the system whereas |
| * the rect is purely used for tracking touch interactions and usually this "session" will |
| * outlast the touch interaction. |
| */ |
| private int mQuickStepStartingRotation = QUICKSTEP_ROTATION_UNINITIALIZED; |
| |
| /** For testability */ |
| interface QuickStepContractInfo { |
| float getWindowCornerRadius(); |
| } |
| |
| |
| OrientationTouchTransformer(Resources resources, SysUINavigationMode.Mode mode, |
| QuickStepContractInfo contractInfo) { |
| mResources = resources; |
| mMode = mode; |
| mContractInfo = contractInfo; |
| mNavBarGesturalHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE); |
| mNavBarLargerGesturalHeight = ResourceUtils.getDimenByName( |
| ResourceUtils.NAVBAR_BOTTOM_GESTURE_LARGER_SIZE, resources, |
| mNavBarGesturalHeight); |
| } |
| |
| private void refreshTouchRegion(Info info, Resources newRes) { |
| // Swipe touch regions are independent of nav mode, so we have to clear them explicitly |
| // here to avoid, for ex, a nav region for 2-button rotation 0 being used for 3-button mode |
| // It tries to cache and reuse swipe regions whenever possible based only on rotation |
| mResources = newRes; |
| mSwipeTouchRegions.clear(); |
| resetSwipeRegions(info); |
| } |
| |
| void setNavigationMode(SysUINavigationMode.Mode newMode, Info info, Resources newRes) { |
| if (DEBUG) { |
| Log.d(TAG, "setNavigationMode new: " + newMode + " oldMode: " + mMode + " " + this); |
| } |
| if (mMode == newMode) { |
| return; |
| } |
| this.mMode = newMode; |
| refreshTouchRegion(info, newRes); |
| } |
| |
| void setGesturalHeight(int newGesturalHeight, Info info, Resources newRes) { |
| if (mNavBarGesturalHeight == newGesturalHeight) { |
| return; |
| } |
| mNavBarGesturalHeight = newGesturalHeight; |
| refreshTouchRegion(info, newRes); |
| } |
| |
| /** |
| * Sets the current nav bar region to listen to events for as determined by |
| * {@param info}. If multiple nav bar regions are enabled, then this region will be added |
| * alongside other regions. |
| * Ok to call multiple times |
| * |
| * @see #enableMultipleRegions(boolean, Info) |
| */ |
| void createOrAddTouchRegion(Info info) { |
| mCurrentDisplay = new CurrentDisplay(info.currentSize, info.rotation); |
| |
| if (mQuickStepStartingRotation > QUICKSTEP_ROTATION_UNINITIALIZED |
| && mCurrentDisplay.rotation == mQuickStepStartingRotation) { |
| // User already was swiping and the current screen is same rotation as the starting one |
| // Remove active nav bars in other rotations except for the one we started out in |
| resetSwipeRegions(info); |
| return; |
| } |
| OrientationRectF region = mSwipeTouchRegions.get(mCurrentDisplay); |
| if (region != null) { |
| return; |
| } |
| |
| if (mEnableMultipleRegions) { |
| mSwipeTouchRegions.put(mCurrentDisplay, createRegionForDisplay(info)); |
| } else { |
| resetSwipeRegions(info); |
| } |
| } |
| |
| /** |
| * Call when we want to start tracking nav bar touch regions in multiple orientations. |
| * ALSO, you BETTER call this with {@param enableMultipleRegions} set to false once you're done. |
| * |
| * @param enableMultipleRegions Set to true to start tracking multiple nav bar regions |
| * @param info The current displayInfo which will be the start of the quickswitch gesture |
| */ |
| void enableMultipleRegions(boolean enableMultipleRegions, Info info) { |
| mEnableMultipleRegions = enableMultipleRegions && |
| mMode != SysUINavigationMode.Mode.TWO_BUTTONS; |
| if (mEnableMultipleRegions) { |
| mQuickStepStartingRotation = info.rotation; |
| } else { |
| mActiveTouchRotation = 0; |
| mQuickStepStartingRotation = QUICKSTEP_ROTATION_UNINITIALIZED; |
| } |
| resetSwipeRegions(info); |
| } |
| |
| /** |
| * Call when removing multiple regions to swipe from, but still in active quickswitch mode (task |
| * list is still frozen). |
| * Ex. This would be called when user has quickswitched to the same app rotation that |
| * they started quickswitching in, indicating that extra nav regions can be ignored. Calling |
| * this will update the value of {@link #mActiveTouchRotation} |
| * |
| * @param displayInfo The display whos rotation will be used as the current active rotation |
| */ |
| void setSingleActiveRegion(Info displayInfo) { |
| mActiveTouchRotation = displayInfo.rotation; |
| resetSwipeRegions(displayInfo); |
| } |
| |
| /** |
| * Only saves the swipe region represented by {@param region}, clears the |
| * rest from {@link #mSwipeTouchRegions} |
| * To be called whenever we want to stop tracking more than one swipe region. |
| * Ok to call multiple times. |
| */ |
| private void resetSwipeRegions(Info region) { |
| if (DEBUG) { |
| Log.d(TAG, "clearing all regions except rotation: " + mCurrentDisplay.rotation); |
| } |
| |
| mCurrentDisplay = new CurrentDisplay(region.currentSize, region.rotation); |
| OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplay); |
| if (regionToKeep == null) { |
| regionToKeep = createRegionForDisplay(region); |
| } |
| mSwipeTouchRegions.clear(); |
| mSwipeTouchRegions.put(mCurrentDisplay, regionToKeep); |
| updateAssistantRegions(regionToKeep); |
| } |
| |
| private void resetSwipeRegions() { |
| OrientationRectF regionToKeep = mSwipeTouchRegions.get(mCurrentDisplay); |
| mSwipeTouchRegions.clear(); |
| if (regionToKeep != null) { |
| mSwipeTouchRegions.put(mCurrentDisplay, regionToKeep); |
| updateAssistantRegions(regionToKeep); |
| } |
| } |
| |
| private OrientationRectF createRegionForDisplay(Info display) { |
| if (DEBUG) { |
| Log.d(TAG, "creating rotation region for: " + mCurrentDisplay.rotation |
| + " with mode: " + mMode + " displayRotation: " + display.rotation); |
| } |
| |
| Point size = display.currentSize; |
| int rotation = display.rotation; |
| int touchHeight = mNavBarGesturalHeight; |
| OrientationRectF orientationRectF = |
| new OrientationRectF(0, 0, size.x, size.y, rotation); |
| if (mMode == SysUINavigationMode.Mode.NO_BUTTON) { |
| orientationRectF.top = orientationRectF.bottom - touchHeight; |
| updateAssistantRegions(orientationRectF); |
| } else { |
| mAssistantLeftRegion.setEmpty(); |
| mAssistantRightRegion.setEmpty(); |
| int navbarSize = getNavbarSize(ResourceUtils.NAVBAR_LANDSCAPE_LEFT_RIGHT_SIZE); |
| switch (rotation) { |
| case Surface.ROTATION_90: |
| orientationRectF.left = orientationRectF.right |
| - navbarSize; |
| break; |
| case Surface.ROTATION_270: |
| orientationRectF.right = orientationRectF.left |
| + navbarSize; |
| break; |
| default: |
| orientationRectF.top = orientationRectF.bottom - touchHeight; |
| } |
| } |
| // One handed gestural only active on portrait mode |
| mOneHandedModeRegion.set(0, orientationRectF.bottom - mNavBarLargerGesturalHeight, |
| size.x, size.y); |
| |
| return orientationRectF; |
| } |
| |
| private void updateAssistantRegions(OrientationRectF orientationRectF) { |
| int navbarHeight = getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE); |
| int assistantWidth = mResources.getDimensionPixelSize(R.dimen.gestures_assistant_width); |
| float assistantHeight = Math.max(navbarHeight, mContractInfo.getWindowCornerRadius()); |
| mAssistantLeftRegion.bottom = mAssistantRightRegion.bottom = orientationRectF.bottom; |
| mAssistantLeftRegion.top = mAssistantRightRegion.top = |
| orientationRectF.bottom - assistantHeight; |
| |
| mAssistantLeftRegion.left = 0; |
| mAssistantLeftRegion.right = assistantWidth; |
| |
| mAssistantRightRegion.right = orientationRectF.right; |
| mAssistantRightRegion.left = orientationRectF.right - assistantWidth; |
| } |
| |
| boolean touchInAssistantRegion(MotionEvent ev) { |
| return mAssistantLeftRegion.contains(ev.getX(), ev.getY()) |
| || mAssistantRightRegion.contains(ev.getX(), ev.getY()); |
| |
| } |
| |
| boolean touchInOneHandedModeRegion(MotionEvent ev) { |
| return mOneHandedModeRegion.contains(ev.getX(), ev.getY()); |
| } |
| |
| private int getNavbarSize(String resName) { |
| return ResourceUtils.getNavbarSize(resName, mResources); |
| } |
| |
| boolean touchInValidSwipeRegions(float x, float y) { |
| if (DEBUG) { |
| Log.d(TAG, "touchInValidSwipeRegions " + x + "," + y + " in " |
| + mLastRectTouched + " this: " + this); |
| } |
| if (mLastRectTouched != null) { |
| return mLastRectTouched.contains(x, y); |
| } |
| return false; |
| } |
| |
| int getCurrentActiveRotation() { |
| return mActiveTouchRotation; |
| } |
| |
| int getQuickStepStartingRotation() { |
| return mQuickStepStartingRotation; |
| } |
| |
| public void transform(MotionEvent event) { |
| int eventAction = event.getActionMasked(); |
| switch (eventAction) { |
| case ACTION_MOVE: { |
| if (mLastRectTouched == null) { |
| return; |
| } |
| mLastRectTouched.applyTransformFromRotation(event, mCurrentDisplay.rotation, true); |
| break; |
| } |
| case ACTION_CANCEL: |
| case ACTION_UP: { |
| if (mLastRectTouched == null) { |
| return; |
| } |
| mLastRectTouched.applyTransformFromRotation(event, mCurrentDisplay.rotation, true); |
| mLastRectTouched = null; |
| break; |
| } |
| case ACTION_POINTER_DOWN: |
| case ACTION_DOWN: { |
| if (mLastRectTouched != null) { |
| return; |
| } |
| |
| for (OrientationRectF rect : mSwipeTouchRegions.values()) { |
| if (rect == null) { |
| continue; |
| } |
| if (rect.applyTransformFromRotation(event, mCurrentDisplay.rotation, false)) { |
| mLastRectTouched = rect; |
| mActiveTouchRotation = rect.getRotation(); |
| if (mEnableMultipleRegions |
| && mCurrentDisplay.rotation == mActiveTouchRotation) { |
| // TODO(b/154580671) might make this block unnecessary |
| // Start a touch session for the default nav region for the display |
| mQuickStepStartingRotation = mLastRectTouched.getRotation(); |
| resetSwipeRegions(); |
| } |
| if (DEBUG) { |
| Log.d(TAG, "set active region: " + rect); |
| } |
| return; |
| } |
| } |
| break; |
| } |
| } |
| } |
| |
| public void dump(PrintWriter pw) { |
| pw.println("OrientationTouchTransformerState: "); |
| pw.println(" currentActiveRotation=" + getCurrentActiveRotation()); |
| pw.println(" lastTouchedRegion=" + mLastRectTouched); |
| pw.println(" multipleRegionsEnabled=" + mEnableMultipleRegions); |
| StringBuilder regions = new StringBuilder(" currentTouchableRotations="); |
| for (CurrentDisplay key: mSwipeTouchRegions.keySet()) { |
| OrientationRectF rectF = mSwipeTouchRegions.get(key); |
| regions.append(rectF).append(" "); |
| } |
| pw.println(regions.toString()); |
| pw.println(" mNavBarGesturalHeight=" + mNavBarGesturalHeight); |
| pw.println(" mNavBarLargerGesturalHeight=" + mNavBarLargerGesturalHeight); |
| pw.println(" mOneHandedModeRegion=" + mOneHandedModeRegion); |
| } |
| } |