Last active
February 14, 2017 17:51
-
-
Save levibostian/94fc1b036670c9532317 to your computer and use it in GitHub Desktop.
**Don't delete me. Public shared.** Android navigation drawer animation duration.Blog post: levibostian.com/blog/android-navigation-drawer-animation-duration/index.html*Note:* Does not include all files required to start app. Only files that require editing as discussed in blog post.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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.levibostian.customdurationnavigationdrawer.app; | |
import android.app.ActionBar; | |
import android.app.Activity; | |
import android.content.res.TypedArray; | |
import android.graphics.drawable.Drawable; | |
import android.util.Log; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import android.widget.ImageView; | |
import com.levibostian.customdurationnavigationdrawer.R; | |
import java.lang.reflect.Method; | |
public class ActionBarDrawerToggleHoneycomb { | |
private static final String TAG = "ActionBarDrawerToggleHoneycomb"; | |
private static final int[] THEME_ATTRS = new int[] { | |
R.attr.homeAsUpIndicator | |
}; | |
public static Object setActionBarUpIndicator(Object info, Activity activity, | |
Drawable drawable, int contentDescRes) { | |
if (info == null) { | |
info = new SetIndicatorInfo(activity); | |
} | |
final SetIndicatorInfo sii = (SetIndicatorInfo) info; | |
if (sii.setHomeAsUpIndicator != null) { | |
try { | |
final ActionBar actionBar = activity.getActionBar(); | |
sii.setHomeAsUpIndicator.invoke(actionBar, drawable); | |
sii.setHomeActionContentDescription.invoke(actionBar, contentDescRes); | |
} catch (Exception e) { | |
Log.w(TAG, "Couldn't set home-as-up indicator via JB-MR2 API", e); | |
} | |
} else if (sii.upIndicatorView != null) { | |
sii.upIndicatorView.setImageDrawable(drawable); | |
} else { | |
Log.w(TAG, "Couldn't set home-as-up indicator"); | |
} | |
return info; | |
} | |
public static Object setActionBarDescription(Object info, Activity activity, | |
int contentDescRes) { | |
if (info == null) { | |
info = new SetIndicatorInfo(activity); | |
} | |
final SetIndicatorInfo sii = (SetIndicatorInfo) info; | |
if (sii.setHomeAsUpIndicator != null) { | |
try { | |
final ActionBar actionBar = activity.getActionBar(); | |
sii.setHomeActionContentDescription.invoke(actionBar, contentDescRes); | |
} catch (Exception e) { | |
Log.w(TAG, "Couldn't set content description via JB-MR2 API", e); | |
} | |
} | |
return info; | |
} | |
public static Drawable getThemeUpIndicator(Activity activity) { | |
final TypedArray a = activity.obtainStyledAttributes(THEME_ATTRS); | |
final Drawable result = a.getDrawable(0); | |
a.recycle(); | |
return result; | |
} | |
private static class SetIndicatorInfo { | |
public Method setHomeAsUpIndicator; | |
public Method setHomeActionContentDescription; | |
public ImageView upIndicatorView; | |
SetIndicatorInfo(Activity activity) { | |
try { | |
setHomeAsUpIndicator = ActionBar.class.getDeclaredMethod("setHomeAsUpIndicator", | |
Drawable.class); | |
setHomeActionContentDescription = ActionBar.class.getDeclaredMethod( | |
"setHomeActionContentDescription", Integer.TYPE); | |
// If we got the method we won't need the stuff below. | |
return; | |
} catch (NoSuchMethodException e) { | |
// Oh well. We'll use the other mechanism below instead. | |
} | |
final View home = activity.findViewById(android.R.id.home); | |
if (home == null) { | |
// Action bar doesn't have a known configuration, an OEM messed with things. | |
return; | |
} | |
final ViewGroup parent = (ViewGroup) home.getParent(); | |
final int childCount = parent.getChildCount(); | |
if (childCount != 2) { | |
// No idea which one will be the right one, an OEM messed with things. | |
return; | |
} | |
final View first = parent.getChildAt(0); | |
final View second = parent.getChildAt(1); | |
final View up = first.getId() == android.R.id.home ? second : first; | |
if (up instanceof ImageView) { | |
// Jackpot! (Probably...) | |
upIndicatorView = (ImageView) up; | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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.levibostian.customdurationnavigationdrawer.app; | |
import android.app.ActionBar; | |
import android.app.Activity; | |
import android.content.res.TypedArray; | |
import android.graphics.drawable.Drawable; | |
import com.levibostian.customdurationnavigationdrawer.R; | |
public class ActionBarDrawerToggleJellybeanMR2 { | |
private static final String TAG = "ActionBarDrawerToggleImplJellybeanMR2"; | |
private static final int[] THEME_ATTRS = new int[] { | |
R.attr.homeAsUpIndicator | |
}; | |
public static Object setActionBarUpIndicator(Object info, Activity activity, | |
Drawable drawable, int contentDescRes) { | |
final ActionBar actionBar = activity.getActionBar(); | |
if (actionBar != null) { | |
actionBar.setHomeAsUpIndicator(drawable); | |
actionBar.setHomeActionContentDescription(contentDescRes); | |
} | |
return info; | |
} | |
public static Object setActionBarDescription(Object info, Activity activity, | |
int contentDescRes) { | |
final ActionBar actionBar = activity.getActionBar(); | |
if (actionBar != null) { | |
actionBar.setHomeActionContentDescription(contentDescRes); | |
} | |
return info; | |
} | |
public static Drawable getThemeUpIndicator(Activity activity) { | |
final TypedArray a = activity.obtainStyledAttributes(THEME_ATTRS); | |
final Drawable result = a.getDrawable(0); | |
a.recycle(); | |
return result; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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.levibostian.customdurationnavigationdrawer.app; | |
import android.app.Activity; | |
import android.content.res.Configuration; | |
import android.graphics.Canvas; | |
import android.graphics.Rect; | |
import android.graphics.drawable.Drawable; | |
import android.graphics.drawable.InsetDrawable; | |
import android.os.Build; | |
import android.support.v4.view.GravityCompat; | |
import android.support.v4.view.ViewCompat; | |
import android.support.v4.widget.DrawerLayout; | |
import android.view.MenuItem; | |
import android.view.View; | |
import com.levibostian.customdurationnavigationdrawer.widget.CustomDurationDrawerLayout; | |
public class CustomDurationActionBarDrawerToggle implements CustomDurationDrawerLayout.DrawerListener { | |
/** | |
* Allows an implementing Activity to return an {@link ActionBarDrawerToggle.Delegate} to use | |
* with ActionBarDrawerToggle. | |
*/ | |
public interface DelegateProvider { | |
/** | |
* @return Delegate to use for ActionBarDrawableToggles, or null if the Activity | |
* does not wish to override the default behavior. | |
*/ | |
Delegate getDrawerToggleDelegate(); | |
} | |
public interface Delegate { | |
/** | |
* @return Up indicator drawable as defined in the Activity's theme, or null if one is not | |
* defined. | |
*/ | |
Drawable getThemeUpIndicator(); | |
/** | |
* Set the Action Bar's up indicator drawable and content description. | |
* | |
* @param upDrawable - Drawable to set as up indicator | |
* @param contentDescRes - Content description to set | |
*/ | |
void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes); | |
/** | |
* Set the Action Bar's up indicator content description. | |
* | |
* @param contentDescRes - Content description to set | |
*/ | |
void setActionBarDescription(int contentDescRes); | |
} | |
private interface ActionBarDrawerToggleImpl { | |
Drawable getThemeUpIndicator(Activity activity); | |
Object setActionBarUpIndicator(Object info, Activity activity, | |
Drawable themeImage, int contentDescRes); | |
Object setActionBarDescription(Object info, Activity activity, int contentDescRes); | |
} | |
private static class ActionBarDrawerToggleImplBase implements ActionBarDrawerToggleImpl { | |
@Override | |
public Drawable getThemeUpIndicator(Activity activity) { | |
return null; | |
} | |
@Override | |
public Object setActionBarUpIndicator(Object info, Activity activity, | |
Drawable themeImage, int contentDescRes) { | |
// No action bar to set. | |
return info; | |
} | |
@Override | |
public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) { | |
// No action bar to set | |
return info; | |
} | |
} | |
private static class ActionBarDrawerToggleImplHC implements ActionBarDrawerToggleImpl { | |
@Override | |
public Drawable getThemeUpIndicator(Activity activity) { | |
return ActionBarDrawerToggleHoneycomb.getThemeUpIndicator(activity); | |
} | |
@Override | |
public Object setActionBarUpIndicator(Object info, Activity activity, | |
Drawable themeImage, int contentDescRes) { | |
return ActionBarDrawerToggleHoneycomb.setActionBarUpIndicator(info, activity, | |
themeImage, contentDescRes); | |
} | |
@Override | |
public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) { | |
return ActionBarDrawerToggleHoneycomb.setActionBarDescription(info, activity, | |
contentDescRes); | |
} | |
} | |
private static class ActionBarDrawerToggleImplJellybeanMR2 | |
implements ActionBarDrawerToggleImpl { | |
@Override | |
public Drawable getThemeUpIndicator(Activity activity) { | |
return ActionBarDrawerToggleJellybeanMR2.getThemeUpIndicator(activity); | |
} | |
@Override | |
public Object setActionBarUpIndicator(Object info, Activity activity, | |
Drawable themeImage, int contentDescRes) { | |
return ActionBarDrawerToggleJellybeanMR2.setActionBarUpIndicator(info, activity, | |
themeImage, contentDescRes); | |
} | |
@Override | |
public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) { | |
return ActionBarDrawerToggleJellybeanMR2.setActionBarDescription(info, activity, | |
contentDescRes); | |
} | |
} | |
private static final ActionBarDrawerToggleImpl IMPL; | |
static { | |
final int version = Build.VERSION.SDK_INT; | |
if (version >= 18) { | |
IMPL = new ActionBarDrawerToggleImplJellybeanMR2(); | |
} else if (version >= 11) { | |
IMPL = new ActionBarDrawerToggleImplHC(); | |
} else { | |
IMPL = new ActionBarDrawerToggleImplBase(); | |
} | |
} | |
/** Fraction of its total width by which to offset the toggle drawable. */ | |
private static final float TOGGLE_DRAWABLE_OFFSET = 1 / 3f; | |
// android.R.id.home as defined by public API in v11 | |
private static final int ID_HOME = 0x0102002c; | |
private final Activity mActivity; | |
private final Delegate mActivityImpl; | |
private final CustomDurationDrawerLayout mDrawerLayout; | |
private boolean mDrawerIndicatorEnabled = true; | |
private Drawable mThemeImage; | |
private Drawable mDrawerImage; | |
private SlideDrawable mSlider; | |
private final int mDrawerImageResource; | |
private final int mOpenDrawerContentDescRes; | |
private final int mCloseDrawerContentDescRes; | |
private Object mSetIndicatorInfo; | |
/** | |
* Construct a new ActionBarDrawerToggle. | |
* | |
* <p>The given {@link Activity} will be linked to the specified {@link DrawerLayout}. | |
* The provided drawer indicator drawable will animate slightly off-screen as the drawer | |
* is opened, indicating that in the open state the drawer will move off-screen when pressed | |
* and in the closed state the drawer will move on-screen when pressed.</p> | |
* | |
* <p>String resources must be provided to describe the open/close drawer actions for | |
* accessibility services.</p> | |
* | |
* @param activity The Activity hosting the drawer | |
* @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar | |
* @param drawerImageRes A Drawable resource to use as the drawer indicator | |
* @param openDrawerContentDescRes A String resource to describe the "open drawer" action | |
* for accessibility | |
* @param closeDrawerContentDescRes A String resource to describe the "close drawer" action | |
* for accessibility | |
*/ | |
public CustomDurationActionBarDrawerToggle(Activity activity, CustomDurationDrawerLayout drawerLayout, | |
int drawerImageRes, int openDrawerContentDescRes, int closeDrawerContentDescRes) { | |
mActivity = activity; | |
// Allow the Activity to provide an impl | |
if (activity instanceof DelegateProvider) { | |
mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate(); | |
} else { | |
mActivityImpl = null; | |
} | |
mDrawerLayout = drawerLayout; | |
mDrawerImageResource = drawerImageRes; | |
mOpenDrawerContentDescRes = openDrawerContentDescRes; | |
mCloseDrawerContentDescRes = closeDrawerContentDescRes; | |
mThemeImage = getThemeUpIndicator(); | |
mDrawerImage = activity.getResources().getDrawable(drawerImageRes); | |
mSlider = new SlideDrawable(mDrawerImage); | |
mSlider.setOffset(TOGGLE_DRAWABLE_OFFSET); | |
} | |
/** | |
* Synchronize the state of the drawer indicator/affordance with the linked DrawerLayout. | |
* | |
* <p>This should be called from your <code>Activity</code>'s | |
* {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} method to synchronize after | |
* the DrawerLayout's instance state has been restored, and any other time when the state | |
* may have diverged in such a way that the ActionBarDrawerToggle was not notified. | |
* (For example, if you stop forwarding appropriate drawer events for a period of time.)</p> | |
*/ | |
public void syncState() { | |
if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { | |
mSlider.setPosition(1); | |
} else { | |
mSlider.setPosition(0); | |
} | |
if (mDrawerIndicatorEnabled) { | |
setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ? | |
mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); | |
} | |
} | |
/** | |
* Enable or disable the drawer indicator. The indicator defaults to enabled. | |
* | |
* <p>When the indicator is disabled, the <code>ActionBar</code> will revert to displaying | |
* the home-as-up indicator provided by the <code>Activity</code>'s theme in the | |
* <code>android.R.attr.homeAsUpIndicator</code> attribute instead of the animated | |
* drawer glyph.</p> | |
* | |
* @param enable true to enable, false to disable | |
*/ | |
public void setDrawerIndicatorEnabled(boolean enable) { | |
if (enable != mDrawerIndicatorEnabled) { | |
if (enable) { | |
setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ? | |
mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); | |
} else { | |
setActionBarUpIndicator(mThemeImage, 0); | |
} | |
mDrawerIndicatorEnabled = enable; | |
} | |
} | |
/** | |
* @return true if the enhanced drawer indicator is enabled, false otherwise | |
* @see #setDrawerIndicatorEnabled(boolean) | |
*/ | |
public boolean isDrawerIndicatorEnabled() { | |
return mDrawerIndicatorEnabled; | |
} | |
/** | |
* This method should always be called by your <code>Activity</code>'s | |
* {@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged} | |
* method. | |
* | |
* @param newConfig The new configuration | |
*/ | |
public void onConfigurationChanged(Configuration newConfig) { | |
// Reload drawables that can change with configuration | |
mThemeImage = getThemeUpIndicator(); | |
mDrawerImage = mActivity.getResources().getDrawable(mDrawerImageResource); | |
syncState(); | |
} | |
/** | |
* This method should be called by your <code>Activity</code>'s | |
* {@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected} method. | |
* If it returns true, your <code>onOptionsItemSelected</code> method should return true and | |
* skip further processing. | |
* | |
* @param item the MenuItem instance representing the selected menu item | |
* @return true if the event was handled and further processing should not occur | |
*/ | |
public boolean onOptionsItemSelected(MenuItem item) { | |
if (item != null && item.getItemId() == ID_HOME && mDrawerIndicatorEnabled) { | |
if (mDrawerLayout.isDrawerVisible(GravityCompat.START)) { | |
mDrawerLayout.closeDrawer(GravityCompat.START); | |
} else { | |
mDrawerLayout.openDrawer(GravityCompat.START); | |
} | |
return true; | |
} | |
return false; | |
} | |
/** | |
* {@link DrawerLayout.DrawerListener} callback method. If you do not use your | |
* ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call | |
* through to this method from your own listener object. | |
* | |
* @param drawerView The child view that was moved | |
* @param slideOffset The new offset of this drawer within its range, from 0-1 | |
*/ | |
@Override | |
public void onDrawerSlide(View drawerView, float slideOffset) { | |
float glyphOffset = mSlider.getPosition(); | |
if (slideOffset > 0.5f) { | |
glyphOffset = Math.max(glyphOffset, Math.max(0.f, slideOffset - 0.5f) * 2); | |
} else { | |
glyphOffset = Math.min(glyphOffset, slideOffset * 2); | |
} | |
mSlider.setPosition(glyphOffset); | |
} | |
/** | |
* {@link DrawerLayout.DrawerListener} callback method. If you do not use your | |
* ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call | |
* through to this method from your own listener object. | |
* | |
* @param drawerView Drawer view that is now open | |
*/ | |
@Override | |
public void onDrawerOpened(View drawerView) { | |
mSlider.setPosition(1); | |
if (mDrawerIndicatorEnabled) { | |
setActionBarDescription(mCloseDrawerContentDescRes); | |
} | |
} | |
/** | |
* {@link DrawerLayout.DrawerListener} callback method. If you do not use your | |
* ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call | |
* through to this method from your own listener object. | |
* | |
* @param drawerView Drawer view that is now closed | |
*/ | |
@Override | |
public void onDrawerClosed(View drawerView) { | |
mSlider.setPosition(0); | |
if (mDrawerIndicatorEnabled) { | |
setActionBarDescription(mOpenDrawerContentDescRes); | |
} | |
} | |
/** | |
* {@link DrawerLayout.DrawerListener} callback method. If you do not use your | |
* ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call | |
* through to this method from your own listener object. | |
* | |
* @param newState The new drawer motion state | |
*/ | |
@Override | |
public void onDrawerStateChanged(int newState) { | |
} | |
Drawable getThemeUpIndicator() { | |
if (mActivityImpl != null) { | |
return mActivityImpl.getThemeUpIndicator(); | |
} | |
return IMPL.getThemeUpIndicator(mActivity); | |
} | |
void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) { | |
if (mActivityImpl != null) { | |
mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes); | |
return; | |
} | |
mSetIndicatorInfo = IMPL | |
.setActionBarUpIndicator(mSetIndicatorInfo, mActivity, upDrawable, contentDescRes); | |
} | |
void setActionBarDescription(int contentDescRes) { | |
if (mActivityImpl != null) { | |
mActivityImpl.setActionBarDescription(contentDescRes); | |
return; | |
} | |
mSetIndicatorInfo = IMPL | |
.setActionBarDescription(mSetIndicatorInfo, mActivity, contentDescRes); | |
} | |
private class SlideDrawable extends InsetDrawable implements Drawable.Callback { | |
private final boolean mHasMirroring = Build.VERSION.SDK_INT > 18; | |
private final Rect mTmpRect = new Rect(); | |
private float mPosition; | |
private float mOffset; | |
private SlideDrawable(Drawable wrapped) { | |
super(wrapped, 0); | |
} | |
/** | |
* Sets the current position along the offset. | |
* | |
* @param position a value between 0 and 1 | |
*/ | |
public void setPosition(float position) { | |
mPosition = position; | |
invalidateSelf(); | |
} | |
public float getPosition() { | |
return mPosition; | |
} | |
/** | |
* Specifies the maximum offset when the position is at 1. | |
* | |
* @param offset maximum offset as a fraction of the drawable width, | |
* positive to shift left or negative to shift right. | |
* @see #setPosition(float) | |
*/ | |
public void setOffset(float offset) { | |
mOffset = offset; | |
invalidateSelf(); | |
} | |
@Override | |
public void draw(Canvas canvas) { | |
copyBounds(mTmpRect); | |
canvas.save(); | |
// Layout direction must be obtained from the activity. | |
final boolean isLayoutRTL = ViewCompat.getLayoutDirection(mActivity.getWindow().getDecorView()) == ViewCompat.LAYOUT_DIRECTION_RTL; | |
final int flipRtl = isLayoutRTL ? -1 : 1; | |
final int width = mTmpRect.width(); | |
canvas.translate(-mOffset * width * mPosition * flipRtl, 0); | |
// Force auto-mirroring if it's not supported by the platform. | |
if (isLayoutRTL && !mHasMirroring) { | |
canvas.translate(width, 0); | |
canvas.scale(-1, 1); | |
} | |
super.draw(canvas); | |
canvas.restore(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
_/* | |
* 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.levibostian.customdurationnavigationdrawer.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.support.v4.widget.DrawerLayout; | |
import android.support.v4.widget.ViewDragHelper; | |
import android.util.AttributeSet; | |
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; | |
import java.util.List; | |
public class CustomDurationDrawerLayout 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 CustomDurationViewDragHelper mLeftDragger; | |
private final CustomDurationViewDragHelper 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 CharSequence mTitleLeft; | |
private CharSequence mTitleRight; | |
/** | |
* 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 CustomDurationDrawerLayout(Context context) { | |
this(context, null); | |
} | |
public CustomDurationDrawerLayout(Context context, AttributeSet attrs) { | |
this(context, attrs, 0); | |
} | |
public CustomDurationDrawerLayout(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 = CustomDurationViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mLeftCallback); | |
mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); | |
mLeftDragger.setMinVelocity(minVel); | |
mLeftCallback.setDragger(mLeftDragger); | |
mRightDragger = CustomDurationViewDragHelper.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. | |
* | |
* <p>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.</p> | |
* | |
* <p>Locking drawers open or closed will implicitly open or close | |
* any drawers as appropriate.</p> | |
* | |
* @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. | |
* | |
* <p>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.</p> | |
* | |
* <p>Locking a drawer open or closed will implicitly open or close | |
* that drawer as appropriate.</p> | |
* | |
* @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 CustomDurationViewDragHelper 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. | |
* | |
* <p>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.</p> | |
* | |
* <p>Locking a drawer open or closed will implicitly open or close | |
* that drawer as appropriate.</p> | |
* | |
* @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; | |
} | |
/** | |
* Sets the title of the drawer with the given gravity. | |
* <p> | |
* When accessibility is turned on, this is the title that will be used to | |
* identify the drawer to the active accessibility service. | |
* | |
* @param edgeGravity Gravity.LEFT, RIGHT, START or END. Expresses which | |
* drawer to set the title for. | |
* @param title The title for the drawer. | |
*/ | |
public void setDrawerTitle(int edgeGravity, CharSequence title) { | |
final int absGravity = GravityCompat.getAbsoluteGravity( | |
edgeGravity, ViewCompat.getLayoutDirection(this)); | |
if (absGravity == Gravity.LEFT) { | |
mTitleLeft = title; | |
} else if (absGravity == Gravity.RIGHT) { | |
mTitleRight = title; | |
} | |
} | |
/** | |
* Returns the title of the drawer with the given gravity. | |
* | |
* @param edgeGravity Gravity.LEFT, RIGHT, START or END. Expresses which | |
* drawer to return the title for. | |
* @return The title of the drawer, or null if none set. | |
* @see #setDrawerTitle(int, CharSequence) | |
*/ | |
public CharSequence getDrawerTitle(int edgeGravity) { | |
final int absGravity = GravityCompat.getAbsoluteGravity( | |
edgeGravity, ViewCompat.getLayoutDirection(this)); | |
if (absGravity == Gravity.LEFT) { | |
return mTitleLeft; | |
} else if (absGravity == Gravity.RIGHT) { | |
return mTitleRight; | |
} | |
return null; | |
} | |
/** | |
* 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); | |
} | |
// Only send WINDOW_STATE_CHANGE if the host has window focus. This | |
// may change if support for multiple foreground windows (e.g. IME) | |
// improves. | |
if (hasWindowFocus()) { | |
final View rootView = getRootView(); | |
if (rootView != null) { | |
rootView.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); | |
} | |
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; | |
switch (action) { | |
case MotionEvent.ACTION_DOWN: { | |
final float x = ev.getX(); | |
final float y = ev.getY(); | |
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(); | |
} | |
break; | |
} | |
case MotionEvent.ACTION_CANCEL: | |
case MotionEvent.ACTION_UP: { | |
closeDrawers(true); | |
mDisallowInterceptRequested = false; | |
mChildrenCanceledTouch = false; | |
} | |
} | |
return interceptForDrag || interceptForTap || hasPeekingDrawer() || mChildrenCanceledTouch; | |
} | |
@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<SavedState> CREATOR = | |
new Parcelable.Creator<SavedState>() { | |
@Override | |
public SavedState createFromParcel(Parcel source) { | |
return new SavedState(source); | |
} | |
@Override | |
public SavedState[] newArray(int size) { | |
return new SavedState[size]; | |
} | |
}; | |
} | |
private class ViewDragCallback extends CustomDurationViewDragHelper.Callback { | |
private final int mAbsGravity; | |
private CustomDurationViewDragHelper mDragger; | |
private final Runnable mPeekRunnable = new Runnable() { | |
@Override public void run() { | |
peekDrawer(); | |
} | |
}; | |
public ViewDragCallback(int gravity) { | |
mAbsGravity = gravity; | |
} | |
public void setDragger(CustomDurationViewDragHelper dragger) { | |
mDragger = dragger; | |
} | |
public void removeCallbacks() { | |
CustomDurationDrawerLayout.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) { | |
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.setClassName(DrawerLayout.class.getName()); | |
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); | |
} | |
@Override | |
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { | |
super.onInitializeAccessibilityEvent(host, event); | |
event.setClassName(DrawerLayout.class.getName()); | |
} | |
@Override | |
public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { | |
// Special case to handle window state change events. As far as | |
// accessibility services are concerned, state changes from | |
// DrawerLayout invalidate the entire contents of the screen (like | |
// an Activity or Dialog) and they should announce the title of the | |
// new content. | |
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { | |
final List<CharSequence> eventText = event.getText(); | |
final View visibleDrawer = findVisibleDrawer(); | |
if (visibleDrawer != null) { | |
final int edgeGravity = getDrawerViewAbsoluteGravity(visibleDrawer); | |
final CharSequence title = getDrawerTitle(edgeGravity); | |
if (title != null) { | |
eventText.add(title); | |
} | |
} | |
return true; | |
} | |
return super.dispatchPopulateAccessibilityEvent(host, event); | |
} | |
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()); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
_/* | |
* 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.levibostian.customdurationnavigationdrawer.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.support.v4.widget.ViewDragHelper; | |
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; | |
public class CustomDurationViewDragHelper { | |
private static final int MAX_SETTLE_DURATION = 0; // ms | |
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; | |
private static final int EDGE_SIZE = 20; // dp | |
private static final int BASE_SETTLE_DURATION = 256; // 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 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. <code>on*</code>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 <code>STATE_*</code> 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. | |
* | |
* <p>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 <code>onViewReleased</code> returns, | |
* the view will stop in place and the ViewDragHelper will return to | |
* {@link #STATE_IDLE}.</p> | |
* | |
* @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 <code>index</code> | |
*/ | |
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. | |
* | |
* <p>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.</p> | |
* | |
* <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)} | |
* will follow if the capture is successful.</p> | |
* | |
* @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 CustomDurationViewDragHelper create(ViewGroup forParent, Callback cb) { | |
return new CustomDurationViewDragHelper(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 CustomDurationViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { | |
final CustomDurationViewDragHelper 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 CustomDurationViewDragHelper(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; | |
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 <code>child</code> 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. | |
* | |
* <p>This operation does not count as a capture event, though {@link #getCapturedView()} | |
* will still report the sliding view while the slide is in progress.</p> | |
* | |
* @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 <code>value</code> | |
*/ | |
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 <code>value</code> | |
*/ | |
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 <code>continueSettling</code> 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 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). | |
* | |
* <p>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.</p> | |
* | |
* @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); | |
saveInitialMotion(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); | |
saveInitialMotion(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) { | |
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 (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; | |
} | |
} | |
} | |
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. | |
* | |
* <p>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.</p> | |
* | |
* @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. | |
* | |
* <p>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.</p> | |
* | |
* @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; | |
if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT; | |
if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP; | |
if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT; | |
if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM; | |
return result; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- A DrawerLayout is intended to be used as the top-level content view using match_parent for both width and height to consume the full space available. --> | |
<com.levibostian.customdurationnavigationdrawer.widget.CustomDurationDrawerLayout | |
xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:id="@+id/drawer_layout" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
tools:context=".MainActivity"> | |
<FrameLayout | |
android:id="@+id/container" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" /> | |
<fragment android:id="@+id/navigation_drawer" | |
android:layout_width="@dimen/navigation_drawer_width" | |
android:layout_height="match_parent" | |
android:layout_gravity="start" | |
android:name="com.levibostian.customdurationnavigationdrawer.fragment.NavigationDrawerFragment" | |
tools:layout="@layout/navigation_drawer" /> | |
</com.levibostian.customdurationnavigationdrawer.widget.CustomDurationDrawerLayout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.levibostian.customdurationnavigationdrawer.activity; | |
import android.app.Activity; | |
import android.support.v7.app.ActionBarActivity; | |
import android.support.v7.app.ActionBar; | |
import android.support.v4.app.Fragment; | |
import android.support.v4.app.FragmentManager; | |
import android.os.Bundle; | |
import android.view.LayoutInflater; | |
import android.view.Menu; | |
import android.view.MenuItem; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import com.levibostian.customdurationnavigationdrawer.fragment.NavigationDrawerFragment; | |
import com.levibostian.customdurationnavigationdrawer.R; | |
import com.levibostian.customdurationnavigationdrawer.widget.CustomDurationDrawerLayout; | |
public class MainActivity extends ActionBarActivity implements NavigationDrawerFragment.NavigationDrawerCallbacks { | |
private NavigationDrawerFragment mNavigationDrawerFragment; | |
@Override | |
protected void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setContentView(R.layout.main_activity); | |
mNavigationDrawerFragment = (NavigationDrawerFragment) getSupportFragmentManager().findFragmentById(R.id.navigation_drawer); | |
mNavigationDrawerFragment.setUp(R.id.navigation_drawer, (CustomDurationDrawerLayout) findViewById(R.id.drawer_layout)); | |
} | |
@Override | |
public void onNavigationDrawerItemSelected(int position) { | |
FragmentManager fragmentManager = getSupportFragmentManager(); | |
fragmentManager.beginTransaction() | |
.replace(R.id.container, PlaceholderFragment.newInstance(position + 1)) | |
.commit(); | |
} | |
public void restoreActionBar() { | |
ActionBar actionBar = getSupportActionBar(); | |
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); | |
actionBar.setDisplayShowTitleEnabled(true); | |
} | |
@Override | |
public boolean onCreateOptionsMenu(Menu menu) { | |
if (!mNavigationDrawerFragment.isDrawerOpen()) { | |
getMenuInflater().inflate(R.menu.main, menu); | |
restoreActionBar(); | |
return true; | |
} | |
return super.onCreateOptionsMenu(menu); | |
} | |
@Override | |
public boolean onOptionsItemSelected(MenuItem item) { | |
int id = item.getItemId(); | |
if (id == R.id.action_settings) { | |
return true; | |
} | |
return super.onOptionsItemSelected(item); | |
} | |
public static class PlaceholderFragment extends Fragment { | |
private static final String ARG_SECTION_NUMBER = "section_number"; | |
public static PlaceholderFragment newInstance(int sectionNumber) { | |
PlaceholderFragment fragment = new PlaceholderFragment(); | |
Bundle args = new Bundle(); | |
args.putInt(ARG_SECTION_NUMBER, sectionNumber); | |
fragment.setArguments(args); | |
return fragment; | |
} | |
public PlaceholderFragment() { | |
} | |
@Override | |
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | |
View rootView = inflater.inflate(R.layout.main_fragment, container, false); | |
return rootView; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You didnt provide dimens.xml and other layout files