Merge "Add swipe-to-dismiss support to PhoneWindow." into klp-modular-dev

This commit is contained in:
Will Haldean Brown
2014-03-04 21:22:30 +00:00
committed by Android (Google) Code Review
14 changed files with 495 additions and 2 deletions

View File

@@ -2850,6 +2850,7 @@ package android.app {
method public void onUserInteraction();
method protected void onUserLeaveHint();
method public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams);
method public void onWindowDismissed();
method public void onWindowFocusChanged(boolean);
method public android.view.ActionMode onWindowStartingActionMode(android.view.ActionMode.Callback);
method public void openContextMenu(android.view.View);
@@ -3366,6 +3367,7 @@ package android.app {
method public boolean onTouchEvent(android.view.MotionEvent);
method public boolean onTrackballEvent(android.view.MotionEvent);
method public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams);
method public void onWindowDismissed();
method public void onWindowFocusChanged(boolean);
method public android.view.ActionMode onWindowStartingActionMode(android.view.ActionMode.Callback);
method public void openContextMenu(android.view.View);
@@ -22701,6 +22703,7 @@ package android.service.dreams {
method public boolean onPreparePanel(int, android.view.View, android.view.Menu);
method public boolean onSearchRequested();
method public void onWindowAttributesChanged(android.view.WindowManager.LayoutParams);
method public void onWindowDismissed();
method public void onWindowFocusChanged(boolean);
method public android.view.ActionMode onWindowStartingActionMode(android.view.ActionMode.Callback);
method public void setContentView(int);
@@ -28648,6 +28651,7 @@ package android.view {
field public static final int FEATURE_OPTIONS_PANEL = 0; // 0x0
field public static final int FEATURE_PROGRESS = 2; // 0x2
field public static final int FEATURE_RIGHT_ICON = 4; // 0x4
field public static final int FEATURE_SWIPE_TO_DISMISS = 11; // 0xb
field public static final int ID_ANDROID_CONTENT = 16908290; // 0x1020002
field public static final int PROGRESS_END = 10000; // 0x2710
field public static final int PROGRESS_INDETERMINATE_OFF = -4; // 0xfffffffc
@@ -28679,6 +28683,7 @@ package android.view {
method public abstract boolean onPreparePanel(int, android.view.View, android.view.Menu);
method public abstract boolean onSearchRequested();
method public abstract void onWindowAttributesChanged(android.view.WindowManager.LayoutParams);
method public abstract void onWindowDismissed();
method public abstract void onWindowFocusChanged(boolean);
method public abstract android.view.ActionMode onWindowStartingActionMode(android.view.ActionMode.Callback);
}

View File

@@ -2402,6 +2402,13 @@ public class Activity extends ContextThemeWrapper
}
return false;
}
/**
* Called when the main window associated with the activity has been dismissed.
*/
public void onWindowDismissed() {
finish();
}
/**
* Called to process key events. You can override this to intercept all

View File

@@ -695,6 +695,10 @@ public class Dialog implements DialogInterface, Window.Callback,
public void onDetachedFromWindow() {
}
public void onWindowDismissed() {
dismiss();
}
/**
* Called to process key events. You can override this to intercept all

View File

@@ -300,6 +300,10 @@ public class DreamService extends Service implements Window.Callback {
public void onDetachedFromWindow() {
}
@Override
public void onWindowDismissed() {
}
/** {@inheritDoc} */
@Override
public void onPanelClosed(int featureId, Menu menu) {

View File

@@ -90,11 +90,16 @@ public abstract class Window {
*/
public static final int FEATURE_ACTION_MODE_OVERLAY = 10;
/**
* Flag for requesting a decoration-free window that is dismissed by swiping from the left.
*/
public static final int FEATURE_SWIPE_TO_DISMISS = 11;
/**
* Max value used as a feature ID
* @hide
*/
public static final int FEATURE_MAX = FEATURE_ACTION_MODE_OVERLAY;
public static final int FEATURE_MAX = FEATURE_SWIPE_TO_DISMISS;
/** Flag for setting the progress bar's visibility to VISIBLE */
public static final int PROGRESS_VISIBILITY_ON = -1;
@@ -385,6 +390,12 @@ public abstract class Window {
* @param mode The mode that was just finished.
*/
public void onActionModeFinished(ActionMode mode);
/**
* Called when a window is dismissed. This informs the callback that the
* window is gone, and it should finish itself.
*/
public void onWindowDismissed();
}
public Window(Context context) {

View File

@@ -0,0 +1,282 @@
/*
* Copyright (C) 2014 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.internal.widget;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
/**
* Special layout that finishes its activity when swiped away.
*/
public class SwipeDismissLayout extends FrameLayout {
private static final String TAG = "SwipeDismissLayout";
private static final float TRANSLATION_MIN_ALPHA = 0.5f;
public interface OnDismissedListener {
void onDismissed(SwipeDismissLayout layout);
}
public interface OnSwipeProgressChangedListener {
/**
* Called when the layout has been swiped and the position of the window should change.
*
* @param progress A number in [-1, 1] representing how far to the left
* or right the window has been swiped. Negative values are swipes
* left, and positives are right.
* @param translate A number in [-w, w], where w is the width of the
* layout. This is equivalent to progress * layout.getWidth().
*/
void onSwipeProgressChanged(SwipeDismissLayout layout, float progress, float translate);
void onSwipeCancelled(SwipeDismissLayout layout);
}
// Cached ViewConfiguration and system-wide constant values
private int mSlop;
private int mMinFlingVelocity;
private int mMaxFlingVelocity;
private long mAnimationTime;
private TimeInterpolator mCancelInterpolator;
private TimeInterpolator mDismissInterpolator;
// Transient properties
private int mActiveTouchId;
private float mDownX;
private float mDownY;
private boolean mSwiping;
private boolean mDismissed;
private boolean mDiscardIntercept;
private VelocityTracker mVelocityTracker;
private float mTranslationX;
private OnDismissedListener mDismissedListener;
private OnSwipeProgressChangedListener mProgressListener;
public SwipeDismissLayout(Context context) {
super(context);
init(context);
}
public SwipeDismissLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
private void init(Context context) {
ViewConfiguration vc = ViewConfiguration.get(getContext());
mSlop = vc.getScaledTouchSlop();
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 16;
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
mAnimationTime = getContext().getResources().getInteger(
android.R.integer.config_shortAnimTime);
mCancelInterpolator = new DecelerateInterpolator(1.5f);
mDismissInterpolator = new AccelerateInterpolator(1.5f);
}
public void setOnDismissedListener(OnDismissedListener listener) {
mDismissedListener = listener;
}
public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) {
mProgressListener = listener;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// offset because the view is translated during swipe
ev.offsetLocation(mTranslationX, 0);
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
resetMembers();
mDownX = ev.getRawX();
mDownY = ev.getRawY();
mActiveTouchId = ev.getPointerId(0);
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
resetMembers();
break;
case MotionEvent.ACTION_MOVE:
if (mVelocityTracker == null || mDiscardIntercept) {
break;
}
int pointerIndex = ev.findPointerIndex(mActiveTouchId);
float dx = ev.getRawX() - mDownX;
float x = ev.getX(pointerIndex);
float y = ev.getY(pointerIndex);
if (dx != 0 && canScroll(this, false, dx, x, y)) {
mDiscardIntercept = true;
break;
}
updateSwiping(ev);
break;
}
return !mDiscardIntercept && mSwiping;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mVelocityTracker == null) {
return super.onTouchEvent(ev);
}
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_UP:
updateDismiss(ev);
if (mDismissed) {
dismiss();
} else if (mSwiping) {
cancel();
}
resetMembers();
break;
case MotionEvent.ACTION_CANCEL:
cancel();
resetMembers();
break;
case MotionEvent.ACTION_MOVE:
mVelocityTracker.addMovement(ev);
updateSwiping(ev);
updateDismiss(ev);
if (mSwiping) {
setProgress(ev.getRawX() - mDownX);
break;
}
}
return true;
}
private void setProgress(float deltaX) {
mTranslationX = deltaX;
if (mProgressListener != null) {
mProgressListener.onSwipeProgressChanged(this, deltaX / getWidth(), deltaX);
}
}
private void dismiss() {
if (mDismissedListener != null) {
mDismissedListener.onDismissed(this);
}
}
protected void cancel() {
if (mProgressListener != null) {
mProgressListener.onSwipeCancelled(this);
}
}
/**
* Resets internal members when canceling.
*/
private void resetMembers() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
}
mVelocityTracker = null;
mTranslationX = 0;
mDownX = 0;
mDownY = 0;
mSwiping = false;
mDismissed = false;
mDiscardIntercept = false;
}
private void updateSwiping(MotionEvent ev) {
if (!mSwiping) {
float deltaX = ev.getRawX() - mDownX;
float deltaY = ev.getRawY() - mDownY;
mSwiping = deltaX > mSlop * 2 && Math.abs(deltaY) < mSlop * 2;
}
}
private void updateDismiss(MotionEvent ev) {
if (!mDismissed) {
mVelocityTracker.addMovement(ev);
mVelocityTracker.computeCurrentVelocity(1000);
float deltaX = ev.getRawX() - mDownX;
float velocityX = mVelocityTracker.getXVelocity();
float absVelocityX = Math.abs(velocityX);
float absVelocityY = Math.abs(mVelocityTracker.getYVelocity());
if (deltaX > getWidth() / 2) {
mDismissed = true;
} else if (absVelocityX >= mMinFlingVelocity
&& absVelocityX <= mMaxFlingVelocity
&& absVelocityY < absVelocityX / 2
&& velocityX > 0
&& deltaX > 0) {
mDismissed = true;
}
}
}
/**
* Tests scrollability within child views of v in the direction of dx.
*
* @param v View to test for horizontal scrollability
* @param checkV Whether the view v passed should itself be checked for scrollability (true),
* or just its children (false).
* @param dx Delta scrolled in pixels. Only the sign of this is used.
* @param x X coordinate of the active touch point
* @param y Y coordinate of the active touch point
* @return true if child views of v can be scrolled by delta of dx.
*/
protected boolean canScroll(View v, boolean checkV, float dx, float x, float y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = v.getScrollX();
final int scrollY = v.getScrollY();
final int count = group.getChildCount();
for (int i = count - 1; i >= 0; i--) {
final View child = group.getChildAt(i);
if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
canScroll(child, true, dx, x + scrollX - child.getLeft(),
y + scrollY - child.getTop())) {
return true;
}
}
}
return checkV && v.canScrollHorizontally((int) -dx);
}
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
** Copyright 2007, 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.
*/
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@interpolator/decelerate_quad" >
<alpha android:fromAlpha="0.0" android:toAlpha="1.0"
android:fillEnabled="true" android:fillBefore="true"
android:fillAfter="true"
android:duration="@android:integer/config_activityDefaultDur" />
</set>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
** Copyright 2007, 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.
*/
-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@interpolator/decelerate_quad" >
<translate android:fromXDelta="0%" android:toXDelta="100%"
android:fillEnabled="true" android:fillBefore="true"
android:fillAfter="true"
android:duration="400" />
</set>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 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.
-->
<!--
This is a layout for a window whose resident activity is finished when swiped away.
-->
<com.android.internal.widget.SwipeDismissLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/content"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>

