diff --git a/bin/dexedLibs/android-support-v4-ad222bec85930bfd3f22cbfb79715dcb.jar b/bin/dexedLibs/android-support-v4-ad222bec85930bfd3f22cbfb79715dcb.jar new file mode 100644 index 000000000..5316ddbe0 Binary files /dev/null and b/bin/dexedLibs/android-support-v4-ad222bec85930bfd3f22cbfb79715dcb.jar differ diff --git a/bin/jarlist.cache b/bin/jarlist.cache new file mode 100644 index 000000000..0565465f2 --- /dev/null +++ b/bin/jarlist.cache @@ -0,0 +1,3 @@ +# cache for current jar dependency. DO NOT EDIT. +# format is +# Encoding is UTF-8 diff --git a/src/com/hippo/ehviewer/UpdateHelper.java b/src/com/hippo/ehviewer/UpdateHelper.java new file mode 100644 index 000000000..bc980e281 --- /dev/null +++ b/src/com/hippo/ehviewer/UpdateHelper.java @@ -0,0 +1,147 @@ +package com.hippo.ehviewer; + +import java.io.File; +import java.net.MalformedURLException; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.view.View; + +import com.hippo.ehviewer.dialog.DialogBuilder; +import com.hippo.ehviewer.network.Downloader; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.EhClient; +import com.hippo.ehviewer.view.AlertButton; + +public class UpdateHelper { + private static final String TAG = "UpdateHelper"; + private Activity mActivity; + private String updateFileName; + private OnCheckUpdateListener listener; + + public UpdateHelper(Activity activity) { + mActivity = activity; + } + + public void checkUpdate() { + EhClient.checkUpdate(new EhClient.OnCheckUpdateListener() { + @Override + public void onSuccess(String pageContext) { + String[] items = pageContext.split("\n"); + if (items.length > 3) { + + if (listener != null) + listener.onSuccess(pageContext); + + String newVer = items[0]; + final String url = EhClient.UPDATE_URL + items[1]; + final String name = url.substring(url.lastIndexOf('/')+1); + updateFileName = name; + String size = items[2]; + String info = items[3]; + + AlertDialog dialog = new DialogBuilder(mActivity).setTitle(R.string.update) + .setMessage(String.format(mActivity.getString(R.string.update_message), newVer, size, info)) + .setNegativeButton(android.R.string.cancel, new View.OnClickListener() { + @Override + public void onClick(View v) { + ((AlertButton)v).dialog.dismiss(); + } + }).setPositiveButton(android.R.string.ok, new View.OnClickListener() { + @Override + public void onClick(View v) { + ((AlertButton)v).dialog.dismiss(); + Downloader d = new Downloader(); + try { + d.resetData(Config.getDownloadPath(), name, url); + d.setOnDownloadListener(new UpdateListener()); + new Thread(d).start(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + } + }).create(); + if (!mActivity.isFinishing()) + dialog.show(); + } else if(pageContext.equals("none")) { + if (listener != null) + listener.onNoUpdate(); + } else if(pageContext.equals("error")){ + if (listener != null) + listener.onFailure(R.string.em_request_error); + } else { + if (listener != null) + listener.onFailure(R.string.em_host_error); + } + } + + @Override + public void onFailure(int eMesgId) { + Log.d(TAG, mActivity.getString(eMesgId)); + if (listener != null) + listener.onFailure(eMesgId); + } + }); + } + + public UpdateHelper SetOnCheckUpdateListener(OnCheckUpdateListener listener) { + this.listener = listener; + return this; + } + + public interface OnCheckUpdateListener { + public void onSuccess(String pageContext); + public void onNoUpdate(); + public void onFailure(int eMesgId); + } + + private class UpdateListener implements Downloader.OnDownloadListener { + private NotificationManager mNotifyManager; + private NotificationCompat.Builder mBuilder; + + public UpdateListener() { + mNotifyManager = (NotificationManager) + mActivity.getSystemService(Context.NOTIFICATION_SERVICE); + mBuilder = new NotificationCompat.Builder(mActivity.getApplicationContext()); + mBuilder.setSmallIcon(R.drawable.ic_launcher); + } + + @Override + public void onDownloadStart(int totalSize) { + mBuilder.setContentTitle("正在下载更新") + .setContentText(null) + .setProgress(0, 0, true).setOngoing(true); + mNotifyManager.notify(233, mBuilder.build()); + } + @Override + public void onDownloadStatusUpdate( + int downloadSize, int totalSize) { + mBuilder.setContentTitle("正在下载更新") + .setContentText(String.format("%.2f / %.2f KB", downloadSize/1024.0f, totalSize/1024.0f)) + .setProgress(totalSize, downloadSize, false).setOngoing(true); + mNotifyManager.notify(233, mBuilder.build()); + + } + @Override + public void onDownloadOver(boolean ok, int eMesgId) { + if (ok) { + mNotifyManager.cancel(233); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(Uri.fromFile(new File(Config.getDownloadPath(), updateFileName)), + "application/vnd.android.package-archive"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mActivity.getApplicationContext().startActivity(intent); + } else { + mBuilder.setContentTitle("下载更新失败") + .setContentText(null) + .setProgress(0, 0, false).setOngoing(false); + } + } + } +} diff --git a/src/com/hippo/ehviewer/view/GetPaddingRelativeLayout.java b/src/com/hippo/ehviewer/view/GetPaddingRelativeLayout.java new file mode 100644 index 000000000..888a0703c --- /dev/null +++ b/src/com/hippo/ehviewer/view/GetPaddingRelativeLayout.java @@ -0,0 +1,46 @@ +package com.hippo.ehviewer.view; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.RelativeLayout; + +public class GetPaddingRelativeLayout extends RelativeLayout { + private static final String TAG = "GetPaddingRelativeLayout"; + private View mView1; + private View mView2; + + public GetPaddingRelativeLayout(Context context) { + super(context); + } + public GetPaddingRelativeLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + public GetPaddingRelativeLayout(Context context, AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + } + + public void setView(View view1, View view2) { + mView1 = view1; + mView2 = view2; + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + boolean re = super.fitSystemWindows(insets); + int pt = this.getPaddingTop(); + int pb = this.getPaddingBottom(); + if (mView1 != null) + mView1.setPadding(mView1.getPaddingLeft(), pt, mView1.getPaddingRight(), pb); + if (mView2 != null) + mView2.setPadding(mView2.getPaddingLeft(), pt, mView2.getPaddingRight(), pb); + return re; + } + + + + +} diff --git a/src/com/hippo/ehviewer/widget/DrawerLayout.java b/src/com/hippo/ehviewer/widget/DrawerLayout.java new file mode 100644 index 000000000..c0cf6082e --- /dev/null +++ b/src/com/hippo/ehviewer/widget/DrawerLayout.java @@ -0,0 +1,1649 @@ +/* + * Copyright (C) 2013 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.hippo.ehviewer.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.GravityCompat; +import android.support.v4.view.KeyEventCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewGroupCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; + +/** + * DrawerLayout acts as a top-level container for window content that allows for + * interactive "drawer" views to be pulled out from the edge of the window. + * + *

Drawer positioning and layout is controlled using the android:layout_gravity + * attribute on child views corresponding to which side of the view you want the drawer + * to emerge from: left or right. (Or start/end on platform versions that support layout direction.) + *

+ * + *

To use a DrawerLayout, position your primary content view as the first child with + * a width and height of match_parent. Add drawers as child views after the main + * content view and set the layout_gravity appropriately. Drawers commonly use + * match_parent for height with a fixed width.

+ * + *

