package com.wpc.library.widget.TitleBar; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.Scroller; import android.widget.TextView; import com.wpc.lcjianlibrary.R; public class MaterialHeaderLayout extends ViewGroup { final boolean DEBUG = true; final boolean DEBUG_LAYOUT = true; final String LOG_TAG = "HeaderLayout"; ContentHandler mContentHandler; HeaderHandler mHeaderHandler; MotionEventIndicator mIndicator; View mHeaderView; View mContent; ScrollChecker mScrollChecker; int mHeaderId; int mContainerId; int mHeaderMaxHeight; int mHeaderMinHeight; int mOriginHeaderMinHeight; // 不带margin的高度,可直接设置 MotionEvent mLastMoveEvent; private boolean mHasSendCancelEvent; private boolean mPreventForHorizontal; private boolean mDisableWhenHorizontalMove; private float mPagingTouchSlop; public MaterialHeaderLayout(Context context) { this(context, null); } public MaterialHeaderLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MaterialHeaderLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.MaterialHeaderLayout, 0, 0); if (arr != null) { //mHeaderId = arr.getResourceId(R.styleable.MaterialHeaderLayout_header, mHeaderId); mContainerId = arr.getResourceId(R.styleable.MaterialHeaderLayout_content, mContainerId); mOriginHeaderMinHeight = arr.getDimensionPixelOffset(R.styleable.MaterialHeaderLayout_minHeight, 0); if (DEBUG) { Log.d(LOG_TAG, "attr: minHeight: " + mHeaderMinHeight); } arr.recycle(); } mScrollChecker = new ScrollChecker(); mIndicator = new MotionEventIndicator(); } @Override protected void onFinishInflate() { final int childCount = getChildCount(); if (childCount > 2) { throw new IllegalStateException("PtrFrameLayout only can host 2 elements"); } else if (childCount == 2) { if (mHeaderId != 0 && mHeaderView == null) { mHeaderView = findViewById(mHeaderId); } if (mContainerId != 0 && mContent == null) { mContent = findViewById(mContainerId); } // not specify header or content if (mContent == null || mHeaderView == null) { View child1 = getChildAt(0); View child2 = getChildAt(1); if (child1 instanceof HeaderHandler) { mHeaderView = child1; mContent = child2; } else if (child2 instanceof HeaderHandler) { mHeaderView = child2; mContent = child1; } else { // both are not specified if (mContent == null && mHeaderView == null) { mHeaderView = child1; mContent = child2; } // only one is specified else { if (mHeaderView == null) { mHeaderView = mContent == child1 ? child2 : child1; } else { mContent = mHeaderView == child1 ? child2 : child1; } } } } } else if (childCount == 1) { mContent = getChildAt(0); } else { TextView errorView = new TextView(getContext()); errorView.setClickable(true); errorView.setTextColor(0xffff6600); errorView.setGravity(Gravity.CENTER); errorView.setTextSize(20); errorView.setText("The content view in PtrFrameLayout is empty. Do you forget to specify its id in xml layout file?"); mContent = errorView; addView(mContent); } if (mContent instanceof ContentHandler) { mContentHandler = (ContentHandler) mContent; } if (mHeaderView != null) { mHeaderView.bringToFront(); if (mHeaderView instanceof HeaderHandler) { mHeaderHandler = (HeaderHandler) mHeaderView; } } super.onFinishInflate(); } /* void lpTransfer(View view){ LayoutParams lp = view.getLayoutParams(); MarginLayoutParams mlp = new MarginLayoutParams(lp.width,lp.height); }*/ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (DEBUG && DEBUG_LAYOUT) { Log.d(LOG_TAG, String.format("onMeasure frame: width: %s, height: %s, padding: %s %s %s %s", getMeasuredHeight(), getMeasuredWidth(), getPaddingLeft(), getPaddingRight(), getPaddingTop(), getPaddingBottom())); } if (mHeaderView != null) { measureChildWithMargins(mHeaderView, widthMeasureSpec, 0, heightMeasureSpec, 0); MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams(); mHeaderMaxHeight = mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; mHeaderMinHeight = mOriginHeaderMinHeight + lp.topMargin + lp.bottomMargin; mIndicator.setHeight(mHeaderMaxHeight, mHeaderMinHeight); mIndicator.setCurrentPosIfFirst(mHeaderMaxHeight); if (DEBUG && DEBUG_LAYOUT) { Log.d(LOG_TAG, String.format("onMeasure, maxHeight: %s, minHeight: %s, margin: %s, %s, %s, %s", mHeaderMaxHeight, mHeaderMinHeight, lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin)); } if (mContentHandler != null) { mContentHandler.onOffsetCalculated(mHeaderMaxHeight - mHeaderMinHeight); } } if (mContent != null) { measureContentView(mContent, widthMeasureSpec, heightMeasureSpec); MarginLayoutParams lp = (MarginLayoutParams) mContent.getLayoutParams(); if (DEBUG && DEBUG_LAYOUT) { Log.d(LOG_TAG, String.format("onMeasure content, width: %s, height: %s, margin: %s %s %s %s", getMeasuredWidth(), getMeasuredHeight(), lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin)); } } } private void measureContentView(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, getPaddingTop() + getPaddingBottom() + lp.topMargin + mHeaderMinHeight, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } @Override protected void onLayout(boolean flag, int i, int j, int k, int l) { layoutChildren(); } private void layoutChildren() { int offsetY = mIndicator.getCurrentPosY(); int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); if (mHeaderView != null) { MarginLayoutParams lp = (MarginLayoutParams) mHeaderView.getLayoutParams(); final int left = paddingLeft + lp.leftMargin; final int top = paddingTop + lp.topMargin + offsetY - mHeaderMaxHeight; final int right = left + mHeaderView.getMeasuredWidth(); final int bottom = top + mHeaderView.getMeasuredHeight(); mHeaderView.layout(left, top, right, bottom); if (DEBUG && DEBUG_LAYOUT) { Log.d(LOG_TAG, String.format("onLayout header: %s %s %s %s", left, top, right, bottom)); } } if (mContent != null) { MarginLayoutParams lp = (MarginLayoutParams) mContent.getLayoutParams(); final int left = paddingLeft + lp.leftMargin; final int top = paddingTop + lp.topMargin + offsetY; final int right = left + mContent.getMeasuredWidth(); final int bottom = top + mContent.getMeasuredHeight(); if (DEBUG && DEBUG_LAYOUT) { Log.d(LOG_TAG, String.format("onLayout content: %s %s %s %s", left, top, right, bottom)); } mContent.layout(left, top, right, bottom); } } @Override public boolean dispatchTouchEvent(MotionEvent e) { if (!isEnabled() || mContent == null || mHeaderView == null) { return super.dispatchTouchEvent(e); } int action = e.getAction(); switch (action) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mIndicator.onRelease(); if (mIndicator.reachMinHeight()) { if (DEBUG) { Log.d(LOG_TAG, "call onRelease when user release"); } if (mIndicator.hasMovedAfterPressedDown()) { sendCancelEvent(); return true; } return super.dispatchTouchEvent(e); } else { return super.dispatchTouchEvent(e); } case MotionEvent.ACTION_DOWN: mHasSendCancelEvent = false; mIndicator.onPressDown(e.getX(), e.getY(), e.getEventTime()); mScrollChecker.abortIfWorking(); mPreventForHorizontal = false; // The cancel event will be sent once the position is moved. // So let the event pass to children. // fix #93, #102 super.dispatchTouchEvent(e); return true; case MotionEvent.ACTION_MOVE: mLastMoveEvent = e; mIndicator.onMove(e.getX(), e.getY(), e.getEventTime()); float offsetX = mIndicator.getOffsetX(); float offsetY = mIndicator.getOffsetY(); if (mDisableWhenHorizontalMove && !mPreventForHorizontal && (Math.abs(offsetX) > mPagingTouchSlop && Math.abs(offsetX) > Math.abs(offsetY))) { if (mIndicator.reachMinHeight()) { mPreventForHorizontal = true; } } if (mPreventForHorizontal) { return super.dispatchTouchEvent(e); } boolean moveDown = offsetY > 0; boolean moveUp = !moveDown; boolean canMoveUp = !mIndicator.reachMinHeight(); boolean canMoveDown = mContentHandler != null ? mContentHandler.checkCanDoRefresh(this, mContent, mHeaderView) : DefaultContentHandler.checkContentCanBePulledDown(this, mContent, mHeaderView); if (DEBUG) { Log.v(LOG_TAG, String.format("ACTION_MOVE: offsetY:%s, currentPos: %s, moveUp: %s, canMoveUp: %s, moveDown: %s: canMoveDown: %s", offsetY, mIndicator.getCurrentPosY(), moveUp, canMoveUp, moveDown, canMoveDown)); } // disable move when header not reach top if (moveDown && !canMoveDown) { return super.dispatchTouchEvent(e); } if ((moveUp && canMoveUp) || moveDown) { // has reached the top if ((offsetY < 0 && mIndicator.reachMinHeight())) { if (DEBUG) { Log.v(LOG_TAG, String.format("has reached the top")); } return super.dispatchTouchEvent(e); } // has reached the top if ((offsetY > 0 && mIndicator.reachMaxHeight())) { if (DEBUG) { Log.v(LOG_TAG, String.format("has reached the bottom")); } return super.dispatchTouchEvent(e); } Log.v(LOG_TAG, "move pos: " + offsetY + ", speed: " + offsetY / mIndicator.getOffsetTime()); if (Math.abs(offsetY / mIndicator.getOffsetTime()) > 2) { if (offsetY > 0) { mScrollChecker.tryToScrollTo(mHeaderMaxHeight, 300); } else { mScrollChecker.tryToScrollTo(mHeaderMinHeight, 300); } } else { movePos(offsetY); } if (Math.abs(offsetY / offsetX) < Math.tan(Math.PI / 6)) { MotionEvent event = MotionEvent.obtain(e); event.setLocation(e.getX(), e.getY() - offsetY); super.dispatchTouchEvent(event); } return true; } break; } return super.dispatchTouchEvent(e); } /** * if deltaY > 0, move the content down * * @param deltaY */ private void movePos(float deltaY) { int to = mIndicator.getCurrentPosY() + (int) deltaY; // over top if (mIndicator.willOverTop(to)) { if (DEBUG) { Log.e(LOG_TAG, String.format("over top")); } to = mIndicator.getMinHeight(); } else if (mIndicator.willOverBottom(to)) { if (DEBUG) { Log.e(LOG_TAG, String.format("over bottom")); } to = mIndicator.getMaxHeight(); } mIndicator.setCurrentPos(to); int change = to - mIndicator.getLastPosY(); updatePos(change); } private void updatePos(int change) { if (mHeaderHandler != null) mHeaderHandler.onChange(mIndicator.getCurrentPercent(), change); if (mContentHandler != null) mContentHandler.onChange(mIndicator.getCurrentPercent(), change); mHeaderView.offsetTopAndBottom(change); mContent.offsetTopAndBottom(change); invalidate(); } private void sendCancelEvent() { if (DEBUG) { Log.d(LOG_TAG, "send cancel event"); } // The ScrollChecker will update position and lead to send cancel event when mLastMoveEvent is null. // fix #104, #80, #92 if (mLastMoveEvent == null) { return; } MotionEvent last = mLastMoveEvent; MotionEvent e = MotionEvent.obtain(last.getDownTime(), last.getEventTime() + ViewConfiguration.getLongPressTimeout(), MotionEvent.ACTION_CANCEL, last.getX(), last.getY(), last.getMetaState()); super.dispatchTouchEvent(e); } public void setHeaderView(View header) { if (mHeaderView != null && header != null && mHeaderView != header) { removeView(mHeaderView); } ViewGroup.LayoutParams lp = header.getLayoutParams(); if (lp == null) { lp = new LayoutParams(-1, -2); header.setLayoutParams(lp); } mHeaderView = header; addView(header); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } public static class LayoutParams extends MarginLayoutParams { public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } public LayoutParams(int width, int height) { super(width, height); } @SuppressWarnings({"unused"}) public LayoutParams(MarginLayoutParams source) { super(source); } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } } class ScrollChecker implements Runnable { private int mLastFlingY; private Scroller mScroller; private boolean mIsRunning = false; private int mStart; private int mTo; public ScrollChecker() { mScroller = new Scroller(getContext()); } public void run() { boolean finish = !mScroller.computeScrollOffset() || mScroller.isFinished(); int curY = mScroller.getCurrY(); int deltaY = curY - mLastFlingY; if (DEBUG) { if (deltaY != 0) { Log.v(LOG_TAG, String.format("scroll: %s, start: %s, to: %s, currentPos: %s, current :%s, last: %s, delta: %s", finish, mStart, mTo, mIndicator.getCurrentPosY(), curY, mLastFlingY, deltaY)); } } if (!finish) { mLastFlingY = curY; movePos(deltaY); post(this); } else { finish(); } } private void finish() { if (DEBUG) { Log.v(LOG_TAG, String.format("finish, currentPos:%s", mIndicator.getCurrentPosY())); } reset(); } private void reset() { mIsRunning = false; mLastFlingY = 0; removeCallbacks(this); } public void abortIfWorking() { if (mIsRunning) { if (!mScroller.isFinished()) { mScroller.forceFinished(true); } reset(); } } public void tryToScrollTo(int to, int duration) { if (mIndicator.isAlreadyHere(to)) { return; } mStart = mIndicator.getCurrentPosY(); mTo = to; int distance = to - mStart; if (DEBUG) { Log.d(LOG_TAG, String.format("tryToScrollTo: start: %s, distance:%s, to:%s", mStart, distance, to)); } removeCallbacks(this); mLastFlingY = 0; // fix #47: Scroller should be reused, https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh/issues/47 if (!mScroller.isFinished()) { mScroller.forceFinished(true); } mScroller.startScroll(0, 0, 0, distance, duration); post(this); mIsRunning = true; } } }