View File

@@ -447,6 +447,10 @@
to {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION}. -->
<attr name="windowTranslucentNavigation" format="boolean" />
<!-- Flag to indicate that a window can be swiped away to be dismissed.
Corresponds to {@link android.view.Window.FEATURE_SWIPE_TO_DISMISS} -->
<attr name="windowSwipeToDismiss" format="boolean" />
<!-- ============ -->
<!-- Alert Dialog styles -->
<!-- ============ -->
@@ -1604,6 +1608,7 @@
<attr name="windowCloseOnTouchOutside" />
<attr name="windowTranslucentStatus" />
<attr name="windowTranslucentNavigation" />
<attr name="windowSwipeToDismiss" />
<!-- The minimum width the window is allowed to be, along the major
axis of the screen. That is, when in landscape. Can be either
an absolute dimension or a fraction of the screen size in that

View File

@@ -225,6 +225,14 @@ please see styles_device_defaults.xml.
<item name="windowExitAnimation">@anim/fast_fade_out</item>
</style>
<!-- Window animations for swipe-dismissable windows. {@hide} -->
<style name="Animation.SwipeDismiss">
<item name="taskOpenEnterAnimation">@anim/swipe_window_enter</item>
<item name="taskOpenExitAnimation">@anim/swipe_window_exit</item>
<item name="taskCloseEnterAnimation">@anim/swipe_window_enter</item>
<item name="taskCloseExitAnimation">@anim/swipe_window_exit</item>
</style>
<!-- Status Bar Styles -->
<style name="TextAppearance.StatusBar">
<item name="android:textAppearance">?android:attr/textAppearanceSmall</item>