{@link DrawerListener} can be used to monitor the state and motion of drawer views. + * Avoid performing expensive operations such as layout during animation as it can cause + * stuttering; try to perform expensive operations during the {@link #STATE_IDLE} state. + * {@link SimpleDrawerListener} offers default/no-op implementations of each callback method.

+ * + *

As per the Android Design guide, any drawers positioned to the left/start should + * always contain content for navigating around the application, whereas any drawers + * positioned to the right/end should always contain actions to take on the current content. + * This preserves the same navigation left, actions right structure present in the Action Bar + * and elsewhere.

+ */ +public class DrawerLayout extends ViewGroup { + private static final String TAG = "DrawerLayout"; + + /** + * Indicates that any drawers are in an idle, settled state. No animation is in progress. + */ + public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE; + + /** + * Indicates that a drawer is currently being dragged by the user. + */ + public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; + + /** + * Indicates that a drawer is in the process of settling to a final position. + */ + public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; + + /** + * The drawer is unlocked. + */ + public static final int LOCK_MODE_UNLOCKED = 0; + + /** + * The drawer is locked closed. The user may not open it, though + * the app may open it programmatically. + */ + public static final int LOCK_MODE_LOCKED_CLOSED = 1; + + /** + * The drawer is locked open. The user may not close it, though the app + * may close it programmatically. + */ + public static final int LOCK_MODE_LOCKED_OPEN = 2; + + private static final int MIN_DRAWER_MARGIN = 64; // dp + + private static final int DEFAULT_SCRIM_COLOR = 0x99000000; + + /** + * Length of time to delay before peeking the drawer. + */ + private static final int PEEK_DELAY = 160; // ms + + /** + * Minimum velocity that will be detected as a fling + */ + private static final int MIN_FLING_VELOCITY = 400; // dips per second + + /** + * Experimental feature. + */ + private static final boolean ALLOW_EDGE_LOCK = false; + + private static final boolean CHILDREN_DISALLOW_INTERCEPT = true; + + private static final float TOUCH_SLOP_SENSITIVITY = 1.f; + + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.layout_gravity + }; + + private int mMinDrawerMargin; + + private int mScrimColor = DEFAULT_SCRIM_COLOR; + private float mScrimOpacity; + private Paint mScrimPaint = new Paint(); + + private final ViewDragHelper mLeftDragger; + private final ViewDragHelper mRightDragger; + private final ViewDragCallback mLeftCallback; + private final ViewDragCallback mRightCallback; + private int mDrawerState; + private boolean mInLayout; + private boolean mFirstLayout = true; + private int mLockModeLeft; + private int mLockModeRight; + private boolean mDisallowInterceptRequested; + private boolean mChildrenCanceledTouch; + + private DrawerListener mListener; + + private float mInitialMotionX; + private float mInitialMotionY; + + private Drawable mShadowLeft; + private Drawable mShadowRight; + + private boolean enabled = true; + + /** + * Listener for monitoring events about drawers. + */ + public interface DrawerListener { + /** + * Called when a drawer's position changes. + * @param drawerView The child view that was moved + * @param slideOffset The new offset of this drawer within its range, from 0-1 + */ + public void onDrawerSlide(View drawerView, float slideOffset); + + /** + * Called when a drawer has settled in a completely open state. + * The drawer is interactive at this point. + * + * @param drawerView Drawer view that is now open + */ + public void onDrawerOpened(View drawerView); + + /** + * Called when a drawer has settled in a completely closed state. + * + * @param drawerView Drawer view that is now closed + */ + public void onDrawerClosed(View drawerView); + + /** + * Called when the drawer motion state changes. The new state will + * be one of {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. + * + * @param newState The new drawer motion state + */ + public void onDrawerStateChanged(int newState); + } + + /** + * Stub/no-op implementations of all methods of {@link DrawerListener}. + * Override this if you only care about a few of the available callback methods. + */ + public static abstract class SimpleDrawerListener implements DrawerListener { + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + } + + @Override + public void onDrawerOpened(View drawerView) { + } + + @Override + public void onDrawerClosed(View drawerView) { + } + + @Override + public void onDrawerStateChanged(int newState) { + } + } + + public DrawerLayout(Context context) { + this(context, null); + } + + public DrawerLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DrawerLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final float density = getResources().getDisplayMetrics().density; + mMinDrawerMargin = (int) (MIN_DRAWER_MARGIN * density + 0.5f); + final float minVel = MIN_FLING_VELOCITY * density; + + mLeftCallback = new ViewDragCallback(Gravity.LEFT); + mRightCallback = new ViewDragCallback(Gravity.RIGHT); + + mLeftDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mLeftCallback); + mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); + mLeftDragger.setMinVelocity(minVel); + mLeftCallback.setDragger(mLeftDragger); + + mRightDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mRightCallback); + mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT); + mRightDragger.setMinVelocity(minVel); + mRightCallback.setDragger(mRightDragger); + + // So that we can catch the back button + setFocusableInTouchMode(true); + + ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); + ViewGroupCompat.setMotionEventSplittingEnabled(this, false); + } + + /** + * Set a simple drawable used for the left or right shadow. + * The drawable provided must have a nonzero intrinsic width. + * + * @param shadowDrawable Shadow drawable to use at the edge of a drawer + * @param gravity Which drawer the shadow should apply to + */ + public void setDrawerShadow(Drawable shadowDrawable, int gravity) { + /* + * TODO Someone someday might want to set more complex drawables here. + * They're probably nuts, but we might want to consider registering callbacks, + * setting states, etc. properly. + */ + + final int absGravity = GravityCompat.getAbsoluteGravity(gravity, + ViewCompat.getLayoutDirection(this)); + if ((absGravity & Gravity.LEFT) == Gravity.LEFT) { + mShadowLeft = shadowDrawable; + invalidate(); + } + if ((absGravity & Gravity.RIGHT) == Gravity.RIGHT) { + mShadowRight = shadowDrawable; + invalidate(); + } + } + + /** + * Set a simple drawable used for the left or right shadow. + * The drawable provided must have a nonzero intrinsic width. + * + * @param resId Resource id of a shadow drawable to use at the edge of a drawer + * @param gravity Which drawer the shadow should apply to + */ + public void setDrawerShadow(int resId, int gravity) { + setDrawerShadow(getResources().getDrawable(resId), gravity); + } + + /** + * Set a color to use for the scrim that obscures primary content while a drawer is open. + * + * @param color Color to use in 0xAARRGGBB format. + */ + public void setScrimColor(int color) { + mScrimColor = color; + invalidate(); + } + + /** + * Set a listener to be notified of drawer events. + * + * @param listener Listener to notify when drawer events occur + * @see DrawerListener + */ + public void setDrawerListener(DrawerListener listener) { + mListener = listener; + } + + /** + * Enable or disable interaction with all drawers. + * + *

This allows the application to restrict the user's ability to open or close + * any drawer within this layout. DrawerLayout will still respond to calls to + * {@link #openDrawer(int)}, {@link #closeDrawer(int)} and friends if a drawer is locked.

+ * + *

Locking drawers open or closed will implicitly open or close + * any drawers as appropriate.

+ * + * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, + * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. + */ + public void setDrawerLockMode(int lockMode) { + setDrawerLockMode(lockMode, Gravity.LEFT); + setDrawerLockMode(lockMode, Gravity.RIGHT); + } + + /** + * Enable or disable interaction with the given drawer. + * + *

This allows the application to restrict the user's ability to open or close + * the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer(int)}, + * {@link #closeDrawer(int)} and friends if a drawer is locked.

+ * + *

Locking a drawer open or closed will implicitly open or close + * that drawer as appropriate.

+ * + * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, + * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. + * @param edgeGravity Gravity.LEFT, RIGHT, START or END. + * Expresses which drawer to change the mode for. + * + * @see #LOCK_MODE_UNLOCKED + * @see #LOCK_MODE_LOCKED_CLOSED + * @see #LOCK_MODE_LOCKED_OPEN + */ + public void setDrawerLockMode(int lockMode, int edgeGravity) { + final int absGravity = GravityCompat.getAbsoluteGravity(edgeGravity, + ViewCompat.getLayoutDirection(this)); + if (absGravity == Gravity.LEFT) { + mLockModeLeft = lockMode; + } else if (absGravity == Gravity.RIGHT) { + mLockModeRight = lockMode; + } + if (lockMode != LOCK_MODE_UNLOCKED) { + // Cancel interaction in progress + final ViewDragHelper helper = absGravity == Gravity.LEFT ? mLeftDragger : mRightDragger; + helper.cancel(); + } + switch (lockMode) { + case LOCK_MODE_LOCKED_OPEN: + final View toOpen = findDrawerWithGravity(absGravity); + if (toOpen != null) { + openDrawer(toOpen); + } + break; + case LOCK_MODE_LOCKED_CLOSED: + final View toClose = findDrawerWithGravity(absGravity); + if (toClose != null) { + closeDrawer(toClose); + } + break; + // default: do nothing + } + } + + /** + * Enable or disable interaction with the given drawer. + * + *

This allows the application to restrict the user's ability to open or close + * the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer(int)}, + * {@link #closeDrawer(int)} and friends if a drawer is locked.

+ * + *

Locking a drawer open or closed will implicitly open or close + * that drawer as appropriate.

+ * + * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, + * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. + * @param drawerView The drawer view to change the lock mode for + * + * @see #LOCK_MODE_UNLOCKED + * @see #LOCK_MODE_LOCKED_CLOSED + * @see #LOCK_MODE_LOCKED_OPEN + */ + public void setDrawerLockMode(int lockMode, View drawerView) { + if (!isDrawerView(drawerView)) { + throw new IllegalArgumentException("View " + drawerView + " is not a " + + "drawer with appropriate layout_gravity"); + } + final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; + setDrawerLockMode(lockMode, gravity); + } + + /** + * Check the lock mode of the drawer with the given gravity. + * + * @param edgeGravity Gravity of the drawer to check + * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or + * {@link #LOCK_MODE_LOCKED_OPEN}. + */ + public int getDrawerLockMode(int edgeGravity) { + final int absGravity = GravityCompat.getAbsoluteGravity( + edgeGravity, ViewCompat.getLayoutDirection(this)); + if (absGravity == Gravity.LEFT) { + return mLockModeLeft; + } else if (absGravity == Gravity.RIGHT) { + return mLockModeRight; + } + return LOCK_MODE_UNLOCKED; + } + + /** + * Check the lock mode of the given drawer view. + * + * @param drawerView Drawer view to check lock mode + * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or + * {@link #LOCK_MODE_LOCKED_OPEN}. + */ + public int getDrawerLockMode(View drawerView) { + final int absGravity = getDrawerViewAbsoluteGravity(drawerView); + if (absGravity == Gravity.LEFT) { + return mLockModeLeft; + } else if (absGravity == Gravity.RIGHT) { + return mLockModeRight; + } + return LOCK_MODE_UNLOCKED; + } + + /** + * Resolve the shared state of all drawers from the component ViewDragHelpers. + * Should be called whenever a ViewDragHelper's state changes. + */ + void updateDrawerState(int forGravity, int activeState, View activeDrawer) { + final int leftState = mLeftDragger.getViewDragState(); + final int rightState = mRightDragger.getViewDragState(); + + final int state; + if (leftState == STATE_DRAGGING || rightState == STATE_DRAGGING) { + state = STATE_DRAGGING; + } else if (leftState == STATE_SETTLING || rightState == STATE_SETTLING) { + state = STATE_SETTLING; + } else { + state = STATE_IDLE; + } + + if (activeDrawer != null && activeState == STATE_IDLE) { + final LayoutParams lp = (LayoutParams) activeDrawer.getLayoutParams(); + if (lp.onScreen == 0) { + dispatchOnDrawerClosed(activeDrawer); + } else if (lp.onScreen == 1) { + dispatchOnDrawerOpened(activeDrawer); + } + } + + if (state != mDrawerState) { + mDrawerState = state; + + if (mListener != null) { + mListener.onDrawerStateChanged(state); + } + } + } + + void dispatchOnDrawerClosed(View drawerView) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + if (lp.knownOpen) { + lp.knownOpen = false; + if (mListener != null) { + mListener.onDrawerClosed(drawerView); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + } + + void dispatchOnDrawerOpened(View drawerView) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + if (!lp.knownOpen) { + lp.knownOpen = true; + if (mListener != null) { + mListener.onDrawerOpened(drawerView); + } + drawerView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + } + + void dispatchOnDrawerSlide(View drawerView, float slideOffset) { + if (mListener != null) { + mListener.onDrawerSlide(drawerView, slideOffset); + } + } + + void setDrawerViewOffset(View drawerView, float slideOffset) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + if (slideOffset == lp.onScreen) { + return; + } + + lp.onScreen = slideOffset; + dispatchOnDrawerSlide(drawerView, slideOffset); + } + + float getDrawerViewOffset(View drawerView) { + return ((LayoutParams) drawerView.getLayoutParams()).onScreen; + } + + /** + * @return the absolute gravity of the child drawerView, resolved according + * to the current layout direction + */ + int getDrawerViewAbsoluteGravity(View drawerView) { + final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; + return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this)); + } + + boolean checkDrawerViewAbsoluteGravity(View drawerView, int checkFor) { + final int absGravity = getDrawerViewAbsoluteGravity(drawerView); + return (absGravity & checkFor) == checkFor; + } + + View findOpenDrawer() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (((LayoutParams) child.getLayoutParams()).knownOpen) { + return child; + } + } + return null; + } + + void moveDrawerToOffset(View drawerView, float slideOffset) { + final float oldOffset = getDrawerViewOffset(drawerView); + final int width = drawerView.getWidth(); + final int oldPos = (int) (width * oldOffset); + final int newPos = (int) (width * slideOffset); + final int dx = newPos - oldPos; + + drawerView.offsetLeftAndRight( + checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT) ? dx : -dx); + setDrawerViewOffset(drawerView, slideOffset); + } + + /** + * @param gravity the gravity of the child to return. If specified as a + * relative value, it will be resolved according to the current + * layout direction. + * @return the drawer with the specified gravity + */ + View findDrawerWithGravity(int gravity) { + final int absHorizGravity = GravityCompat.getAbsoluteGravity( + gravity, ViewCompat.getLayoutDirection(this)) & Gravity.HORIZONTAL_GRAVITY_MASK; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final int childAbsGravity = getDrawerViewAbsoluteGravity(child); + if ((childAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == absHorizGravity) { + return child; + } + } + return null; + } + + /** + * Simple gravity to string - only supports LEFT and RIGHT for debugging output. + * + * @param gravity Absolute gravity value + * @return LEFT or RIGHT as appropriate, or a hex string + */ + static String gravityToString(int gravity) { + if ((gravity & Gravity.LEFT) == Gravity.LEFT) { + return "LEFT"; + } + if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { + return "RIGHT"; + } + return Integer.toHexString(gravity); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mFirstLayout = true; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { + if (isInEditMode()) { + // Don't crash the layout editor. Consume all of the space if specified + // or pick a magic number from thin air otherwise. + // TODO Better communication with tools of this bogus state. + // It will crash on a real device. + if (widthMode == MeasureSpec.AT_MOST) { + widthMode = MeasureSpec.EXACTLY; + } else if (widthMode == MeasureSpec.UNSPECIFIED) { + widthMode = MeasureSpec.EXACTLY; + widthSize = 300; + } + if (heightMode == MeasureSpec.AT_MOST) { + heightMode = MeasureSpec.EXACTLY; + } + else if (heightMode == MeasureSpec.UNSPECIFIED) { + heightMode = MeasureSpec.EXACTLY; + heightSize = 300; + } + } else { + throw new IllegalArgumentException( + "DrawerLayout must be measured with MeasureSpec.EXACTLY."); + } + } + + setMeasuredDimension(widthSize, heightSize); + + // Gravity value for each drawer we've seen. Only one of each permitted. + int foundDrawers = 0; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (isContentView(child)) { + // Content views get measured at exactly the layout's size. + final int contentWidthSpec = MeasureSpec.makeMeasureSpec( + widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); + final int contentHeightSpec = MeasureSpec.makeMeasureSpec( + heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); + child.measure(contentWidthSpec, contentHeightSpec); + } else if (isDrawerView(child)) { + final int childGravity = + getDrawerViewAbsoluteGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK; + if ((foundDrawers & childGravity) != 0) { + throw new IllegalStateException("Child drawer has absolute gravity " + + gravityToString(childGravity) + " but this " + TAG + " already has a " + + "drawer view along that edge"); + } + final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, + mMinDrawerMargin + lp.leftMargin + lp.rightMargin, + lp.width); + final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec, + lp.topMargin + lp.bottomMargin, + lp.height); + child.measure(drawerWidthSpec, drawerHeightSpec); + } else { + throw new IllegalStateException("Child " + child + " at index " + i + + " does not have a valid layout_gravity - must be Gravity.LEFT, " + + "Gravity.RIGHT or Gravity.NO_GRAVITY"); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mInLayout = true; + final int width = r - l; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (isContentView(child)) { + child.layout(lp.leftMargin, lp.topMargin, + lp.leftMargin + child.getMeasuredWidth(), + lp.topMargin + child.getMeasuredHeight()); + } else { // Drawer, if it wasn't onMeasure would have thrown an exception. + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + int childLeft; + + final float newOffset; + if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + childLeft = -childWidth + (int) (childWidth * lp.onScreen); + newOffset = (float) (childWidth + childLeft) / childWidth; + } else { // Right; onMeasure checked for us. + childLeft = width - (int) (childWidth * lp.onScreen); + newOffset = (float) (width - childLeft) / childWidth; + } + + final boolean changeOffset = newOffset != lp.onScreen; + + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (vgrav) { + default: + case Gravity.TOP: { + child.layout(childLeft, lp.topMargin, childLeft + childWidth, + lp.topMargin + childHeight); + break; + } + + case Gravity.BOTTOM: { + final int height = b - t; + child.layout(childLeft, + height - lp.bottomMargin - child.getMeasuredHeight(), + childLeft + childWidth, + height - lp.bottomMargin); + break; + } + + case Gravity.CENTER_VERTICAL: { + final int height = b - t; + int childTop = (height - childHeight) / 2; + + // Offset for margins. If things don't fit right because of + // bad measurement before, oh well. + if (childTop < lp.topMargin) { + childTop = lp.topMargin; + } else if (childTop + childHeight > height - lp.bottomMargin) { + childTop = height - lp.bottomMargin - childHeight; + } + child.layout(childLeft, childTop, childLeft + childWidth, + childTop + childHeight); + break; + } + } + + if (changeOffset) { + setDrawerViewOffset(child, newOffset); + } + + final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE; + if (child.getVisibility() != newVisibility) { + child.setVisibility(newVisibility); + } + } + } + mInLayout = false; + mFirstLayout = false; + } + + @Override + public void requestLayout() { + if (!mInLayout) { + super.requestLayout(); + } + } + + @Override + public void computeScroll() { + final int childCount = getChildCount(); + float scrimOpacity = 0; + for (int i = 0; i < childCount; i++) { + final float onscreen = ((LayoutParams) getChildAt(i).getLayoutParams()).onScreen; + scrimOpacity = Math.max(scrimOpacity, onscreen); + } + mScrimOpacity = scrimOpacity; + + // "|" used on purpose; both need to run. + if (mLeftDragger.continueSettling(true) | mRightDragger.continueSettling(true)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private static boolean hasOpaqueBackground(View v) { + final Drawable bg = v.getBackground(); + if (bg != null) { + return bg.getOpacity() == PixelFormat.OPAQUE; + } + return false; + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + final int height = getHeight(); + final boolean drawingContent = isContentView(child); + int clipLeft = 0, clipRight = getWidth(); + + final int restoreCount = canvas.save(); + if (drawingContent) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + if (v == child || v.getVisibility() != VISIBLE || + !hasOpaqueBackground(v) || !isDrawerView(v) || + v.getHeight() < height) { + continue; + } + + if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) { + final int vright = v.getRight(); + if (vright > clipLeft) clipLeft = vright; + } else { + final int vleft = v.getLeft(); + if (vleft < clipRight) clipRight = vleft; + } + } + canvas.clipRect(clipLeft, 0, clipRight, getHeight()); + } + final boolean result = super.drawChild(canvas, child, drawingTime); + canvas.restoreToCount(restoreCount); + + if (mScrimOpacity > 0 && drawingContent) { + final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; + final int imag = (int) (baseAlpha * mScrimOpacity); + final int color = imag << 24 | (mScrimColor & 0xffffff); + mScrimPaint.setColor(color); + + canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint); + } else if (mShadowLeft != null && checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + final int shadowWidth = mShadowLeft.getIntrinsicWidth(); + final int childRight = child.getRight(); + final int drawerPeekDistance = mLeftDragger.getEdgeSize(); + final float alpha = + Math.max(0, Math.min((float) childRight / drawerPeekDistance, 1.f)); + mShadowLeft.setBounds(childRight, child.getTop(), + childRight + shadowWidth, child.getBottom()); + mShadowLeft.setAlpha((int) (0xff * alpha)); + mShadowLeft.draw(canvas); + } else if (mShadowRight != null && checkDrawerViewAbsoluteGravity(child, Gravity.RIGHT)) { + final int shadowWidth = mShadowRight.getIntrinsicWidth(); + final int childLeft = child.getLeft(); + final int showing = getWidth() - childLeft; + final int drawerPeekDistance = mRightDragger.getEdgeSize(); + final float alpha = + Math.max(0, Math.min((float) showing / drawerPeekDistance, 1.f)); + mShadowRight.setBounds(childLeft - shadowWidth, child.getTop(), + childLeft, child.getBottom()); + mShadowRight.setAlpha((int) (0xff * alpha)); + mShadowRight.draw(canvas); + } + return result; + } + + boolean isContentView(View child) { + return ((LayoutParams) child.getLayoutParams()).gravity == Gravity.NO_GRAVITY; + } + + boolean isDrawerView(View child) { + final int gravity = ((LayoutParams) child.getLayoutParams()).gravity; + final int absGravity = GravityCompat.getAbsoluteGravity(gravity, + ViewCompat.getLayoutDirection(child)); + return (absGravity & (Gravity.LEFT | Gravity.RIGHT)) != 0; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + + // "|" used deliberately here; both methods should be invoked. + final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) | + mRightDragger.shouldInterceptTouchEvent(ev); + + boolean interceptForTap = false; + boolean interceptForSlide = false; + + final float x = ev.getX(); + final float y = ev.getY(); + switch (action) { + case MotionEvent.ACTION_DOWN: { + mInitialMotionX = x; + mInitialMotionY = y; + if (mScrimOpacity > 0 && + isContentView(mLeftDragger.findTopChildUnder((int) x, (int) y))) { + interceptForTap = true; + } + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + break; + } + + case MotionEvent.ACTION_MOVE: { + // If we cross the touch slop, don't perform the delayed peek for an edge touch. + if (mLeftDragger.checkTouchSlop(ViewDragHelper.DIRECTION_ALL)) { + mLeftCallback.removeCallbacks(); + mRightCallback.removeCallbacks(); + } + + int xOffset = (int)(x - mInitialMotionX); + int yOffset = (int)Math.abs(y - mInitialMotionY); + xOffset = (xOffset == 0 ? 1 : xOffset); + yOffset = (yOffset == 0 ? 1 : yOffset); + float scale = (float)xOffset/yOffset; + + int centerX = (getLeft() + getRight())/2; + if (((findDrawerWithGravity(Gravity.START) != null) + && mInitialMotionX < centerX + && scale > 4) + ||((findDrawerWithGravity(Gravity.END) != null) + && mInitialMotionX > centerX + && scale < -4)) + interceptForSlide = true; + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + closeDrawers(true); + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + } + } + + return enabled && (interceptForDrag || interceptForSlide || interceptForTap || hasPeekingDrawer() || mChildrenCanceledTouch); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mLeftDragger.processTouchEvent(ev); + mRightDragger.processTouchEvent(ev); + + final int action = ev.getAction(); + boolean wantTouchEvents = true; + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + mInitialMotionX = x; + mInitialMotionY = y; + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + break; + } + + case MotionEvent.ACTION_UP: { + final float x = ev.getX(); + final float y = ev.getY(); + boolean peekingOnly = true; + final View touchedView = mLeftDragger.findTopChildUnder((int) x, (int) y); + if (touchedView != null && isContentView(touchedView)) { + final float dx = x - mInitialMotionX; + final float dy = y - mInitialMotionY; + final int slop = mLeftDragger.getTouchSlop(); + if (dx * dx + dy * dy < slop * slop) { + // Taps close a dimmed open drawer but only if it isn't locked open. + final View openDrawer = findOpenDrawer(); + if (openDrawer != null) { + peekingOnly = getDrawerLockMode(openDrawer) == LOCK_MODE_LOCKED_OPEN; + } + } + } + closeDrawers(peekingOnly); + mDisallowInterceptRequested = false; + break; + } + + case MotionEvent.ACTION_CANCEL: { + closeDrawers(true); + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + break; + } + } + + return wantTouchEvents; + } + + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (CHILDREN_DISALLOW_INTERCEPT || + (!mLeftDragger.isEdgeTouched(ViewDragHelper.EDGE_LEFT) && + !mRightDragger.isEdgeTouched(ViewDragHelper.EDGE_RIGHT))) { + // If we have an edge touch we want to skip this and track it for later instead. + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + mDisallowInterceptRequested = disallowIntercept; + if (disallowIntercept) { + closeDrawers(true); + } + } + + /** + * Close all currently open drawer views by animating them out of view. + */ + public void closeDrawers() { + closeDrawers(false); + } + + void closeDrawers(boolean peekingOnly) { + boolean needsInvalidate = false; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (!isDrawerView(child) || (peekingOnly && !lp.isPeeking)) { + continue; + } + + final int childWidth = child.getWidth(); + + if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + needsInvalidate |= mLeftDragger.smoothSlideViewTo(child, + -childWidth, child.getTop()); + } else { + needsInvalidate |= mRightDragger.smoothSlideViewTo(child, + getWidth(), child.getTop()); + } + + lp.isPeeking = false; + } + + mLeftCallback.removeCallbacks(); + mRightCallback.removeCallbacks(); + + if (needsInvalidate) { + invalidate(); + } + } + + /** + * Open the specified drawer view by animating it into view. + * + * @param drawerView Drawer view to open + */ + public void openDrawer(View drawerView) { + if (!isDrawerView(drawerView)) { + throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); + } + + if (mFirstLayout) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + lp.onScreen = 1.f; + lp.knownOpen = true; + } else { + if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { + mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop()); + } else { + mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(), + drawerView.getTop()); + } + } + invalidate(); + } + + /** + * Open the specified drawer by animating it out of view. + * + * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. + * GravityCompat.START or GravityCompat.END may also be used. + */ + public void openDrawer(int gravity) { + final View drawerView = findDrawerWithGravity(gravity); + if (drawerView == null) { + throw new IllegalArgumentException("No drawer view found with gravity " + + gravityToString(gravity)); + } + openDrawer(drawerView); + } + + /** + * Close the specified drawer view by animating it into view. + * + * @param drawerView Drawer view to close + */ + public void closeDrawer(View drawerView) { + if (!isDrawerView(drawerView)) { + throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); + } + + if (mFirstLayout) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + lp.onScreen = 0.f; + lp.knownOpen = false; + } else { + if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { + mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(), + drawerView.getTop()); + } else { + mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop()); + } + } + invalidate(); + } + + /** + * Close the specified drawer by animating it out of view. + * + * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. + * GravityCompat.START or GravityCompat.END may also be used. + */ + public void closeDrawer(int gravity) { + final View drawerView = findDrawerWithGravity(gravity); + if (drawerView == null) { + throw new IllegalArgumentException("No drawer view found with gravity " + + gravityToString(gravity)); + } + closeDrawer(drawerView); + } + + /** + * Check if the given drawer view is currently in an open state. + * To be considered "open" the drawer must have settled into its fully + * visible state. To check for partial visibility use + * {@link #isDrawerVisible(android.view.View)}. + * + * @param drawer Drawer view to check + * @return true if the given drawer view is in an open state + * @see #isDrawerVisible(android.view.View) + */ + public boolean isDrawerOpen(View drawer) { + if (!isDrawerView(drawer)) { + throw new IllegalArgumentException("View " + drawer + " is not a drawer"); + } + return ((LayoutParams) drawer.getLayoutParams()).knownOpen; + } + + /** + * Check if the given drawer view is currently in an open state. + * To be considered "open" the drawer must have settled into its fully + * visible state. If there is no drawer with the given gravity this method + * will return false. + * + * @param drawerGravity Gravity of the drawer to check + * @return true if the given drawer view is in an open state + */ + public boolean isDrawerOpen(int drawerGravity) { + final View drawerView = findDrawerWithGravity(drawerGravity); + if (drawerView != null) { + return isDrawerOpen(drawerView); + } + return false; + } + + /** + * Check if a given drawer view is currently visible on-screen. The drawer + * may be only peeking onto the screen, fully extended, or anywhere inbetween. + * + * @param drawer Drawer view to check + * @return true if the given drawer is visible on-screen + * @see #isDrawerOpen(android.view.View) + */ + public boolean isDrawerVisible(View drawer) { + if (!isDrawerView(drawer)) { + throw new IllegalArgumentException("View " + drawer + " is not a drawer"); + } + return ((LayoutParams) drawer.getLayoutParams()).onScreen > 0; + } + + /** + * Check if a given drawer view is currently visible on-screen. The drawer + * may be only peeking onto the screen, fully extended, or anywhere inbetween. + * If there is no drawer with the given gravity this method will return false. + * + * @param drawerGravity Gravity of the drawer to check + * @return true if the given drawer is visible on-screen + */ + public boolean isDrawerVisible(int drawerGravity) { + final View drawerView = findDrawerWithGravity(drawerGravity); + if (drawerView != null) { + return isDrawerVisible(drawerView); + } + return false; + } + + private boolean hasPeekingDrawer() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); + if (lp.isPeeking) { + return true; + } + } + return false; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams + ? new LayoutParams((LayoutParams) p) + : p instanceof ViewGroup.MarginLayoutParams + ? new LayoutParams((MarginLayoutParams) p) + : new LayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + private boolean hasVisibleDrawer() { + return findVisibleDrawer() != null; + } + + private View findVisibleDrawer() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (isDrawerView(child) && isDrawerVisible(child)) { + return child; + } + } + return null; + } + + void cancelChildViewTouch() { + // Cancel child touches + if (!mChildrenCanceledTouch) { + final long now = SystemClock.uptimeMillis(); + final MotionEvent cancelEvent = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + getChildAt(i).dispatchTouchEvent(cancelEvent); + } + cancelEvent.recycle(); + mChildrenCanceledTouch = true; + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) { + KeyEventCompat.startTracking(event); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + final View visibleDrawer = findVisibleDrawer(); + if (visibleDrawer != null && getDrawerLockMode(visibleDrawer) == LOCK_MODE_UNLOCKED) { + closeDrawers(); + } + return visibleDrawer != null; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + final SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (ss.openDrawerGravity != Gravity.NO_GRAVITY) { + final View toOpen = findDrawerWithGravity(ss.openDrawerGravity); + if (toOpen != null) { + openDrawer(toOpen); + } + } + + setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT); + setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + + final SavedState ss = new SavedState(superState); + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (!isDrawerView(child)) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.knownOpen) { + ss.openDrawerGravity = lp.gravity; + // Only one drawer can be open at a time. + break; + } + } + + ss.lockModeLeft = mLockModeLeft; + ss.lockModeRight = mLockModeRight; + + return ss; + } + + /** + * State persisted across instances + */ + protected static class SavedState extends BaseSavedState { + int openDrawerGravity = Gravity.NO_GRAVITY; + int lockModeLeft = LOCK_MODE_UNLOCKED; + int lockModeRight = LOCK_MODE_UNLOCKED; + + public SavedState(Parcel in) { + super(in); + openDrawerGravity = in.readInt(); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(openDrawerGravity); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel source) { + return new SavedState(source); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + private class ViewDragCallback extends ViewDragHelper.Callback { + private final int mAbsGravity; + private ViewDragHelper mDragger; + + private final Runnable mPeekRunnable = new Runnable() { + @Override public void run() { + peekDrawer(); + } + }; + + public ViewDragCallback(int gravity) { + mAbsGravity = gravity; + } + + public void setDragger(ViewDragHelper dragger) { + mDragger = dragger; + } + + public void removeCallbacks() { + DrawerLayout.this.removeCallbacks(mPeekRunnable); + } + + @Override + public boolean tryCaptureView(View child, int pointerId) { + // Only capture views where the gravity matches what we're looking for. + // This lets us use two ViewDragHelpers, one for each side drawer. + return isDrawerView(child) && checkDrawerViewAbsoluteGravity(child, mAbsGravity) + && getDrawerLockMode(child) == LOCK_MODE_UNLOCKED; + } + + @Override + public void onViewDragStateChanged(int state) { + updateDrawerState(mAbsGravity, state, mDragger.getCapturedView()); + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + float offset; + final int childWidth = changedView.getWidth(); + + // This reverses the positioning shown in onLayout. + if (checkDrawerViewAbsoluteGravity(changedView, Gravity.LEFT)) { + offset = (float) (childWidth + left) / childWidth; + } else { + final int width = getWidth(); + offset = (float) (width - left) / childWidth; + } + setDrawerViewOffset(changedView, offset); + changedView.setVisibility(offset == 0 ? INVISIBLE : VISIBLE); + invalidate(); + } + + @Override + public void onViewCaptured(View capturedChild, int activePointerId) { + final LayoutParams lp = (LayoutParams) capturedChild.getLayoutParams(); + lp.isPeeking = false; + + closeOtherDrawer(); + } + + private void closeOtherDrawer() { + final int otherGrav = mAbsGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT; + final View toClose = findDrawerWithGravity(otherGrav); + if (toClose != null) { + closeDrawer(toClose); + } + } + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + // Offset is how open the drawer is, therefore left/right values + // are reversed from one another. + final float offset = getDrawerViewOffset(releasedChild); + final int childWidth = releasedChild.getWidth(); + + int left; + if (checkDrawerViewAbsoluteGravity(releasedChild, Gravity.LEFT)) { + left = xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -childWidth; + } else { + final int width = getWidth(); + left = xvel < 0 || xvel == 0 && offset > 0.5f ? width - childWidth : width; + } + + mDragger.settleCapturedViewAt(left, releasedChild.getTop()); + invalidate(); + } + + @Override + public void onEdgeTouched(int edgeFlags, int pointerId) { + // TODO no do show animate + //postDelayed(mPeekRunnable, PEEK_DELAY); + } + + private void peekDrawer() { + final View toCapture; + final int childLeft; + final int peekDistance = mDragger.getEdgeSize(); + final boolean leftEdge = mAbsGravity == Gravity.LEFT; + if (leftEdge) { + toCapture = findDrawerWithGravity(Gravity.LEFT); + childLeft = (toCapture != null ? -toCapture.getWidth() : 0) + peekDistance; + } else { + toCapture = findDrawerWithGravity(Gravity.RIGHT); + childLeft = getWidth() - peekDistance; + } + // Only peek if it would mean making the drawer more visible and the drawer isn't locked + if (toCapture != null && ((leftEdge && toCapture.getLeft() < childLeft) || + (!leftEdge && toCapture.getLeft() > childLeft)) && + getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { + final LayoutParams lp = (LayoutParams) toCapture.getLayoutParams(); + mDragger.smoothSlideViewTo(toCapture, childLeft, toCapture.getTop()); + lp.isPeeking = true; + invalidate(); + + closeOtherDrawer(); + + cancelChildViewTouch(); + } + } + + @Override + public boolean onEdgeLock(int edgeFlags) { + if (ALLOW_EDGE_LOCK) { + final View drawer = findDrawerWithGravity(mAbsGravity); + if (drawer != null && !isDrawerOpen(drawer)) { + closeDrawer(drawer); + } + return true; + } + return false; + } + + @Override + public void onEdgeDragStarted(int edgeFlags, int pointerId) { + final View toCapture; + if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT) { + toCapture = findDrawerWithGravity(Gravity.LEFT); + } else { + toCapture = findDrawerWithGravity(Gravity.RIGHT); + } + + if (toCapture != null && getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { + mDragger.captureChildView(toCapture, pointerId); + } + } + + @Override + public int getViewHorizontalDragRange(View child) { + return child.getWidth(); + } + + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + return Math.max(-child.getWidth(), Math.min(left, 0)); + } else { + final int width = getWidth(); + return Math.max(width - child.getWidth(), Math.min(left, width)); + } + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + return child.getTop(); + } + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + + public int gravity = Gravity.NO_GRAVITY; + float onScreen; + boolean isPeeking; + boolean knownOpen; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + this.gravity = a.getInt(0, Gravity.NO_GRAVITY); + a.recycle(); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(int width, int height, int gravity) { + this(width, height); + this.gravity = gravity; + } + + public LayoutParams(LayoutParams source) { + super(source); + this.gravity = source.gravity; + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + } + + class AccessibilityDelegate extends AccessibilityDelegateCompat { + private final Rect mTmpRect = new Rect(); + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info); + super.onInitializeAccessibilityNodeInfo(host, superNode); + + info.setSource(host); + final ViewParent parent = ViewCompat.getParentForAccessibility(host); + if (parent instanceof View) { + info.setParent((View) parent); + } + copyNodeInfoNoChildren(info, superNode); + + superNode.recycle(); + + addChildrenForAccessibility(info, (ViewGroup) host); + } + + private void addChildrenForAccessibility(AccessibilityNodeInfoCompat info, ViewGroup v) { + final int childCount = v.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = v.getChildAt(i); + if (filter(child)) { + continue; + } + + // Adding children that are marked as not important for + // accessibility will break the hierarchy, so we need to check + // that value and re-parent views if necessary. + final int importance = ViewCompat.getImportantForAccessibility(child); + switch (importance) { + case ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS: + // Always skip NO_HIDE views and their descendants. + break; + case ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO: + // Re-parent children of NO view groups, skip NO views. + if (child instanceof ViewGroup) { + addChildrenForAccessibility(info, (ViewGroup) child); + } + break; + case ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO: + // Force AUTO views to YES and add them. + ViewCompat.setImportantForAccessibility( + child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + case ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES: + info.addChild(child); + break; + } + } + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + if (!filter(child)) { + return super.onRequestSendAccessibilityEvent(host, child, event); + } + return false; + } + + public boolean filter(View child) { + final View openDrawer = findOpenDrawer(); + return openDrawer != null && openDrawer != child; + } + + /** + * This should really be in AccessibilityNodeInfoCompat, but there unfortunately + * seem to be a few elements that are not easily cloneable using the underlying API. + * Leave it private here as it's not general-purpose useful. + */ + private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, + AccessibilityNodeInfoCompat src) { + final Rect rect = mTmpRect; + + src.getBoundsInParent(rect); + dest.setBoundsInParent(rect); + + src.getBoundsInScreen(rect); + dest.setBoundsInScreen(rect); + + dest.setVisibleToUser(src.isVisibleToUser()); + dest.setPackageName(src.getPackageName()); + dest.setClassName(src.getClassName()); + dest.setContentDescription(src.getContentDescription()); + + dest.setEnabled(src.isEnabled()); + dest.setClickable(src.isClickable()); + dest.setFocusable(src.isFocusable()); + dest.setFocused(src.isFocused()); + dest.setAccessibilityFocused(src.isAccessibilityFocused()); + dest.setSelected(src.isSelected()); + dest.setLongClickable(src.isLongClickable()); + + dest.addAction(src.getActions()); + } + } +} diff --git a/src/com/hippo/ehviewer/widget/ViewDragHelper.java b/src/com/hippo/ehviewer/widget/ViewDragHelper.java new file mode 100644 index 000000000..4140ffc9f --- /dev/null +++ b/src/com/hippo/ehviewer/widget/ViewDragHelper.java @@ -0,0 +1,1485 @@ +/* + * Copyright (C) 2013 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.hippo.ehviewer.widget; + +import android.content.Context; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.VelocityTrackerCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.widget.ScrollerCompat; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.Interpolator; + +import java.util.Arrays; + +/** + * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number + * of useful operations and state tracking for allowing a user to drag and reposition + * views within their parent ViewGroup. + */ +public class ViewDragHelper { + @SuppressWarnings("unused") + private static final String TAG = "ViewDragHelper"; + + /** + * A null/invalid pointer ID. + */ + public static final int INVALID_POINTER = -1; + + /** + * A view is not currently being dragged or animating as a result of a fling/snap. + */ + public static final int STATE_IDLE = 0; + + /** + * A view is currently being dragged. The position is currently changing as a result + * of user input or simulated user input. + */ + public static final int STATE_DRAGGING = 1; + + /** + * A view is currently settling into place as a result of a fling or + * predefined non-interactive motion. + */ + public static final int STATE_SETTLING = 2; + + /** + * Edge flag indicating that the left edge should be affected. + */ + public static final int EDGE_LEFT = 1 << 0; + + /** + * Edge flag indicating that the right edge should be affected. + */ + public static final int EDGE_RIGHT = 1 << 1; + + /** + * Edge flag indicating that the top edge should be affected. + */ + public static final int EDGE_TOP = 1 << 2; + + /** + * Edge flag indicating that the bottom edge should be affected. + */ + public static final int EDGE_BOTTOM = 1 << 3; + + /** + * Edge flag set indicating all edges should be affected. + */ + public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM; + + /** + * Indicates that a check should occur along the horizontal axis + */ + public static final int DIRECTION_HORIZONTAL = 1 << 0; + + /** + * Indicates that a check should occur along the vertical axis + */ + public static final int DIRECTION_VERTICAL = 1 << 1; + + /** + * Indicates that a check should occur along all axes + */ + public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + + // TODO make it large enough + private static final int EDGE_SIZE_INTERCEPT = 20; + private static final int EDGE_SIZE = 400; // dp + + private static final int BASE_SETTLE_DURATION = 256; // ms + private static final int MAX_SETTLE_DURATION = 600; // ms + + // Current drag state; idle, dragging or settling + private int mDragState; + + // Distance to travel before a drag may begin + private int mTouchSlop; + + // Last known position/pointer tracking + private int mActivePointerId = INVALID_POINTER; + private float[] mInitialMotionX; + private float[] mInitialMotionY; + private float[] mLastMotionX; + private float[] mLastMotionY; + private int[] mInitialEdgesTouched; + private int[] mEdgeDragsInProgress; + private int[] mEdgeDragsLocked; + private int mPointersDown; + + private VelocityTracker mVelocityTracker; + private float mMaxVelocity; + private float mMinVelocity; + + private int mEdgeSize; + private int mEdgeSizeIntercept; + private int mTrackingEdges; + + private ScrollerCompat mScroller; + + private final Callback mCallback; + + private View mCapturedView; + private boolean mReleaseInProgress; + + private final ViewGroup mParentView; + + /** + * A Callback is used as a communication channel with the ViewDragHelper back to the + * parent view using it. on*methods are invoked on siginficant events and several + * accessor methods are expected to provide the ViewDragHelper with more information + * about the state of the parent view upon request. The callback also makes decisions + * governing the range and draggability of child views. + */ + public static abstract class Callback { + /** + * Called when the drag state changes. See the STATE_* constants + * for more information. + * + * @param state The new drag state + * + * @see #STATE_IDLE + * @see #STATE_DRAGGING + * @see #STATE_SETTLING + */ + public void onViewDragStateChanged(int state) {} + + /** + * Called when the captured view's position changes as the result of a drag or settle. + * + * @param changedView View whose position changed + * @param left New X coordinate of the left edge of the view + * @param top New Y coordinate of the top edge of the view + * @param dx Change in X position from the last call + * @param dy Change in Y position from the last call + */ + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {} + + /** + * Called when a child view is captured for dragging or settling. The ID of the pointer + * currently dragging the captured view is supplied. If activePointerId is + * identified as {@link #INVALID_POINTER} the capture is programmatic instead of + * pointer-initiated. + * + * @param capturedChild Child view that was captured + * @param activePointerId Pointer id tracking the child capture + */ + public void onViewCaptured(View capturedChild, int activePointerId) {} + + /** + * Called when the child view is no longer being actively dragged. + * The fling velocity is also supplied, if relevant. The velocity values may + * be clamped to system minimums or maximums. + * + *

Calling code may decide to fling or otherwise release the view to let it + * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)} + * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes + * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING} + * and the view capture will not fully end until it comes to a complete stop. + * If neither of these methods is invoked before onViewReleased returns, + * the view will stop in place and the ViewDragHelper will return to + * {@link #STATE_IDLE}.

+ * + * @param releasedChild The captured child view now being released + * @param xvel X velocity of the pointer as it left the screen in pixels per second. + * @param yvel Y velocity of the pointer as it left the screen in pixels per second. + */ + public void onViewReleased(View releasedChild, float xvel, float yvel) {} + + /** + * Called when one of the subscribed edges in the parent view has been touched + * by the user while no child view is currently captured. + * + * @param edgeFlags A combination of edge flags describing the edge(s) currently touched + * @param pointerId ID of the pointer touching the described edge(s) + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void onEdgeTouched(int edgeFlags, int pointerId) {} + + /** + * Called when the given edge may become locked. This can happen if an edge drag + * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)} + * was called. This method should return true to lock this edge or false to leave it + * unlocked. The default behavior is to leave edges unlocked. + * + * @param edgeFlags A combination of edge flags describing the edge(s) locked + * @return true to lock the edge, false to leave it unlocked + */ + public boolean onEdgeLock(int edgeFlags) { + return false; + } + + /** + * Called when the user has started a deliberate drag away from one + * of the subscribed edges in the parent view while no child view is currently captured. + * + * @param edgeFlags A combination of edge flags describing the edge(s) dragged + * @param pointerId ID of the pointer touching the described edge(s) + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void onEdgeDragStarted(int edgeFlags, int pointerId) {} + + /** + * Called to determine the Z-order of child views. + * + * @param index the ordered position to query for + * @return index of the view that should be ordered at position index + */ + public int getOrderedChildIndex(int index) { + return index; + } + + /** + * Return the magnitude of a draggable child view's horizontal range of motion in pixels. + * This method should return 0 for views that cannot move horizontally. + * + * @param child Child view to check + * @return range of horizontal motion in pixels + */ + public int getViewHorizontalDragRange(View child) { + return 0; + } + + /** + * Return the magnitude of a draggable child view's vertical range of motion in pixels. + * This method should return 0 for views that cannot move vertically. + * + * @param child Child view to check + * @return range of vertical motion in pixels + */ + public int getViewVerticalDragRange(View child) { + return 0; + } + + /** + * Called when the user's input indicates that they want to capture the given child view + * with the pointer indicated by pointerId. The callback should return true if the user + * is permitted to drag the given view with the indicated pointer. + * + *

ViewDragHelper may call this method multiple times for the same view even if + * the view is already captured; this indicates that a new pointer is trying to take + * control of the view.

+ * + *

If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)} + * will follow if the capture is successful.

+ * + * @param child Child the user is attempting to capture + * @param pointerId ID of the pointer attempting the capture + * @return true if capture should be allowed, false otherwise + */ + public abstract boolean tryCaptureView(View child, int pointerId); + + /** + * Restrict the motion of the dragged child view along the horizontal axis. + * The default implementation does not allow horizontal motion; the extending + * class must override this method and provide the desired clamping. + * + * + * @param child Child view being dragged + * @param left Attempted motion along the X axis + * @param dx Proposed change in position for left + * @return The new clamped position for left + */ + public int clampViewPositionHorizontal(View child, int left, int dx) { + return 0; + } + + /** + * Restrict the motion of the dragged child view along the vertical axis. + * The default implementation does not allow vertical motion; the extending + * class must override this method and provide the desired clamping. + * + * + * @param child Child view being dragged + * @param top Attempted motion along the Y axis + * @param dy Proposed change in position for top + * @return The new clamped position for top + */ + public int clampViewPositionVertical(View child, int top, int dy) { + return 0; + } + } + + /** + * Interpolator defining the animation curve for mScroller + */ + private static final Interpolator sInterpolator = new Interpolator() { + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private final Runnable mSetIdleRunnable = new Runnable() { + public void run() { + setDragState(STATE_IDLE); + } + }; + + /** + * Factory method to create a new ViewDragHelper. + * + * @param forParent Parent view to monitor + * @param cb Callback to provide information and receive events + * @return a new ViewDragHelper instance + */ + public static ViewDragHelper create(ViewGroup forParent, Callback cb) { + return new ViewDragHelper(forParent.getContext(), forParent, cb); + } + + /** + * Factory method to create a new ViewDragHelper. + * + * @param forParent Parent view to monitor + * @param sensitivity Multiplier for how sensitive the helper should be about detecting + * the start of a drag. Larger values are more sensitive. 1.0f is normal. + * @param cb Callback to provide information and receive events + * @return a new ViewDragHelper instance + */ + public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { + final ViewDragHelper helper = create(forParent, cb); + helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); + return helper; + } + + /** + * Apps should use ViewDragHelper.create() to get a new instance. + * This will allow VDH to use internal compatibility implementations for different + * platform versions. + * + * @param context Context to initialize config-dependent params from + * @param forParent Parent view to monitor + */ + private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { + if (forParent == null) { + throw new IllegalArgumentException("Parent view may not be null"); + } + if (cb == null) { + throw new IllegalArgumentException("Callback may not be null"); + } + + mParentView = forParent; + mCallback = cb; + + final ViewConfiguration vc = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + mEdgeSizeIntercept = (int) (EDGE_SIZE_INTERCEPT * density + 0.5f); + mEdgeSize = (int) (EDGE_SIZE * density + 0.5f); + + mTouchSlop = vc.getScaledTouchSlop(); + mMaxVelocity = vc.getScaledMaximumFlingVelocity(); + mMinVelocity = vc.getScaledMinimumFlingVelocity(); + mScroller = ScrollerCompat.create(context, sInterpolator); + } + + /** + * Set the minimum velocity that will be detected as having a magnitude greater than zero + * in pixels per second. Callback methods accepting a velocity will be clamped appropriately. + * + * @param minVel Minimum velocity to detect + */ + public void setMinVelocity(float minVel) { + mMinVelocity = minVel; + } + + /** + * Return the currently configured minimum velocity. Any flings with a magnitude less + * than this value in pixels per second. Callback methods accepting a velocity will receive + * zero as a velocity value if the real detected velocity was below this threshold. + * + * @return the minimum velocity that will be detected + */ + public float getMinVelocity() { + return mMinVelocity; + } + + /** + * Retrieve the current drag state of this helper. This will return one of + * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. + * @return The current drag state + */ + public int getViewDragState() { + return mDragState; + } + + /** + * Enable edge tracking for the selected edges of the parent view. + * The callback's {@link Callback#onEdgeTouched(int, int)} and + * {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked + * for edges for which edge tracking has been enabled. + * + * @param edgeFlags Combination of edge flags describing the edges to watch + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void setEdgeTrackingEnabled(int edgeFlags) { + mTrackingEdges = edgeFlags; + } + + /** + * Return the size of an edge. This is the range in pixels along the edges of this view + * that will actively detect edge touches or drags if edge tracking is enabled. + * + * @return The size of an edge in pixels + * @see #setEdgeTrackingEnabled(int) + */ + public int getEdgeSize() { + return mEdgeSize; + } + + /** + * Capture a specific child view for dragging within the parent. The callback will be notified + * but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to + * capture this view. + * + * @param childView Child view to capture + * @param activePointerId ID of the pointer that is dragging the captured child view + */ + public void captureChildView(View childView, int activePointerId) { + if (childView.getParent() != mParentView) { + throw new IllegalArgumentException("captureChildView: parameter must be a descendant " + + "of the ViewDragHelper's tracked parent view (" + mParentView + ")"); + } + + mCapturedView = childView; + mActivePointerId = activePointerId; + mCallback.onViewCaptured(childView, activePointerId); + setDragState(STATE_DRAGGING); + } + + /** + * @return The currently captured view, or null if no view has been captured. + */ + public View getCapturedView() { + return mCapturedView; + } + + /** + * @return The ID of the pointer currently dragging the captured view, + * or {@link #INVALID_POINTER}. + */ + public int getActivePointerId() { + return mActivePointerId; + } + + /** + * @return The minimum distance in pixels that the user must travel to initiate a drag + */ + public int getTouchSlop() { + return mTouchSlop; + } + + /** + * The result of a call to this method is equivalent to + * {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event. + */ + public void cancel() { + mActivePointerId = INVALID_POINTER; + clearMotionHistory(); + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** + * {@link #cancel()}, but also abort all motion in progress and snap to the end of any + * animation. + */ + public void abort() { + cancel(); + if (mDragState == STATE_SETTLING) { + final int oldX = mScroller.getCurrX(); + final int oldY = mScroller.getCurrY(); + mScroller.abortAnimation(); + final int newX = mScroller.getCurrX(); + final int newY = mScroller.getCurrY(); + mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY); + } + setDragState(STATE_IDLE); + } + + /** + * Animate the view child to the given (left, top) position. + * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} + * on each subsequent frame to continue the motion until it returns false. If this method + * returns false there is no further work to do to complete the movement. + * + *

This operation does not count as a capture event, though {@link #getCapturedView()} + * will still report the sliding view while the slide is in progress.

+ * + * @param child Child view to capture and animate + * @param finalLeft Final left position of child + * @param finalTop Final top position of child + * @return true if animation should continue through {@link #continueSettling(boolean)} calls + */ + public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { + mCapturedView = child; + mActivePointerId = INVALID_POINTER; + + return forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0); + } + + /** + * Settle the captured view at the given (left, top) position. + * The appropriate velocity from prior motion will be taken into account. + * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} + * on each subsequent frame to continue the motion until it returns false. If this method + * returns false there is no further work to do to complete the movement. + * + * @param finalLeft Settled left edge position for the captured view + * @param finalTop Settled top edge position for the captured view + * @return true if animation should continue through {@link #continueSettling(boolean)} calls + */ + public boolean settleCapturedViewAt(int finalLeft, int finalTop) { + if (!mReleaseInProgress) { + throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + + "Callback#onViewReleased"); + } + + return forceSettleCapturedViewAt(finalLeft, finalTop, + (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), + (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId)); + } + + /** + * Settle the captured view at the given (left, top) position. + * + * @param finalLeft Target left position for the captured view + * @param finalTop Target top position for the captured view + * @param xvel Horizontal velocity + * @param yvel Vertical velocity + * @return true if animation should continue through {@link #continueSettling(boolean)} calls + */ + private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { + final int startLeft = mCapturedView.getLeft(); + final int startTop = mCapturedView.getTop(); + final int dx = finalLeft - startLeft; + final int dy = finalTop - startTop; + + if (dx == 0 && dy == 0) { + // Nothing to do. Send callbacks, be done. + mScroller.abortAnimation(); + setDragState(STATE_IDLE); + return false; + } + + final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); + mScroller.startScroll(startLeft, startTop, dx, dy, duration); + + setDragState(STATE_SETTLING); + return true; + } + + private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) { + xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity); + yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity); + final int absDx = Math.abs(dx); + final int absDy = Math.abs(dy); + final int absXVel = Math.abs(xvel); + final int absYVel = Math.abs(yvel); + final int addedVel = absXVel + absYVel; + final int addedDistance = absDx + absDy; + + final float xweight = xvel != 0 ? (float) absXVel / addedVel : + (float) absDx / addedDistance; + final float yweight = yvel != 0 ? (float) absYVel / addedVel : + (float) absDy / addedDistance; + + int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child)); + int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child)); + + return (int) (xduration * xweight + yduration * yweight); + } + + private int computeAxisDuration(int delta, int velocity, int motionRange) { + if (delta == 0) { + return 0; + } + + final int width = mParentView.getWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width); + final float distance = halfWidth + halfWidth * + distanceInfluenceForSnapDuration(distanceRatio); + + int duration; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float range = (float) Math.abs(delta) / motionRange; + duration = (int) ((range + 1) * BASE_SETTLE_DURATION); + } + return Math.min(duration, MAX_SETTLE_DURATION); + } + + /** + * Clamp the magnitude of value for absMin and absMax. + * If the value is below the minimum, it will be clamped to zero. + * If the value is above the maximum, it will be clamped to the maximum. + * + * @param value Value to clamp + * @param absMin Absolute value of the minimum significant value to return + * @param absMax Absolute value of the maximum value to return + * @return The clamped value with the same sign as value + */ + private int clampMag(int value, int absMin, int absMax) { + final int absValue = Math.abs(value); + if (absValue < absMin) return 0; + if (absValue > absMax) return value > 0 ? absMax : -absMax; + return value; + } + + /** + * Clamp the magnitude of value for absMin and absMax. + * If the value is below the minimum, it will be clamped to zero. + * If the value is above the maximum, it will be clamped to the maximum. + * + * @param value Value to clamp + * @param absMin Absolute value of the minimum significant value to return + * @param absMax Absolute value of the maximum value to return + * @return The clamped value with the same sign as value + */ + private float clampMag(float value, float absMin, float absMax) { + final float absValue = Math.abs(value); + if (absValue < absMin) return 0; + if (absValue > absMax) return value > 0 ? absMax : -absMax; + return value; + } + + private float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Settle the captured view based on standard free-moving fling behavior. + * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame + * to continue the motion until it returns false. + * + * @param minLeft Minimum X position for the view's left edge + * @param minTop Minimum Y position for the view's top edge + * @param maxLeft Maximum X position for the view's left edge + * @param maxTop Maximum Y position for the view's top edge + */ + public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) { + if (!mReleaseInProgress) { + throw new IllegalStateException("Cannot flingCapturedView outside of a call to " + + "Callback#onViewReleased"); + } + + mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), + (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), + (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), + minLeft, maxLeft, minTop, maxTop); + + setDragState(STATE_SETTLING); + } + + /** + * Move the captured settling view by the appropriate amount for the current time. + * If continueSettling returns true, the caller should call it again + * on the next frame to continue. + * + * @param deferCallbacks true if state callbacks should be deferred via posted message. + * Set this to true if you are calling this method from + * {@link android.view.View#computeScroll()} or similar methods + * invoked as part of layout or drawing. + * @return true if settle is still in progress + */ + public boolean continueSettling(boolean deferCallbacks) { + if (mDragState == STATE_SETTLING) { + boolean keepGoing = mScroller.computeScrollOffset(); + final int x = mScroller.getCurrX(); + final int y = mScroller.getCurrY(); + final int dx = x - mCapturedView.getLeft(); + final int dy = y - mCapturedView.getTop(); + + if (dx != 0) { + mCapturedView.offsetLeftAndRight(dx); + } + if (dy != 0) { + mCapturedView.offsetTopAndBottom(dy); + } + + if (dx != 0 || dy != 0) { + mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy); + } + + if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) { + // Close enough. The interpolator/scroller might think we're still moving + // but the user sure doesn't. + mScroller.abortAnimation(); + keepGoing = mScroller.isFinished(); + } + + if (!keepGoing) { + if (deferCallbacks) { + mParentView.post(mSetIdleRunnable); + } else { + setDragState(STATE_IDLE); + } + } + } + + return mDragState == STATE_SETTLING; + } + + /** + * Like all callback events this must happen on the UI thread, but release + * involves some extra semantics. During a release (mReleaseInProgress) + * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)} + * or {@link #flingCapturedView(int, int, int, int)}. + */ + private void dispatchViewReleased(float xvel, float yvel) { + mReleaseInProgress = true; + mCallback.onViewReleased(mCapturedView, xvel, yvel); + mReleaseInProgress = false; + + if (mDragState == STATE_DRAGGING) { + // onViewReleased didn't call a method that would have changed this. Go idle. + setDragState(STATE_IDLE); + } + } + + private void clearMotionHistory() { + if (mInitialMotionX == null) { + return; + } + Arrays.fill(mInitialMotionX, 0); + Arrays.fill(mInitialMotionY, 0); + Arrays.fill(mLastMotionX, 0); + Arrays.fill(mLastMotionY, 0); + Arrays.fill(mInitialEdgesTouched, 0); + Arrays.fill(mEdgeDragsInProgress, 0); + Arrays.fill(mEdgeDragsLocked, 0); + mPointersDown = 0; + } + + private void clearMotionHistory(int pointerId) { + if (mInitialMotionX == null) { + return; + } + mInitialMotionX[pointerId] = 0; + mInitialMotionY[pointerId] = 0; + mLastMotionX[pointerId] = 0; + mLastMotionY[pointerId] = 0; + mInitialEdgesTouched[pointerId] = 0; + mEdgeDragsInProgress[pointerId] = 0; + mEdgeDragsLocked[pointerId] = 0; + mPointersDown &= ~(1 << pointerId); + } + + private void ensureMotionHistorySizeForId(int pointerId) { + if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) { + float[] imx = new float[pointerId + 1]; + float[] imy = new float[pointerId + 1]; + float[] lmx = new float[pointerId + 1]; + float[] lmy = new float[pointerId + 1]; + int[] iit = new int[pointerId + 1]; + int[] edip = new int[pointerId + 1]; + int[] edl = new int[pointerId + 1]; + + if (mInitialMotionX != null) { + System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length); + System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length); + System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length); + System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length); + System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length); + System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length); + System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length); + } + + mInitialMotionX = imx; + mInitialMotionY = imy; + mLastMotionX = lmx; + mLastMotionY = lmy; + mInitialEdgesTouched = iit; + mEdgeDragsInProgress = edip; + mEdgeDragsLocked = edl; + } + } + + private void saveInitialMotion(float x, float y, int pointerId) { + ensureMotionHistorySizeForId(pointerId); + mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x; + mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y; + mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y); + mPointersDown |= 1 << pointerId; + } + + private void saveInitialMotionIntercept(float x, float y, int pointerId) { + ensureMotionHistorySizeForId(pointerId); + mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x; + mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y; + mInitialEdgesTouched[pointerId] = getEdgesTouchedIntercept((int) x, (int) y); + } + + + private void saveLastMotion(MotionEvent ev) { + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = MotionEventCompat.getPointerId(ev, i); + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + mLastMotionX[pointerId] = x; + mLastMotionY[pointerId] = y; + } + } + + /** + * Check if the given pointer ID represents a pointer that is currently down (to the best + * of the ViewDragHelper's knowledge). + * + *

The state used to report this information is populated by the methods + * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or + * {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not + * been called for all relevant MotionEvents to track, the information reported + * by this method may be stale or incorrect.

+ * + * @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent + * @return true if the pointer with the given ID is still down + */ + public boolean isPointerDown(int pointerId) { + return (mPointersDown & 1 << pointerId) != 0; + } + + void setDragState(int state) { + if (mDragState != state) { + mDragState = state; + mCallback.onViewDragStateChanged(state); + if (state == STATE_IDLE) { + mCapturedView = null; + } + } + } + + /** + * Attempt to capture the view with the given pointer ID. The callback will be involved. + * This will put us into the "dragging" state. If we've already captured this view with + * this pointer this method will immediately return true without consulting the callback. + * + * @param toCapture View to capture + * @param pointerId Pointer to capture with + * @return true if capture was successful + */ + boolean tryCaptureViewForDrag(View toCapture, int pointerId) { + if (toCapture == mCapturedView && mActivePointerId == pointerId) { + // Already done! + return true; + } + if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { + mActivePointerId = pointerId; + captureChildView(toCapture, pointerId); + return true; + } + return false; + } + + /** + * Tests scrollability within child views of v given a delta 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 along the X axis + * @param dy Delta scrolled in pixels along the Y axis + * @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, int dx, int dy, int x, int 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(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + 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, dy, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && (ViewCompat.canScrollHorizontally(v, -dx) || + ViewCompat.canScrollVertically(v, -dy)); + } + + /** + * Check if this event as provided to the parent view's onInterceptTouchEvent should + * cause the parent to intercept the touch event stream. + * + * @param ev MotionEvent provided to onInterceptTouchEvent + * @return true if the parent view should return true from onInterceptTouchEvent + */ + public boolean shouldInterceptTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + final int actionIndex = MotionEventCompat.getActionIndex(ev); + + if (action == MotionEvent.ACTION_DOWN) { + // Reset things for a new event stream, just in case we didn't get + // the whole previous stream. + cancel(); + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + final int pointerId = MotionEventCompat.getPointerId(ev, 0); + saveInitialMotionIntercept(x, y, pointerId); + + final View toCapture = findTopChildUnder((int) x, (int) y); + + // Catch a settling view if possible. + if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { + tryCaptureViewForDrag(toCapture, pointerId); + } + + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + break; + } + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + final float x = MotionEventCompat.getX(ev, actionIndex); + final float y = MotionEventCompat.getY(ev, actionIndex); + + saveInitialMotionIntercept(x, y, pointerId); + + // A ViewDragHelper can only manipulate one view at a time. + if (mDragState == STATE_IDLE) { + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + } else if (mDragState == STATE_SETTLING) { + // Catch a settling view if possible. + final View toCapture = findTopChildUnder((int) x, (int) y); + if (toCapture == mCapturedView) { + tryCaptureViewForDrag(toCapture, pointerId); + } + } + break; + } + + case MotionEvent.ACTION_MOVE: { + // First to cross a touch slop over a draggable view wins. Also report edge drags. + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = MotionEventCompat.getPointerId(ev, i); + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + final float dx = x - mInitialMotionX[pointerId]; + final float dy = y - mInitialMotionY[pointerId]; + + reportNewEdgeDrags(dx, dy, pointerId); + if (mDragState == STATE_DRAGGING) { + // Callback might have started an edge drag + break; + } + + final View toCapture = findTopChildUnder((int) x, (int) y); + if (toCapture != null && checkTouchSlop(toCapture, dx, dy) && + tryCaptureViewForDrag(toCapture, pointerId)) { + break; + } + } + saveLastMotion(ev); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + clearMotionHistory(pointerId); + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + cancel(); + break; + } + } + + return mDragState == STATE_DRAGGING; + } + + /** + * Process a touch event received by the parent view. This method will dispatch callback events + * as needed before returning. The parent view's onTouchEvent implementation should call this. + * + * @param ev The touch event received by the parent view + */ + public void processTouchEvent(MotionEvent ev) { + try { + final int action = MotionEventCompat.getActionMasked(ev); + final int actionIndex = MotionEventCompat.getActionIndex(ev); + + if (action == MotionEvent.ACTION_DOWN) { + // Reset things for a new event stream, just in case we didn't get + // the whole previous stream. + cancel(); + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + final int pointerId = MotionEventCompat.getPointerId(ev, 0); + final View toCapture = findTopChildUnder((int) x, (int) y); + + saveInitialMotion(x, y, pointerId); + + // Since the parent is already directly processing this touch event, + // there is no reason to delay for a slop before dragging. + // Start immediately if possible. + tryCaptureViewForDrag(toCapture, pointerId); + + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + break; + } + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + final float x = MotionEventCompat.getX(ev, actionIndex); + final float y = MotionEventCompat.getY(ev, actionIndex); + + saveInitialMotion(x, y, pointerId); + + // A ViewDragHelper can only manipulate one view at a time. + if (mDragState == STATE_IDLE) { + // If we're idle we can do anything! Treat it like a normal down event. + + final View toCapture = findTopChildUnder((int) x, (int) y); + tryCaptureViewForDrag(toCapture, pointerId); + + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + } else if (isCapturedViewUnder((int) x, (int) y)) { + // We're still tracking a captured view. If the same view is under this + // point, we'll swap to controlling it with this pointer instead. + // (This will still work if we're "catching" a settling view.) + + tryCaptureViewForDrag(mCapturedView, pointerId); + } + break; + } + + case MotionEvent.ACTION_MOVE: { + if ((mPointersDown & (1 << MotionEventCompat.getPointerId(ev, 0))) == 0) { + final float x = ev.getX(); + final float y = ev.getY(); + saveInitialMotion(x, y, MotionEventCompat.getPointerId(ev, 0)); + } + + if (mDragState == STATE_DRAGGING) { + final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, index); + final float y = MotionEventCompat.getY(ev, index); + final int idx = (int) (x - mLastMotionX[mActivePointerId]); + final int idy = (int) (y - mLastMotionY[mActivePointerId]); + + dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); + + saveLastMotion(ev); + } else { + // Check to see if any pointer is now over a draggable view. + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = MotionEventCompat.getPointerId(ev, i); + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + final float dx = x - mInitialMotionX[pointerId]; + final float dy = y - mInitialMotionY[pointerId]; + + reportNewEdgeDrags(dx, dy, pointerId); + if (mDragState == STATE_DRAGGING) { + // Callback might have started an edge drag. + break; + } + + final View toCapture = findTopChildUnder((int) x, (int) y); + if (checkTouchSlop(toCapture, dx, dy) && + tryCaptureViewForDrag(toCapture, pointerId)) { + break; + } + } + saveLastMotion(ev); + } + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) { + // Try to find another pointer that's still holding on to the captured view. + int newActivePointer = INVALID_POINTER; + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int id = MotionEventCompat.getPointerId(ev, i); + if (id == mActivePointerId) { + // This one's going away, skip. + continue; + } + + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + if (findTopChildUnder((int) x, (int) y) == mCapturedView && + tryCaptureViewForDrag(mCapturedView, id)) { + newActivePointer = mActivePointerId; + break; + } + } + + if (newActivePointer == INVALID_POINTER) { + // We didn't find another pointer still touching the view, release it. + releaseViewForPointerUp(); + } + } + clearMotionHistory(pointerId); + break; + } + + case MotionEvent.ACTION_UP: { + if (mDragState == STATE_DRAGGING) { + releaseViewForPointerUp(); + } + cancel(); + break; + } + + case MotionEvent.ACTION_CANCEL: { + if (mDragState == STATE_DRAGGING) { + dispatchViewReleased(0, 0); + } + cancel(); + break; + } + } + } catch (Exception e) { + // TODO + } + } + + private void reportNewEdgeDrags(float dx, float dy, int pointerId) { + int dragsStarted = 0; + if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) { + dragsStarted |= EDGE_LEFT; + } + if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) { + dragsStarted |= EDGE_TOP; + } + if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) { + dragsStarted |= EDGE_RIGHT; + } + if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) { + dragsStarted |= EDGE_BOTTOM; + } + + if (dragsStarted != 0) { + mEdgeDragsInProgress[pointerId] |= dragsStarted; + mCallback.onEdgeDragStarted(dragsStarted, pointerId); + } + } + + private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) { + final float absDelta = Math.abs(delta); + final float absODelta = Math.abs(odelta); + + if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 || + (mEdgeDragsLocked[pointerId] & edge) == edge || + (mEdgeDragsInProgress[pointerId] & edge) == edge || + (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) { + return false; + } + if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) { + mEdgeDragsLocked[pointerId] |= edge; + return false; + } + return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop; + } + + /** + * Check if we've crossed a reasonable touch slop for the given child view. + * If the child cannot be dragged along the horizontal or vertical axis, motion + * along that axis will not count toward the slop check. + * + * @param child Child to check + * @param dx Motion since initial position along X axis + * @param dy Motion since initial position along Y axis + * @return true if the touch slop has been crossed + */ + private boolean checkTouchSlop(View child, float dx, float dy) { + if (child == null) { + return false; + } + final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0; + final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0; + + if (checkHorizontal && checkVertical) { + return dx * dx + dy * dy > mTouchSlop * mTouchSlop; + } else if (checkHorizontal) { + return Math.abs(dx) > mTouchSlop; + } else if (checkVertical) { + return Math.abs(dy) > mTouchSlop; + } + return false; + } + + /** + * Check if any pointer tracked in the current gesture has crossed + * the required slop threshold. + * + *

This depends on internal state populated by + * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or + * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on + * the results of this method after all currently available touch data + * has been provided to one of these two methods.

+ * + * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, + * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL} + * @return true if the slop threshold has been crossed, false otherwise + */ + public boolean checkTouchSlop(int directions) { + final int count = mInitialMotionX.length; + for (int i = 0; i < count; i++) { + if (checkTouchSlop(directions, i)) { + return true; + } + } + return false; + } + + /** + * Check if the specified pointer tracked in the current gesture has crossed + * the required slop threshold. + * + *

This depends on internal state populated by + * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or + * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on + * the results of this method after all currently available touch data + * has been provided to one of these two methods.

+ * + * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, + * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL} + * @param pointerId ID of the pointer to slop check as specified by MotionEvent + * @return true if the slop threshold has been crossed, false otherwise + */ + public boolean checkTouchSlop(int directions, int pointerId) { + if (!isPointerDown(pointerId)) { + return false; + } + + final boolean checkHorizontal = (directions & DIRECTION_HORIZONTAL) == DIRECTION_HORIZONTAL; + final boolean checkVertical = (directions & DIRECTION_VERTICAL) == DIRECTION_VERTICAL; + + final float dx = mLastMotionX[pointerId] - mInitialMotionX[pointerId]; + final float dy = mLastMotionY[pointerId] - mInitialMotionY[pointerId]; + + if (checkHorizontal && checkVertical) { + return dx * dx + dy * dy > mTouchSlop * mTouchSlop; + } else if (checkHorizontal) { + return Math.abs(dx) > mTouchSlop; + } else if (checkVertical) { + return Math.abs(dy) > mTouchSlop; + } + return false; + } + + /** + * Check if any of the edges specified were initially touched in the currently active gesture. + * If there is no currently active gesture this method will return false. + * + * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, + * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and + * {@link #EDGE_ALL} + * @return true if any of the edges specified were initially touched in the current gesture + */ + public boolean isEdgeTouched(int edges) { + final int count = mInitialEdgesTouched.length; + for (int i = 0; i < count; i++) { + if (isEdgeTouched(edges, i)) { + return true; + } + } + return false; + } + + /** + * Check if any of the edges specified were initially touched by the pointer with + * the specified ID. If there is no currently active gesture or if there is no pointer with + * the given ID currently down this method will return false. + * + * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, + * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and + * {@link #EDGE_ALL} + * @return true if any of the edges specified were initially touched in the current gesture + */ + public boolean isEdgeTouched(int edges, int pointerId) { + return isPointerDown(pointerId) && (mInitialEdgesTouched[pointerId] & edges) != 0; + } + + private void releaseViewForPointerUp() { + mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); + final float xvel = clampMag( + VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), + mMinVelocity, mMaxVelocity); + final float yvel = clampMag( + VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), + mMinVelocity, mMaxVelocity); + dispatchViewReleased(xvel, yvel); + } + + private void dragTo(int left, int top, int dx, int dy) { + int clampedX = left; + int clampedY = top; + final int oldLeft = mCapturedView.getLeft(); + final int oldTop = mCapturedView.getTop(); + if (dx != 0) { + clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); + mCapturedView.offsetLeftAndRight(clampedX - oldLeft); + } + if (dy != 0) { + clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); + mCapturedView.offsetTopAndBottom(clampedY - oldTop); + } + + if (dx != 0 || dy != 0) { + final int clampedDx = clampedX - oldLeft; + final int clampedDy = clampedY - oldTop; + mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, + clampedDx, clampedDy); + } + } + + /** + * Determine if the currently captured view is under the given point in the + * parent view's coordinate system. If there is no captured view this method + * will return false. + * + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return true if the captured view is under the given point, false otherwise + */ + public boolean isCapturedViewUnder(int x, int y) { + return isViewUnder(mCapturedView, x, y); + } + + /** + * Determine if the supplied view is under the given point in the + * parent view's coordinate system. + * + * @param view Child view of the parent to hit test + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return true if the supplied view is under the given point, false otherwise + */ + public boolean isViewUnder(View view, int x, int y) { + if (view == null) { + return false; + } + return x >= view.getLeft() && + x < view.getRight() && + y >= view.getTop() && + y < view.getBottom(); + } + + /** + * Find the topmost child under the given point within the parent view's coordinate system. + * The child order is determined using {@link Callback#getOrderedChildIndex(int)}. + * + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return The topmost child view under (x, y) or null if none found. + */ + public View findTopChildUnder(int x, int y) { + final int childCount = mParentView.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i)); + if (x >= child.getLeft() && x < child.getRight() && + y >= child.getTop() && y < child.getBottom()) { + return child; + } + } + return null; + } + + private int getEdgesTouched(int x, int y) { + int result = 0; + int centerX = mParentView.getLeft() + mParentView.getRight()/2; + int centerY = mParentView.getLeft() + mParentView.getRight()/2; + if (x < centerX) result |= EDGE_LEFT; + if (y < centerY) result |= EDGE_TOP; + if (x > centerX) result |= EDGE_RIGHT; + if (y > centerY) result |= EDGE_BOTTOM; + + return result; + } + + private int getEdgesTouchedIntercept(int x, int y) { + int result = 0; + + if (x < mParentView.getLeft() + mEdgeSizeIntercept) result |= EDGE_LEFT; + if (y < mParentView.getTop() + mEdgeSizeIntercept) result |= EDGE_TOP; + if (x > mParentView.getRight() - mEdgeSizeIntercept) result |= EDGE_RIGHT; + if (y > mParentView.getBottom() - mEdgeSizeIntercept) result |= EDGE_BOTTOM; + + return result; + } +}