557 lines
20 KiB
Java
557 lines
20 KiB
Java
/*
|
|
* Twidere - Twitter client for Android
|
|
*
|
|
* Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package org.mariotaku.twidere.view;
|
|
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Context;
|
|
import android.content.res.TypedArray;
|
|
import android.os.SystemClock;
|
|
import android.support.annotation.NonNull;
|
|
import android.support.v4.view.ViewCompat;
|
|
import android.support.v4.widget.ScrollerCompat;
|
|
import android.support.v4.widget.ViewDragHelper;
|
|
import android.util.AttributeSet;
|
|
import android.view.GestureDetector;
|
|
import android.view.GestureDetector.SimpleOnGestureListener;
|
|
import android.view.LayoutInflater;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
|
|
import org.mariotaku.twidere.R;
|
|
import org.mariotaku.twidere.util.TwidereMathUtils;
|
|
|
|
/**
|
|
* Custom ViewGroup for user profile page like Google+ but with tab swipe
|
|
*
|
|
* @author mariotaku
|
|
*/
|
|
public class HeaderDrawerLayout extends ViewGroup {
|
|
|
|
private final ViewDragHelper mDragHelper;
|
|
private final ScrollerCompat mScroller;
|
|
private final GestureDetector mGestureDetector;
|
|
|
|
private final InternalContainer mContainer;
|
|
private final DragCallback mDragCallback;
|
|
|
|
private DrawerCallback mDrawerCallback;
|
|
private boolean mUsingDragHelper;
|
|
private boolean mScrollingHeaderByGesture, mScrollingContentCallback;
|
|
private boolean mTouchDown, mTouchingScrollableContent;
|
|
|
|
private int mHeaderOffset;
|
|
private int mTop;
|
|
|
|
public HeaderDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
super(context, attrs, defStyleAttr);
|
|
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HeaderDrawerLayout);
|
|
final int headerLayoutId = a.getResourceId(R.styleable.HeaderDrawerLayout_hdl_headerLayout, 0);
|
|
final int contentLayoutId = a.getResourceId(R.styleable.HeaderDrawerLayout_hdl_contentLayout, 0);
|
|
addView(mContainer = new InternalContainer(this, context, headerLayoutId, contentLayoutId),
|
|
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
|
|
a.recycle();
|
|
mDragHelper = ViewDragHelper.create(this, mDragCallback = new DragCallback(this));
|
|
mGestureDetector = new GestureDetector(context, new GestureListener(this));
|
|
mScroller = ScrollerCompat.create(context);
|
|
}
|
|
|
|
public HeaderDrawerLayout(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public HeaderDrawerLayout(Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
@Override
|
|
public boolean dispatchTouchEvent(@NonNull MotionEvent ev) {
|
|
switch (ev.getAction()) {
|
|
case MotionEvent.ACTION_DOWN: {
|
|
mScroller.abortAnimation();
|
|
mTouchDown = true;
|
|
mTouchingScrollableContent = isScrollContentCallback(ev.getX(), ev.getY());
|
|
mUsingDragHelper = false;
|
|
break;
|
|
}
|
|
case MotionEvent.ACTION_CANCEL:
|
|
case MotionEvent.ACTION_UP: {
|
|
mTouchDown = false;
|
|
mTouchingScrollableContent = false;
|
|
mUsingDragHelper = false;
|
|
}
|
|
}
|
|
mGestureDetector.onTouchEvent(ev);
|
|
return super.dispatchTouchEvent(ev);
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
|
if (mDragHelper.shouldInterceptTouchEvent(ev) || mScrollingHeaderByGesture) {
|
|
mUsingDragHelper = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
for (int i = 0, j = getChildCount(); i < j; i++) {
|
|
final View child = getChildAt(i);
|
|
final int left = getPaddingLeft(), right = left + child.getMeasuredWidth();
|
|
final int top;
|
|
if (i == 0) {
|
|
if (shouldLayoutHeaderBottomCallback() && child.getHeight() != 0) {
|
|
final int heightDelta = child.getMeasuredHeight() - child.getHeight();
|
|
top = mHeaderOffset + getPaddingTop() - heightDelta;
|
|
} else {
|
|
top = mHeaderOffset + getPaddingTop();
|
|
}
|
|
} else {
|
|
top = getChildAt(i - 1).getBottom();
|
|
}
|
|
final int bottom = top + child.getMeasuredHeight();
|
|
child.layout(left, top, right, bottom);
|
|
notifyOffsetChanged();
|
|
}
|
|
}
|
|
|
|
public void flingHeader(float velocity) {
|
|
if (mTouchDown) {
|
|
mScroller.abortAnimation();
|
|
return;
|
|
}
|
|
mScroller.fling(0, getHeaderTop(), 0, (int) velocity, 0, 0,
|
|
mContainer.getHeaderTopMinimum(), mContainer.getHeaderTopMaximum());
|
|
ViewCompat.postInvalidateOnAnimation(this);
|
|
}
|
|
|
|
public View getContent() {
|
|
return mContainer.getContent();
|
|
}
|
|
|
|
public View getHeader() {
|
|
return mContainer.getHeader();
|
|
}
|
|
|
|
public int getHeaderTop() {
|
|
return mContainer.getTop();
|
|
}
|
|
|
|
@Override
|
|
public void computeScroll() {
|
|
boolean invalidate = mDragHelper.continueSettling(true);
|
|
if (!mTouchDown && mScroller.computeScrollOffset()) {
|
|
if (!invalidate) {
|
|
offsetHeaderBy(mScroller.getCurrY() - getHeaderTop());
|
|
}
|
|
invalidate = true;
|
|
}
|
|
updateViewOffset();
|
|
if (invalidate) {
|
|
ViewCompat.postInvalidateOnAnimation(this);
|
|
}
|
|
}
|
|
|
|
public int getHeaderTopMaximum() {
|
|
return mContainer.getHeaderTopMaximum();
|
|
}
|
|
|
|
public int getHeaderTopMinimum() {
|
|
return mContainer.getHeaderTopMinimum();
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(@NonNull MotionEvent event) {
|
|
mDragHelper.processTouchEvent(event);
|
|
return true;
|
|
}
|
|
|
|
public void setDrawerCallback(DrawerCallback callback) {
|
|
mDrawerCallback = callback;
|
|
}
|
|
|
|
private boolean canScrollCallback(float dy) {
|
|
return mDrawerCallback.canScroll(dy);
|
|
}
|
|
|
|
@Override
|
|
protected void onFinishInflate() {
|
|
super.onFinishInflate();
|
|
if (getChildCount() != 1) {
|
|
throw new IllegalArgumentException("Add subview by XML is not allowed.");
|
|
}
|
|
}
|
|
|
|
private void cancelTouchCallback() {
|
|
mDrawerCallback.cancelTouch();
|
|
}
|
|
|
|
private void flingCallback(float velocity) {
|
|
mDrawerCallback.fling(velocity);
|
|
}
|
|
|
|
private float getDragTouchSlop() {
|
|
return mDragHelper.getTouchSlop();
|
|
}
|
|
|
|
private int getScrollRange() {
|
|
return mContainer.getScrollRange();
|
|
}
|
|
|
|
private boolean isScrollContentCallback(float x, float y) {
|
|
return mDrawerCallback.isScrollContent(x, y);
|
|
}
|
|
|
|
private boolean isScrollingContentCallback() {
|
|
return mScrollingContentCallback;
|
|
}
|
|
|
|
private void setScrollingContentCallback(boolean scrolling) {
|
|
mScrollingContentCallback = scrolling;
|
|
}
|
|
|
|
private boolean isScrollingHeaderByHelper() {
|
|
return mDragCallback.isScrollingHeaderByHelper();
|
|
}
|
|
|
|
private boolean isTouchingScrollableContent() {
|
|
return mTouchingScrollableContent;
|
|
}
|
|
|
|
private boolean isUsingDragHelper() {
|
|
return mUsingDragHelper;
|
|
}
|
|
|
|
private boolean isValidScroll(float direction, float other) {
|
|
return Math.abs(direction) > getDragTouchSlop() && Math.abs(direction) > Math.abs(other);
|
|
}
|
|
|
|
private static int makeChildMeasureSpec(int spec, int padding) {
|
|
final int size = MeasureSpec.getSize(spec), mode = MeasureSpec.getMode(spec);
|
|
return MeasureSpec.makeMeasureSpec(size - padding, mode);
|
|
}
|
|
|
|
private void notifyOffsetChanged() {
|
|
final int top = getHeaderTop();
|
|
if (mTop == top) return;
|
|
mHeaderOffset = top - getPaddingTop();
|
|
mDrawerCallback.topChanged(top);
|
|
mTop = top;
|
|
}
|
|
|
|
private void offsetHeaderBy(int dy) {
|
|
final int prevTop = mContainer.getTop();
|
|
final int clampedDy = TwidereMathUtils.clamp(prevTop + dy, getHeaderTopMinimum(), getHeaderTopMaximum()) - prevTop;
|
|
mContainer.offsetTopAndBottom(clampedDy);
|
|
}
|
|
|
|
private void scrollByCallback(float dy) {
|
|
final int top = getHeaderTop();
|
|
setScrollingContentCallback(top > getHeaderTopMinimum() && top < getHeaderTopMaximum());
|
|
mDrawerCallback.scrollBy(dy);
|
|
}
|
|
|
|
private void setScrollingHeaderByGesture(boolean scrolling) {
|
|
mScrollingHeaderByGesture = scrolling;
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
final View child = getChildAt(0);
|
|
|
|
final int childWidthMeasureSpec = makeChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight());
|
|
final int childHeightMeasureSpec = makeChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom());
|
|
|
|
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
}
|
|
|
|
private boolean shouldLayoutHeaderBottomCallback() {
|
|
if (mDragCallback == null || isInEditMode()) return false;
|
|
return mDrawerCallback.shouldLayoutHeaderBottom();
|
|
}
|
|
|
|
private void updateViewOffset() {
|
|
}
|
|
|
|
public interface DrawerCallback {
|
|
|
|
boolean canScroll(float dy);
|
|
|
|
void cancelTouch();
|
|
|
|
void fling(float velocity);
|
|
|
|
boolean isScrollContent(float x, float y);
|
|
|
|
void scrollBy(float dy);
|
|
|
|
boolean shouldLayoutHeaderBottom();
|
|
|
|
void topChanged(int offset);
|
|
}
|
|
|
|
private static class DragCallback extends ViewDragHelper.Callback {
|
|
|
|
private final HeaderDrawerLayout mDrawer;
|
|
private long mTime;
|
|
private float mDx, mDy, mVelocity;
|
|
private boolean mScrollingHeaderByHelper;
|
|
|
|
public DragCallback(HeaderDrawerLayout drawer) {
|
|
mDrawer = drawer;
|
|
mTime = -1;
|
|
mDx = Float.NaN;
|
|
mDy = Float.NaN;
|
|
mVelocity = Float.NaN;
|
|
}
|
|
|
|
@Override
|
|
public void onViewDragStateChanged(int state) {
|
|
switch (state) {
|
|
case ViewDragHelper.STATE_SETTLING:
|
|
case ViewDragHelper.STATE_DRAGGING: {
|
|
mScrollingHeaderByHelper = false;
|
|
break;
|
|
}
|
|
case ViewDragHelper.STATE_IDLE: {
|
|
if (mTime > 0 && !Float.isNaN(mVelocity)) {
|
|
final float velocity = mVelocity;
|
|
if (velocity < 0 && mDrawer.getHeaderTop() <= mDrawer.getHeaderTopMinimum()) {
|
|
mDrawer.flingCallback(-velocity);
|
|
}
|
|
}
|
|
mTime = -1;
|
|
mDx = Float.NaN;
|
|
mDy = Float.NaN;
|
|
mVelocity = Float.NaN;
|
|
mScrollingHeaderByHelper = false;
|
|
break;
|
|
}
|
|
}
|
|
super.onViewDragStateChanged(state);
|
|
}
|
|
|
|
@Override
|
|
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
|
|
super.onViewPositionChanged(changedView, left, top, dx, dy);
|
|
final long time = SystemClock.uptimeMillis();
|
|
final float timeDelta = time - mTime;
|
|
mVelocity = mDy / timeDelta * 1000;
|
|
mTime = time;
|
|
mDy = dy;
|
|
}
|
|
|
|
@Override
|
|
public void onViewReleased(View releasedChild, float xvel, float yvel) {
|
|
mDrawer.mDragHelper.flingCapturedView(mDrawer.getPaddingLeft(),
|
|
mDrawer.getHeaderTopMinimum(), mDrawer.getPaddingLeft(),
|
|
mDrawer.getHeaderTopMaximum());
|
|
ViewCompat.postInvalidateOnAnimation(mDrawer);
|
|
}
|
|
|
|
@Override
|
|
public int getViewVerticalDragRange(View child) {
|
|
return mDrawer.getScrollRange();
|
|
}
|
|
|
|
@Override
|
|
public boolean tryCaptureView(View view, int pointerId) {
|
|
return view == mDrawer.mContainer;
|
|
}
|
|
|
|
@Override
|
|
public int clampViewPositionHorizontal(View child, int left, int dx) {
|
|
mDx = dx;
|
|
return mDrawer.getPaddingLeft();
|
|
}
|
|
|
|
@Override
|
|
public int clampViewPositionVertical(final View child, final int top, final int dy) {
|
|
final int current = mDrawer.getHeaderTop();
|
|
if (!Float.isNaN(mDx) && mDrawer.isValidScroll(mDx, dy)) {
|
|
mScrollingHeaderByHelper = false;
|
|
return current;
|
|
}
|
|
if (dy > 0 && mDrawer.canScrollCallback(-dy) && mDrawer.isTouchingScrollableContent()) {
|
|
if (!mDrawer.isUsingDragHelper()) {
|
|
// Scrolling up while list still has space to scroll, so make header still
|
|
mScrollingHeaderByHelper = false;
|
|
return current;
|
|
} else {
|
|
mDrawer.scrollByCallback(-dy);
|
|
mScrollingHeaderByHelper = false;
|
|
return current;
|
|
}
|
|
}
|
|
final int min = mDrawer.getHeaderTopMinimum(), max = mDrawer.getHeaderTopMaximum();
|
|
if (top < min && mDrawer.isTouchingScrollableContent() && mDrawer.isUsingDragHelper()) {
|
|
mDrawer.scrollByCallback(-dy);
|
|
}
|
|
mScrollingHeaderByHelper = true;
|
|
return TwidereMathUtils.clamp(top, min, max);
|
|
}
|
|
|
|
private boolean isScrollingHeaderByHelper() {
|
|
return mScrollingHeaderByHelper;
|
|
}
|
|
}
|
|
|
|
private static class GestureListener extends SimpleOnGestureListener {
|
|
|
|
private final HeaderDrawerLayout mDrawer;
|
|
|
|
public GestureListener(HeaderDrawerLayout drawer) {
|
|
mDrawer = drawer;
|
|
}
|
|
|
|
@Override
|
|
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
|
if (!mDrawer.isUsingDragHelper() && mDrawer.isValidScroll(distanceY, distanceX)) {
|
|
final int offset = mDrawer.getHeaderTop(), min = mDrawer.getHeaderTopMinimum();
|
|
if (!mDrawer.canScrollCallback(-1)) {
|
|
if (distanceY < 0) {
|
|
if (!mDrawer.isScrollingHeaderByHelper()) {
|
|
mDrawer.offsetHeaderBy(Math.round(-distanceY));
|
|
}
|
|
mDrawer.setScrollingHeaderByGesture(true);
|
|
} else if (distanceY > 0 && offset > min) {
|
|
// Scrolling up when scrolling to list top, so we cancel touch event and scrolling header up
|
|
mDrawer.cancelTouchCallback();
|
|
if (!mDrawer.isScrollingHeaderByHelper()) {
|
|
mDrawer.offsetHeaderBy(Math.round(-distanceY));
|
|
}
|
|
} else if (offset <= min) {
|
|
mDrawer.scrollByCallback(-distanceX);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
|
final int top = mDrawer.getHeaderTop(), min = mDrawer.getHeaderTopMinimum();
|
|
final boolean showingFullContent = top <= min, flingUp = velocityY < 0;
|
|
final boolean verticalFling = Math.abs(velocityY) > Math.abs(velocityX);
|
|
if (!verticalFling) return true;
|
|
if (showingFullContent) {
|
|
if (flingUp) {
|
|
// Fling list up when showing full content
|
|
if (mDrawer.isScrollingContentCallback()) {
|
|
mDrawer.flingCallback(-velocityY);
|
|
}
|
|
} else {
|
|
// Fling down when list reached top and not dragging user ViewDragHelper,
|
|
// so we fling header down here
|
|
if (!mDrawer.canScrollCallback(1) && !mDrawer.isUsingDragHelper()) {
|
|
mDrawer.flingHeader(velocityY);
|
|
}
|
|
}
|
|
} else {
|
|
// Header still visible
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onDown(MotionEvent e) {
|
|
mDrawer.setScrollingHeaderByGesture(false);
|
|
mDrawer.setScrollingContentCallback(false);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
@SuppressLint("ViewConstructor")
|
|
private static class InternalContainer extends ViewGroup {
|
|
|
|
private final HeaderDrawerLayout mParent;
|
|
private final View mHeaderView, mContentView;
|
|
|
|
public InternalContainer(HeaderDrawerLayout parent, Context context, int headerLayoutId, int contentLayoutId) {
|
|
super(context);
|
|
mParent = parent;
|
|
final LayoutInflater inflater = LayoutInflater.from(context);
|
|
addView(mHeaderView = inflater.inflate(headerLayoutId, this, false));
|
|
addView(mContentView = inflater.inflate(contentLayoutId, this, false));
|
|
}
|
|
|
|
public View getContent() {
|
|
return mContentView;
|
|
}
|
|
|
|
public View getHeader() {
|
|
return mHeaderView;
|
|
}
|
|
|
|
public int getHeaderTopMaximum() {
|
|
return mParent.getPaddingTop();
|
|
}
|
|
|
|
public int getHeaderTopMinimum() {
|
|
return mParent.getPaddingTop() - mHeaderView.getHeight();
|
|
}
|
|
|
|
public int getScrollRange() {
|
|
return getHeaderTopMaximum() - getHeaderTopMinimum();
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
final int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
|
|
int heightSum = 0;
|
|
for (int i = 0, j = getChildCount(); i < j; i++) {
|
|
final View child = getChildAt(i);
|
|
final LayoutParams lp = child.getLayoutParams();
|
|
final int childHeightSpec;
|
|
if (lp.height == LayoutParams.MATCH_PARENT) {
|
|
childHeightSpec = heightMeasureSpec;
|
|
} else if (lp.height == LayoutParams.WRAP_CONTENT) {
|
|
childHeightSpec = MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.UNSPECIFIED);
|
|
} else {
|
|
childHeightSpec = MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.EXACTLY);
|
|
}
|
|
child.measure(widthMeasureSpec, childHeightSpec);
|
|
heightSum += child.getMeasuredHeight();
|
|
}
|
|
final int hSpec = MeasureSpec.makeMeasureSpec(heightSum, MeasureSpec.EXACTLY);
|
|
super.onMeasure(widthMeasureSpec, hSpec);
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
for (int i = 0, j = getChildCount(); i < j; i++) {
|
|
final View child = getChildAt(i);
|
|
final int left = getPaddingLeft(), right = left + child.getMeasuredWidth();
|
|
final int top = i == 0 ? getPaddingTop() : getChildAt(i - 1).getBottom();
|
|
final int bottom = top + child.getMeasuredHeight();
|
|
child.layout(left, top, right, bottom);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void offsetTopAndBottom(int offset) {
|
|
super.offsetTopAndBottom(offset);
|
|
mParent.notifyOffsetChanged();
|
|
}
|
|
}
|
|
|
|
} |