View File

@@ -1374,6 +1374,7 @@
<java-symbol type="layout" name="screen_progress" />
<java-symbol type="layout" name="screen_simple" />
<java-symbol type="layout" name="screen_simple_overlay_action_mode" />
<java-symbol type="layout" name="screen_swipe_dismiss" />
<java-symbol type="layout" name="screen_title" />
<java-symbol type="layout" name="screen_title_icons" />
<java-symbol type="string" name="system_ui_date_pattern" />

View File

@@ -16,22 +16,42 @@
<resources>
<style name="Theme.Micro" parent="Theme.Holo">
<item name="numberPickerStyle">@android:style/Widget.Micro.NumberPicker</item>
<item name="windowAnimationStyle">@android:style/Animation.SwipeDismiss</item>
<item name="windowIsFloating">false</item>
<item name="windowIsTranslucent">true</item>
<item name="windowSwipeToDismiss">true</item>
</style>
<style name="Theme.Micro.NoActionBar" parent="Theme.Holo.NoActionBar">
<item name="textViewStyle">@android:style/Widget.Micro.TextView</item>
<item name="numberPickerStyle">@android:style/Widget.Micro.NumberPicker</item>
<item name="windowAnimationStyle">@android:style/Animation.SwipeDismiss</item>
<item name="windowIsFloating">false</item>
<item name="windowIsTranslucent">true</item>
<item name="windowSwipeToDismiss">true</item>
</style>
<style name="Theme.Micro.Light" parent="Theme.Holo.Light">
<item name="numberPickerStyle">@android:style/Widget.Micro.NumberPicker</item>
<item name="windowAnimationStyle">@android:style/Animation.SwipeDismiss</item>
<item name="windowIsFloating">false</item>
<item name="windowIsTranslucent">true</item>
<item name="windowSwipeToDismiss">true</item>
</style>
<style name="Theme.Micro.Light.NoActionBar" parent="Theme.Holo.Light.NoActionBar">
<item name="textViewStyle">@android:style/Widget.Micro.TextView</item>
<item name="numberPickerStyle">@android:style/Widget.Micro.NumberPicker</item>
<item name="windowAnimationStyle">@android:style/Animation.SwipeDismiss</item>
<item name="windowIsFloating">false</item>
<item name="windowIsTranslucent">true</item>
<item name="windowSwipeToDismiss">true</item>
</style>
<style name="Theme.Micro.Light.DarkActionBar" parent="Theme.Holo.Light.DarkActionBar">
<item name="textViewStyle">@android:style/Widget.Micro.TextView</item>
<item name="numberPickerStyle">@android:style/Widget.Micro.NumberPicker</item>
<item name="windowAnimationStyle">@android:style/Animation.SwipeDismiss</item>
<item name="windowIsFloating">false</item>
<item name="windowIsTranslucent">true</item>
<item name="windowSwipeToDismiss">true</item>
</style>
</resources>

