Loading quickstep/res/layout/back_gesture_tutorial_fragment.xml +0 −2 Original line number Diff line number Diff line Loading @@ -16,9 +16,7 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:layerType="software" android:background="@color/back_gesture_tutorial_background_color"> <!--The layout is rendered on the software layer to avoid b/136158117--> <ImageView android:id="@+id/back_gesture_tutorial_fragment_hand_coaching" Loading quickstep/res/values/colors.xml 0 → 100644 +18 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2020 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. --> <resources> <color name="back_arrow_color_dark">#99000000</color> </resources> No newline at end of file quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java +21 −8 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import android.widget.TextView; import com.android.launcher3.R; import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep; import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult; import java.util.Optional; Loading Loading @@ -79,22 +80,34 @@ abstract class BackGestureTutorialController { mHandCoachingAnimation.stop(); } void onGestureDetected() { hideHandCoachingAnimation(); if (mTutorialStep == TutorialStep.CONFIRM) { void onGestureAttempted(BackGestureResult result) { if (mTutorialStep == TutorialStep.CONFIRM && (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT)) { mFragment.closeTutorial(); return; } if (mTutorialTypeInfo.get().getTutorialType() == TutorialType.RIGHT_EDGE_BACK_NAVIGATION) { mFragment.changeController(TutorialStep.ENGAGED, TutorialType.LEFT_EDGE_BACK_NAVIGATION); if (!mTutorialTypeInfo.isPresent()) { return; } switch (mTutorialTypeInfo.get().getTutorialType()) { case RIGHT_EDGE_BACK_NAVIGATION: if (result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) { hideHandCoachingAnimation(); mFragment.changeController( TutorialStep.ENGAGED, TutorialType.LEFT_EDGE_BACK_NAVIGATION); } break; case LEFT_EDGE_BACK_NAVIGATION: if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT) { hideHandCoachingAnimation(); mFragment.changeController(TutorialStep.CONFIRM); } break; } } abstract Optional<Integer> getTitleStringId(); Loading quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java +27 −6 Original line number Diff line number Diff line Loading @@ -17,21 +17,26 @@ package com.android.quickstep.interaction; import android.content.ActivityNotFoundException; import android.content.Intent; import android.graphics.Insets; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import com.android.launcher3.R; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult; import java.net.URISyntaxException; import java.util.Optional; /** Shows the Back gesture interactive tutorial. */ public class BackGestureTutorialFragment extends Fragment { public class BackGestureTutorialFragment extends Fragment implements BackGestureAttemptCallback { private static final String LOG_TAG = "TutorialFragment"; private static final String KEY_TUTORIAL_STEP = "tutorialStep"; Loading @@ -47,6 +52,7 @@ public class BackGestureTutorialFragment extends Fragment { private Optional<BackGestureTutorialController> mTutorialController = Optional.empty(); private View mRootView; private BackGestureTutorialHandAnimation mHandCoachingAnimation; private EdgeBackGestureHandler mEdgeBackGestureHandler; public static BackGestureTutorialFragment newInstance( TutorialStep tutorialStep, TutorialType tutorialType) { Loading @@ -64,17 +70,25 @@ public class BackGestureTutorialFragment extends Fragment { Bundle args = savedInstanceState != null ? savedInstanceState : getArguments(); mTutorialStep = (TutorialStep) args.getSerializable(KEY_TUTORIAL_STEP); mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE); mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext()); mEdgeBackGestureHandler.registerBackGestureAttemptCallback(this); } @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); mRootView = inflater.inflate(R.layout.back_gesture_tutorial_fragment, container, /* attachToRoot= */ false); mRootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button) .setOnClickListener(this::onCloseButtonClicked); mRootView.setOnApplyWindowInsetsListener((view, insets) -> { Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars()); mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right); return insets; }); mRootView.setOnTouchListener(mEdgeBackGestureHandler); mHandCoachingAnimation = new BackGestureTutorialHandAnimation(getContext(), mRootView); return mRootView; Loading @@ -92,6 +106,14 @@ public class BackGestureTutorialFragment extends Fragment { mHandCoachingAnimation.stop(); } void onAttachedToWindow() { mEdgeBackGestureHandler.setIsEnabled(true); } void onDetachedFromWindow() { mEdgeBackGestureHandler.setIsEnabled(false); } @Override public void onSaveInstanceState(Bundle savedInstanceState) { savedInstanceState.putSerializable(KEY_TUTORIAL_STEP, mTutorialStep); Loading Loading @@ -125,10 +147,9 @@ public class BackGestureTutorialFragment extends Fragment { this.mTutorialType = tutorialType; } void onBackPressed() { if (mTutorialController.isPresent()) { mTutorialController.get().onGestureDetected(); } @Override public void onBackGestureAttempted(BackGestureResult result) { mTutorialController.ifPresent(controller -> controller.onGestureAttempted(result)); } void closeTutorial() { Loading quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java 0 → 100644 +274 −0 Original line number Diff line number Diff line /* * Copyright (C) 2020 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.interaction; import android.content.Context; import android.content.res.Resources; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.PointF; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; import android.os.Handler; import android.os.Looper; import android.os.SystemProperties; import android.view.Display; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewConfiguration; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import com.android.launcher3.ResourceUtils; /** * Utility class to handle edge swipes for back gestures. * * Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java. */ public class EdgeBackGestureHandler implements DisplayListener, OnTouchListener { private static final String TAG = "EdgeBackGestureHandler"; private static final int MAX_LONG_PRESS_TIMEOUT = SystemProperties.getInt( "gestures.back_timeout", 250); private final Context mContext; private final Point mDisplaySize = new Point(); private final int mDisplayId; // The edge width where touch down is allowed private int mEdgeWidth; // The bottom gesture area height private int mBottomGestureHeight; // The slop to distinguish between horizontal and vertical motion private final float mTouchSlop; // Duration after which we consider the event as longpress. private final int mLongPressTimeout; private final PointF mDownPoint = new PointF(); private boolean mThresholdCrossed = false; private boolean mAllowGesture = false; private boolean mIsEnabled; private int mLeftInset; private int mRightInset; private EdgeBackGesturePanel mEdgeBackPanel; private BackGestureAttemptCallback mGestureCallback; private final EdgeBackGesturePanel.BackCallback mBackCallback = new EdgeBackGesturePanel.BackCallback() { @Override public void triggerBack() { if (mGestureCallback != null) { mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel() ? BackGestureResult.BACK_COMPLETED_FROM_LEFT : BackGestureResult.BACK_COMPLETED_FROM_RIGHT); } } @Override public void cancelBack() { if (mGestureCallback != null) { mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel() ? BackGestureResult.BACK_CANCELLED_FROM_LEFT : BackGestureResult.BACK_CANCELLED_FROM_RIGHT); } } }; EdgeBackGestureHandler(Context context) { final Resources res = context.getResources(); mContext = context; mDisplayId = context.getDisplay() == null ? Display.DEFAULT_DISPLAY : context.getDisplay().getDisplayId(); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT, ViewConfiguration.getLongPressTimeout()); mBottomGestureHeight = ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, res); mEdgeWidth = ResourceUtils.getNavbarSize("config_backGestureInset", res); } void setIsEnabled(boolean isEnabled) { if (isEnabled == mIsEnabled) { return; } mIsEnabled = isEnabled; if (mEdgeBackPanel != null) { mEdgeBackPanel.onDestroy(); mEdgeBackPanel = null; } if (!mIsEnabled) { mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this); } else { updateDisplaySize(); mContext.getSystemService(DisplayManager.class).registerDisplayListener(this, new Handler(Looper.getMainLooper())); // Add a nav bar panel window. mEdgeBackPanel = new EdgeBackGesturePanel(mContext); mEdgeBackPanel.setBackCallback(mBackCallback); mEdgeBackPanel.setLayoutParams(createLayoutParams()); updateDisplaySize(); } } void registerBackGestureAttemptCallback(BackGestureAttemptCallback callback) { mGestureCallback = callback; } private WindowManager.LayoutParams createLayoutParams() { Resources resources = mContext.getResources(); WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( ResourceUtils.getNavbarSize("navigation_edge_panel_width", resources), ResourceUtils.getNavbarSize("navigation_edge_panel_height", resources), LayoutParams.TYPE_APPLICATION_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT); layoutParams.setTitle(TAG + mDisplayId); layoutParams.windowAnimations = 0; layoutParams.setFitInsetsTypes(0 /* types */); return layoutParams; } @Override public boolean onTouch(View view, MotionEvent motionEvent) { if (mIsEnabled) { onMotionEvent(motionEvent); return true; } return false; } private boolean isWithinTouchRegion(int x, int y) { // Disallow if too far from the edge if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) { return false; } // Disallow if we are in the bottom gesture area if (y >= (mDisplaySize.y - mBottomGestureHeight)) { return false; } return true; } private void cancelGesture(MotionEvent ev) { // Send action cancel to reset all the touch events mAllowGesture = false; MotionEvent cancelEv = MotionEvent.obtain(ev); cancelEv.setAction(MotionEvent.ACTION_CANCEL); mEdgeBackPanel.onMotionEvent(cancelEv); cancelEv.recycle(); } private void onMotionEvent(MotionEvent ev) { int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { boolean isOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset; mAllowGesture = isWithinTouchRegion((int) ev.getX(), (int) ev.getY()); if (mAllowGesture) { mEdgeBackPanel.setIsLeftPanel(isOnLeftEdge); mEdgeBackPanel.onMotionEvent(ev); mDownPoint.set(ev.getX(), ev.getY()); mThresholdCrossed = false; } } else if (mAllowGesture) { if (!mThresholdCrossed) { if (action == MotionEvent.ACTION_POINTER_DOWN) { // We do not support multi touch for back gesture cancelGesture(ev); return; } else if (action == MotionEvent.ACTION_MOVE) { if ((ev.getEventTime() - ev.getDownTime()) > mLongPressTimeout) { cancelGesture(ev); return; } float dx = Math.abs(ev.getX() - mDownPoint.x); float dy = Math.abs(ev.getY() - mDownPoint.y); if (dy > dx && dy > mTouchSlop) { cancelGesture(ev); return; } else if (dx > dy && dx > mTouchSlop) { mThresholdCrossed = true; } } } // forward touch mEdgeBackPanel.onMotionEvent(ev); } if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { if (!mAllowGesture && mGestureCallback != null) { mGestureCallback.onBackGestureAttempted(BackGestureResult.BACK_NOT_STARTED); } } } @Override public void onDisplayAdded(int displayId) { } @Override public void onDisplayRemoved(int displayId) { } @Override public void onDisplayChanged(int displayId) { if (displayId == mDisplayId) { updateDisplaySize(); } } private void updateDisplaySize() { mContext.getDisplay().getRealSize(mDisplaySize); if (mEdgeBackPanel != null) { mEdgeBackPanel.setDisplaySize(mDisplaySize); } } void setInsets(int leftInset, int rightInset) { mLeftInset = leftInset; mRightInset = rightInset; } enum BackGestureResult { UNKNOWN, BACK_COMPLETED_FROM_LEFT, BACK_COMPLETED_FROM_RIGHT, BACK_CANCELLED_FROM_LEFT, BACK_CANCELLED_FROM_RIGHT, BACK_NOT_STARTED, } /** Callback to let the UI react to attempted back gestures. */ interface BackGestureAttemptCallback { /** Called whenever any touch is completed. */ void onBackGestureAttempted(BackGestureResult result); } } Loading
quickstep/res/layout/back_gesture_tutorial_fragment.xml +0 −2 Original line number Diff line number Diff line Loading @@ -16,9 +16,7 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:layerType="software" android:background="@color/back_gesture_tutorial_background_color"> <!--The layout is rendered on the software layer to avoid b/136158117--> <ImageView android:id="@+id/back_gesture_tutorial_fragment_hand_coaching" Loading
quickstep/res/values/colors.xml 0 → 100644 +18 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2020 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. --> <resources> <color name="back_arrow_color_dark">#99000000</color> </resources> No newline at end of file
quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java +21 −8 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import android.widget.TextView; import com.android.launcher3.R; import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialStep; import com.android.quickstep.interaction.BackGestureTutorialFragment.TutorialType; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult; import java.util.Optional; Loading Loading @@ -79,22 +80,34 @@ abstract class BackGestureTutorialController { mHandCoachingAnimation.stop(); } void onGestureDetected() { hideHandCoachingAnimation(); if (mTutorialStep == TutorialStep.CONFIRM) { void onGestureAttempted(BackGestureResult result) { if (mTutorialStep == TutorialStep.CONFIRM && (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT)) { mFragment.closeTutorial(); return; } if (mTutorialTypeInfo.get().getTutorialType() == TutorialType.RIGHT_EDGE_BACK_NAVIGATION) { mFragment.changeController(TutorialStep.ENGAGED, TutorialType.LEFT_EDGE_BACK_NAVIGATION); if (!mTutorialTypeInfo.isPresent()) { return; } switch (mTutorialTypeInfo.get().getTutorialType()) { case RIGHT_EDGE_BACK_NAVIGATION: if (result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) { hideHandCoachingAnimation(); mFragment.changeController( TutorialStep.ENGAGED, TutorialType.LEFT_EDGE_BACK_NAVIGATION); } break; case LEFT_EDGE_BACK_NAVIGATION: if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT) { hideHandCoachingAnimation(); mFragment.changeController(TutorialStep.CONFIRM); } break; } } abstract Optional<Integer> getTitleStringId(); Loading
quickstep/src/com/android/quickstep/interaction/BackGestureTutorialFragment.java +27 −6 Original line number Diff line number Diff line Loading @@ -17,21 +17,26 @@ package com.android.quickstep.interaction; import android.content.ActivityNotFoundException; import android.content.Intent; import android.graphics.Insets; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import com.android.launcher3.R; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback; import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult; import java.net.URISyntaxException; import java.util.Optional; /** Shows the Back gesture interactive tutorial. */ public class BackGestureTutorialFragment extends Fragment { public class BackGestureTutorialFragment extends Fragment implements BackGestureAttemptCallback { private static final String LOG_TAG = "TutorialFragment"; private static final String KEY_TUTORIAL_STEP = "tutorialStep"; Loading @@ -47,6 +52,7 @@ public class BackGestureTutorialFragment extends Fragment { private Optional<BackGestureTutorialController> mTutorialController = Optional.empty(); private View mRootView; private BackGestureTutorialHandAnimation mHandCoachingAnimation; private EdgeBackGestureHandler mEdgeBackGestureHandler; public static BackGestureTutorialFragment newInstance( TutorialStep tutorialStep, TutorialType tutorialType) { Loading @@ -64,17 +70,25 @@ public class BackGestureTutorialFragment extends Fragment { Bundle args = savedInstanceState != null ? savedInstanceState : getArguments(); mTutorialStep = (TutorialStep) args.getSerializable(KEY_TUTORIAL_STEP); mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE); mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext()); mEdgeBackGestureHandler.registerBackGestureAttemptCallback(this); } @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); mRootView = inflater.inflate(R.layout.back_gesture_tutorial_fragment, container, /* attachToRoot= */ false); mRootView.findViewById(R.id.back_gesture_tutorial_fragment_close_button) .setOnClickListener(this::onCloseButtonClicked); mRootView.setOnApplyWindowInsetsListener((view, insets) -> { Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars()); mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right); return insets; }); mRootView.setOnTouchListener(mEdgeBackGestureHandler); mHandCoachingAnimation = new BackGestureTutorialHandAnimation(getContext(), mRootView); return mRootView; Loading @@ -92,6 +106,14 @@ public class BackGestureTutorialFragment extends Fragment { mHandCoachingAnimation.stop(); } void onAttachedToWindow() { mEdgeBackGestureHandler.setIsEnabled(true); } void onDetachedFromWindow() { mEdgeBackGestureHandler.setIsEnabled(false); } @Override public void onSaveInstanceState(Bundle savedInstanceState) { savedInstanceState.putSerializable(KEY_TUTORIAL_STEP, mTutorialStep); Loading Loading @@ -125,10 +147,9 @@ public class BackGestureTutorialFragment extends Fragment { this.mTutorialType = tutorialType; } void onBackPressed() { if (mTutorialController.isPresent()) { mTutorialController.get().onGestureDetected(); } @Override public void onBackGestureAttempted(BackGestureResult result) { mTutorialController.ifPresent(controller -> controller.onGestureAttempted(result)); } void closeTutorial() { Loading
quickstep/src/com/android/quickstep/interaction/EdgeBackGestureHandler.java 0 → 100644 +274 −0 Original line number Diff line number Diff line /* * Copyright (C) 2020 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.interaction; import android.content.Context; import android.content.res.Resources; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.PointF; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; import android.os.Handler; import android.os.Looper; import android.os.SystemProperties; import android.view.Display; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewConfiguration; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import com.android.launcher3.ResourceUtils; /** * Utility class to handle edge swipes for back gestures. * * Forked from platform/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java. */ public class EdgeBackGestureHandler implements DisplayListener, OnTouchListener { private static final String TAG = "EdgeBackGestureHandler"; private static final int MAX_LONG_PRESS_TIMEOUT = SystemProperties.getInt( "gestures.back_timeout", 250); private final Context mContext; private final Point mDisplaySize = new Point(); private final int mDisplayId; // The edge width where touch down is allowed private int mEdgeWidth; // The bottom gesture area height private int mBottomGestureHeight; // The slop to distinguish between horizontal and vertical motion private final float mTouchSlop; // Duration after which we consider the event as longpress. private final int mLongPressTimeout; private final PointF mDownPoint = new PointF(); private boolean mThresholdCrossed = false; private boolean mAllowGesture = false; private boolean mIsEnabled; private int mLeftInset; private int mRightInset; private EdgeBackGesturePanel mEdgeBackPanel; private BackGestureAttemptCallback mGestureCallback; private final EdgeBackGesturePanel.BackCallback mBackCallback = new EdgeBackGesturePanel.BackCallback() { @Override public void triggerBack() { if (mGestureCallback != null) { mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel() ? BackGestureResult.BACK_COMPLETED_FROM_LEFT : BackGestureResult.BACK_COMPLETED_FROM_RIGHT); } } @Override public void cancelBack() { if (mGestureCallback != null) { mGestureCallback.onBackGestureAttempted(mEdgeBackPanel.getIsLeftPanel() ? BackGestureResult.BACK_CANCELLED_FROM_LEFT : BackGestureResult.BACK_CANCELLED_FROM_RIGHT); } } }; EdgeBackGestureHandler(Context context) { final Resources res = context.getResources(); mContext = context; mDisplayId = context.getDisplay() == null ? Display.DEFAULT_DISPLAY : context.getDisplay().getDisplayId(); mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT, ViewConfiguration.getLongPressTimeout()); mBottomGestureHeight = ResourceUtils.getNavbarSize(ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE, res); mEdgeWidth = ResourceUtils.getNavbarSize("config_backGestureInset", res); } void setIsEnabled(boolean isEnabled) { if (isEnabled == mIsEnabled) { return; } mIsEnabled = isEnabled; if (mEdgeBackPanel != null) { mEdgeBackPanel.onDestroy(); mEdgeBackPanel = null; } if (!mIsEnabled) { mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this); } else { updateDisplaySize(); mContext.getSystemService(DisplayManager.class).registerDisplayListener(this, new Handler(Looper.getMainLooper())); // Add a nav bar panel window. mEdgeBackPanel = new EdgeBackGesturePanel(mContext); mEdgeBackPanel.setBackCallback(mBackCallback); mEdgeBackPanel.setLayoutParams(createLayoutParams()); updateDisplaySize(); } } void registerBackGestureAttemptCallback(BackGestureAttemptCallback callback) { mGestureCallback = callback; } private WindowManager.LayoutParams createLayoutParams() { Resources resources = mContext.getResources(); WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( ResourceUtils.getNavbarSize("navigation_edge_panel_width", resources), ResourceUtils.getNavbarSize("navigation_edge_panel_height", resources), LayoutParams.TYPE_APPLICATION_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT); layoutParams.setTitle(TAG + mDisplayId); layoutParams.windowAnimations = 0; layoutParams.setFitInsetsTypes(0 /* types */); return layoutParams; } @Override public boolean onTouch(View view, MotionEvent motionEvent) { if (mIsEnabled) { onMotionEvent(motionEvent); return true; } return false; } private boolean isWithinTouchRegion(int x, int y) { // Disallow if too far from the edge if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) { return false; } // Disallow if we are in the bottom gesture area if (y >= (mDisplaySize.y - mBottomGestureHeight)) { return false; } return true; } private void cancelGesture(MotionEvent ev) { // Send action cancel to reset all the touch events mAllowGesture = false; MotionEvent cancelEv = MotionEvent.obtain(ev); cancelEv.setAction(MotionEvent.ACTION_CANCEL); mEdgeBackPanel.onMotionEvent(cancelEv); cancelEv.recycle(); } private void onMotionEvent(MotionEvent ev) { int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { boolean isOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset; mAllowGesture = isWithinTouchRegion((int) ev.getX(), (int) ev.getY()); if (mAllowGesture) { mEdgeBackPanel.setIsLeftPanel(isOnLeftEdge); mEdgeBackPanel.onMotionEvent(ev); mDownPoint.set(ev.getX(), ev.getY()); mThresholdCrossed = false; } } else if (mAllowGesture) { if (!mThresholdCrossed) { if (action == MotionEvent.ACTION_POINTER_DOWN) { // We do not support multi touch for back gesture cancelGesture(ev); return; } else if (action == MotionEvent.ACTION_MOVE) { if ((ev.getEventTime() - ev.getDownTime()) > mLongPressTimeout) { cancelGesture(ev); return; } float dx = Math.abs(ev.getX() - mDownPoint.x); float dy = Math.abs(ev.getY() - mDownPoint.y); if (dy > dx && dy > mTouchSlop) { cancelGesture(ev); return; } else if (dx > dy && dx > mTouchSlop) { mThresholdCrossed = true; } } } // forward touch mEdgeBackPanel.onMotionEvent(ev); } if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { if (!mAllowGesture && mGestureCallback != null) { mGestureCallback.onBackGestureAttempted(BackGestureResult.BACK_NOT_STARTED); } } } @Override public void onDisplayAdded(int displayId) { } @Override public void onDisplayRemoved(int displayId) { } @Override public void onDisplayChanged(int displayId) { if (displayId == mDisplayId) { updateDisplaySize(); } } private void updateDisplaySize() { mContext.getDisplay().getRealSize(mDisplaySize); if (mEdgeBackPanel != null) { mEdgeBackPanel.setDisplaySize(mDisplaySize); } } void setInsets(int leftInset, int rightInset) { mLeftInset = leftInset; mRightInset = rightInset; } enum BackGestureResult { UNKNOWN, BACK_COMPLETED_FROM_LEFT, BACK_COMPLETED_FROM_RIGHT, BACK_CANCELLED_FROM_LEFT, BACK_CANCELLED_FROM_RIGHT, BACK_NOT_STARTED, } /** Callback to let the UI react to attempted back gestures. */ interface BackGestureAttemptCallback { /** Called whenever any touch is completed. */ void onBackGestureAttempted(BackGestureResult result); } }