Created
June 24, 2015 03:23
-
-
Save kyze8439690/a088f2f74434fbd06af4 to your computer and use it in GitHub Desktop.
NestedAppBarLayout, make AppBarLayout scrollable in CoordinatorLayout, merge code from NestedScrollView, lots of thing to improve...
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
import android.content.Context; | |
import android.support.annotation.NonNull; | |
import android.support.design.widget.AppBarLayout; | |
import android.support.v4.view.MotionEventCompat; | |
import android.support.v4.view.NestedScrollingChild; | |
import android.support.v4.view.NestedScrollingChildHelper; | |
import android.support.v4.view.VelocityTrackerCompat; | |
import android.support.v4.view.ViewCompat; | |
import android.support.v4.widget.ScrollerCompat; | |
import android.util.AttributeSet; | |
import android.util.Log; | |
import android.view.MotionEvent; | |
import android.view.VelocityTracker; | |
import android.view.ViewConfiguration; | |
import android.view.ViewParent; | |
public class NestedAppBarLayout extends AppBarLayout implements NestedScrollingChild { | |
private static final String TAG = "NestedAppBarLayout"; | |
private ScrollerCompat mScroller; | |
/** | |
* Position of the last motion event. | |
*/ | |
private int mLastMotionY; | |
/** | |
* True if the user is currently dragging this ScrollView around. This is | |
* not the same as 'is being flinged', which can be checked by | |
* mScroller.isFinished() (flinging begins when the user lifts his finger). | |
*/ | |
private boolean mIsBeingDragged = false; | |
/** | |
* Determines speed during touch scrolling | |
*/ | |
private VelocityTracker mVelocityTracker; | |
private int mTouchSlop; | |
private int mMinimumVelocity; | |
private int mMaximumVelocity; | |
/** | |
* ID of the active pointer. This is used to retain consistency during | |
* drags/flings if multiple pointers are used. | |
*/ | |
private int mActivePointerId = INVALID_POINTER; | |
/** | |
* Used during scrolling to retrieve the new offset within the window. | |
*/ | |
private final int[] mScrollOffset = new int[2]; | |
private int mNestedYOffset; | |
/** | |
* Sentinel value for no current active pointer. | |
* Used by {@link #mActivePointerId}. | |
*/ | |
private static final int INVALID_POINTER = -1; | |
private final NestedScrollingChildHelper mChildHelper; | |
public NestedAppBarLayout(Context context) { | |
this(context, null); | |
} | |
public NestedAppBarLayout(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
initScrollView(); | |
mChildHelper = new NestedScrollingChildHelper(this); | |
// ...because why else would you be using this widget? | |
setNestedScrollingEnabled(true); | |
} | |
// NestedScrollingChild | |
@Override | |
public void setNestedScrollingEnabled(boolean enabled) { | |
mChildHelper.setNestedScrollingEnabled(enabled); | |
} | |
@Override | |
public boolean isNestedScrollingEnabled() { | |
return mChildHelper.isNestedScrollingEnabled(); | |
} | |
@Override | |
public boolean startNestedScroll(int axes) { | |
return mChildHelper.startNestedScroll(axes); | |
} | |
@Override | |
public void stopNestedScroll() { | |
mChildHelper.stopNestedScroll(); | |
} | |
@Override | |
public boolean hasNestedScrollingParent() { | |
return mChildHelper.hasNestedScrollingParent(); | |
} | |
@Override | |
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, | |
int dyUnconsumed, int[] offsetInWindow) { | |
return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, | |
offsetInWindow); | |
} | |
@Override | |
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { | |
return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); | |
} | |
@Override | |
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { | |
return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); | |
} | |
@Override | |
public boolean dispatchNestedPreFling(float velocityX, float velocityY) { | |
return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); | |
} | |
private void initScrollView() { | |
mScroller = ScrollerCompat.create(getContext()); | |
setFocusable(true); | |
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); | |
final ViewConfiguration configuration = ViewConfiguration.get(getContext()); | |
mTouchSlop = configuration.getScaledTouchSlop(); | |
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); | |
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); | |
} | |
private void initOrResetVelocityTracker() { | |
if (mVelocityTracker == null) { | |
mVelocityTracker = VelocityTracker.obtain(); | |
} else { | |
mVelocityTracker.clear(); | |
} | |
} | |
private void initVelocityTrackerIfNotExists() { | |
if (mVelocityTracker == null) { | |
mVelocityTracker = VelocityTracker.obtain(); | |
} | |
} | |
private void recycleVelocityTracker() { | |
if (mVelocityTracker != null) { | |
mVelocityTracker.recycle(); | |
mVelocityTracker = null; | |
} | |
} | |
@Override | |
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { | |
if (disallowIntercept) { | |
recycleVelocityTracker(); | |
} | |
super.requestDisallowInterceptTouchEvent(disallowIntercept); | |
} | |
@Override | |
public boolean onInterceptTouchEvent(@NonNull MotionEvent ev) { | |
/* | |
* This method JUST determines whether we want to intercept the motion. | |
* If we return true, onMotionEvent will be called and we do the actual | |
* scrolling there. | |
*/ | |
/* | |
* Shortcut the most recurring case: the user is in the dragging | |
* state and he is moving his finger. We want to intercept this | |
* motion. | |
*/ | |
final int action = ev.getAction(); | |
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { | |
return true; | |
} | |
switch (action & MotionEventCompat.ACTION_MASK) { | |
case MotionEvent.ACTION_MOVE: { | |
/* | |
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check | |
* whether the user has moved far enough from his original down touch. | |
*/ | |
/* | |
* Locally do absolute value. mLastMotionY is set to the y value | |
* of the down event. | |
*/ | |
final int activePointerId = mActivePointerId; | |
if (activePointerId == INVALID_POINTER) { | |
// If we don't have a valid id, the touch down wasn't on content. | |
break; | |
} | |
final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); | |
if (pointerIndex == -1) { | |
Log.e(TAG, "Invalid pointerId=" + activePointerId | |
+ " in onInterceptTouchEvent"); | |
break; | |
} | |
final int y = (int) MotionEventCompat.getY(ev, pointerIndex); | |
final int yDiff = Math.abs(y - mLastMotionY); | |
if (yDiff > mTouchSlop) { | |
mIsBeingDragged = true; | |
mLastMotionY = y; | |
initVelocityTrackerIfNotExists(); | |
mVelocityTracker.addMovement(ev); | |
mNestedYOffset = 0; | |
final ViewParent parent = getParent(); | |
if (parent != null) { | |
parent.requestDisallowInterceptTouchEvent(true); | |
} | |
} | |
break; | |
} | |
case MotionEvent.ACTION_DOWN: { | |
/* | |
* Remember location of down touch. | |
* ACTION_DOWN always refers to pointer index 0. | |
*/ | |
mLastMotionY = (int) ev.getY(); | |
mActivePointerId = MotionEventCompat.getPointerId(ev, 0); | |
initOrResetVelocityTracker(); | |
mVelocityTracker.addMovement(ev); | |
/* | |
* If being flinged and user touches the screen, initiate drag; | |
* otherwise don't. mScroller.isFinished should be false when | |
* being flinged. | |
*/ | |
mIsBeingDragged = !mScroller.isFinished(); | |
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); | |
break; | |
} | |
case MotionEvent.ACTION_CANCEL: | |
case MotionEvent.ACTION_UP: | |
/* Release the drag */ | |
mIsBeingDragged = false; | |
mActivePointerId = INVALID_POINTER; | |
recycleVelocityTracker(); | |
stopNestedScroll(); | |
break; | |
case MotionEventCompat.ACTION_POINTER_UP: | |
onSecondaryPointerUp(ev); | |
break; | |
} | |
/* | |
* The only time we want to intercept motion events is if we are in the | |
* drag mode. | |
*/ | |
return mIsBeingDragged; | |
} | |
@Override | |
public boolean onTouchEvent(@NonNull MotionEvent ev) { | |
initVelocityTrackerIfNotExists(); | |
MotionEvent vtev = MotionEvent.obtain(ev); | |
final int actionMasked = MotionEventCompat.getActionMasked(ev); | |
if (actionMasked == MotionEvent.ACTION_DOWN) { | |
mNestedYOffset = 0; | |
} | |
vtev.offsetLocation(0, mNestedYOffset); | |
switch (actionMasked) { | |
case MotionEvent.ACTION_DOWN: { | |
if (getChildCount() == 0) { | |
return false; | |
} | |
if ((mIsBeingDragged = !mScroller.isFinished())) { | |
final ViewParent parent = getParent(); | |
if (parent != null) { | |
parent.requestDisallowInterceptTouchEvent(true); | |
} | |
} | |
/* | |
* If being flinged and user touches, stop the fling. isFinished | |
* will be false if being flinged. | |
*/ | |
if (!mScroller.isFinished()) { | |
mScroller.abortAnimation(); | |
} | |
// Remember where the motion event started | |
mLastMotionY = (int) ev.getY(); | |
mActivePointerId = MotionEventCompat.getPointerId(ev, 0); | |
startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); | |
break; | |
} | |
case MotionEvent.ACTION_MOVE: | |
final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, | |
mActivePointerId); | |
if (activePointerIndex == -1) { | |
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); | |
break; | |
} | |
final int y = (int) MotionEventCompat.getY(ev, activePointerIndex); | |
int deltaY = mLastMotionY - y; | |
if (dispatchNestedPreScroll(0, deltaY, null, mScrollOffset)) { | |
vtev.offsetLocation(0, mScrollOffset[1]); | |
mNestedYOffset += mScrollOffset[1]; | |
} | |
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { | |
final ViewParent parent = getParent(); | |
if (parent != null) { | |
parent.requestDisallowInterceptTouchEvent(true); | |
} | |
mIsBeingDragged = true; | |
if (deltaY > 0) { | |
deltaY -= mTouchSlop; | |
} else { | |
deltaY += mTouchSlop; | |
} | |
} | |
if (mIsBeingDragged) { | |
// Scroll to follow the motion event | |
mLastMotionY = y - mScrollOffset[1]; | |
// Calling overScrollByCompat will call onOverScrolled, which | |
// calls onScrollChanged if applicable. | |
if (!hasNestedScrollingParent()) { | |
// Break our velocity if we hit a scroll barrier. | |
mVelocityTracker.clear(); | |
} | |
if (dispatchNestedScroll(0, 0, 0, deltaY, mScrollOffset)) { | |
mLastMotionY -= mScrollOffset[1]; | |
vtev.offsetLocation(0, mScrollOffset[1]); | |
mNestedYOffset += mScrollOffset[1]; | |
} | |
} | |
break; | |
case MotionEvent.ACTION_UP: | |
if (mIsBeingDragged) { | |
final VelocityTracker velocityTracker = mVelocityTracker; | |
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); | |
int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker, | |
mActivePointerId); | |
if ((Math.abs(initialVelocity) > mMinimumVelocity)) { | |
flingWithNestedDispatch(-initialVelocity); | |
} | |
mActivePointerId = INVALID_POINTER; | |
endDrag(); | |
} | |
break; | |
case MotionEvent.ACTION_CANCEL: | |
if (mIsBeingDragged && getChildCount() > 0) { | |
mActivePointerId = INVALID_POINTER; | |
endDrag(); | |
} | |
break; | |
case MotionEventCompat.ACTION_POINTER_DOWN: { | |
final int index = MotionEventCompat.getActionIndex(ev); | |
mLastMotionY = (int) MotionEventCompat.getY(ev, index); | |
mActivePointerId = MotionEventCompat.getPointerId(ev, index); | |
break; | |
} | |
case MotionEventCompat.ACTION_POINTER_UP: | |
onSecondaryPointerUp(ev); | |
mLastMotionY = (int) MotionEventCompat.getY(ev, | |
MotionEventCompat.findPointerIndex(ev, mActivePointerId)); | |
break; | |
} | |
if (mVelocityTracker != null) { | |
mVelocityTracker.addMovement(vtev); | |
} | |
vtev.recycle(); | |
return true; | |
} | |
private void onSecondaryPointerUp(MotionEvent ev) { | |
final int pointerIndex = (ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK) >> | |
MotionEventCompat.ACTION_POINTER_INDEX_SHIFT; | |
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); | |
if (pointerId == mActivePointerId) { | |
// This was our active pointer going up. Choose a new | |
// active pointer and adjust accordingly. | |
// TODO: Make this decision more intelligent. | |
final int newPointerIndex = pointerIndex == 0 ? 1 : 0; | |
mLastMotionY = (int) MotionEventCompat.getY(ev, newPointerIndex); | |
mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); | |
if (mVelocityTracker != null) { | |
mVelocityTracker.clear(); | |
} | |
} | |
} | |
@Override | |
protected int computeVerticalScrollOffset() { | |
return Math.max(0, super.computeVerticalScrollOffset()); | |
} | |
/** | |
* Fling the scroll view | |
* | |
* @param velocityY The initial velocity in the Y direction. Positive | |
* numbers mean that the finger/cursor is moving down the screen, | |
* which means we want to scroll towards the top. | |
*/ | |
public void fling(int velocityY) { | |
mScroller.fling(0, 0, 0, velocityY, 0, 0, 0, 0); | |
} | |
private void flingWithNestedDispatch(int velocityY) { | |
if (!dispatchNestedPreFling(0, velocityY)) { | |
dispatchNestedFling(0, velocityY, true); | |
fling(velocityY); | |
} | |
} | |
private void endDrag() { | |
mIsBeingDragged = false; | |
recycleVelocityTracker(); | |
} | |
} |
Very usefull. Appreciate it!
Fixed a scroll bug I've spent too much time on, thank you!
发现没有效果啊,需要特殊处理什么么?
<com.tmall.wireless.tangram.example.cc.NestedAppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#c07"
app:layout_scrollFlags="scroll|enterAlways" />
</com.tmall.wireless.tangram.example.cc.NestedAppBarLayout>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
nice and helpful