View File

@@ -38,6 +38,7 @@ import com.android.internal.widget.ActionBarContainer;
import com.android.internal.widget.ActionBarContextView;
import com.android.internal.widget.ActionBarOverlayLayout;
import com.android.internal.widget.ActionBarView;
import com.android.internal.widget.SwipeDismissLayout;
import android.app.KeyguardManager;
import android.content.Context;
@@ -267,6 +268,15 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
// Remove the action bar feature if we have no title. No title dominates.
removeFeature(FEATURE_ACTION_BAR);
}
if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_SWIPE_TO_DISMISS) {
throw new AndroidRuntimeException(
"You cannot combine swipe dismissal and the action bar.");
}
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0 && featureId == FEATURE_ACTION_BAR) {
throw new AndroidRuntimeException(
"You cannot combine swipe dismissal and the action bar.");
}
return super.requestFeature(featureId);
}
@@ -2838,6 +2848,10 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
requestFeature(FEATURE_ACTION_MODE_OVERLAY);
}
if (a.getBoolean(com.android.internal.R.styleable.Window_windowSwipeToDismiss, false)) {
requestFeature(FEATURE_SWIPE_TO_DISMISS);
}
if (a.getBoolean(com.android.internal.R.styleable.Window_windowFullscreen, false)) {
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
}
@@ -2964,7 +2978,9 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = com.android.internal.R.layout.screen_swipe_dismiss;
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
@@ -3034,6 +3050,10 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
}
}
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
registerSwipeCallbacks();
}
// Remaining setup -- of background and title -- that only applies
// to top-level windows.
if (getContainer() == null) {
@@ -3390,6 +3410,53 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
return (mRightIconView = (ImageView)findViewById(com.android.internal.R.id.right_icon));
}
private void registerSwipeCallbacks() {
SwipeDismissLayout swipeDismiss =
(SwipeDismissLayout) findViewById(com.android.internal.R.id.content);
swipeDismiss.setOnDismissedListener(new SwipeDismissLayout.OnDismissedListener() {
@Override
public void onDismissed(SwipeDismissLayout layout) {
Callback cb = getCallback();
if (cb != null) {
try {
cb.onWindowDismissed();
} catch (AbstractMethodError e) {
Log.e(TAG, "onWindowDismissed not implemented in " +
cb.getClass().getSimpleName(), e);
}
}
}
});
swipeDismiss.setOnSwipeProgressChangedListener(
new SwipeDismissLayout.OnSwipeProgressChangedListener() {
private boolean mIsTranslucent = false;
@Override
public void onSwipeProgressChanged(
SwipeDismissLayout layout, float progress, float translate) {
WindowManager.LayoutParams newParams = getAttributes();
newParams.x = (int) translate;
setAttributes(newParams);
int flags = 0;
if (newParams.x == 0) {
flags = FLAG_FULLSCREEN;
} else {
flags = FLAG_LAYOUT_NO_LIMITS;
}
setFlags(flags, FLAG_FULLSCREEN | FLAG_LAYOUT_NO_LIMITS);
}
@Override
public void onSwipeCancelled(SwipeDismissLayout layout) {
WindowManager.LayoutParams newParams = getAttributes();
newParams.x = 0;
setAttributes(newParams);
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN | FLAG_LAYOUT_NO_LIMITS);
}
});
}
/**
* Helper method for calling the {@link Callback#onPanelClosed(int, Menu)}
* callback. This method will grab whatever extra state is needed for the