Java tutorial
/* * Copyright (C) 2014 Peng fei Pan <sky@xiaopan.me> * * 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 me.xiaopan.android.widget; import android.content.Context; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import android.widget.AbsListView; import android.widget.Scroller; /** * * @author xiaopan * @version 1.0.0 Home https://github.com/xiaopansky/PullRefreshLayout */ public class PullRefreshLayout extends ViewGroup implements ScrollManager.Bridge, ScrollManager.ScrollListener { private static final String NAME = PullRefreshLayout.class.getSimpleName(); private static final int INVALID_POINTER = -1; private View targetView; private View headerView; private PullRefreshHeader headerInterface; private Class<? extends PullRefreshHeader> pullRefreshHeaderClass; private int touchSlop; // ??????? private int currentOffset; // ???? private int originalOffset; // ??? private int activePointerId = INVALID_POINTER; private int targetViewHeightDecrement; // TargetView???Target private float downMotionY; // Y? private float lastMotionY; // Y???Y?? private float elasticForce = 0.5f; //? private boolean mIsBeingDragged; // ? private ScrollManager scrollManager; // TargetViewHeaderView private OnRefreshListener onRefreshListener; // ? private boolean ready; // PullRefreshLayout??? private boolean waitRefresh; // ? public PullRefreshLayout(Context context) { this(context, null); } public PullRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); originalOffset = getPaddingTop(); scrollManager = new ScrollManager(getContext(), this, this); } @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // ????1 if (getChildCount() < 1) { throw new IllegalStateException(NAME + " can be only one directly child"); } // ????2 if (getChildCount() > 2) { throw new IllegalStateException(NAME + " can host only two direct child"); } // ????PullRefreshLayout int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); int height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); getChildAt(0).measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); if (getChildCount() < 2) { if (pullRefreshHeaderClass != null) { try { addView((View) pullRefreshHeaderClass.getConstructor(Context.class).newInstance(getContext())); } catch (Exception e) { e.printStackTrace(); return; } } else { return; } } // ?HeaderViewRefreshHeader? if (!(getChildAt(1) instanceof PullRefreshHeader)) { throw new IllegalStateException(NAME + " the second view must implement " + PullRefreshHeader.class.getSimpleName() + " interface"); } // ???UNSPECIFIED getChildAt(1).measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.UNSPECIFIED)); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childLeft = getPaddingLeft(); int childTop; int childRight; int childBottom; View childView; // ? if (getChildCount() < 1) return; childView = getChildAt(0); childTop = currentOffset; childRight = childLeft + childView.getMeasuredWidth(); childBottom = childTop + childView.getMeasuredHeight() - targetViewHeightDecrement; childView.layout(childLeft, childTop, childRight, childBottom); targetView = childView; // ? if (getChildCount() < 2) return; childView = getChildAt(1); childTop = currentOffset - childView.getMeasuredHeight(); childRight = childLeft + childView.getMeasuredWidth(); childBottom = childTop + childView.getMeasuredHeight(); childView.layout(childLeft, childTop, childRight, childBottom); headerView = childView; headerInterface = (PullRefreshHeader) headerView; // ? if (!ready) { ready = true; if (waitRefresh) { delayedStartRefresh(); } } } /** * ? */ private void delayedStartRefresh() { postDelayed(new Runnable() { @Override public void run() { startRefresh(); } }, getResources().getInteger(android.R.integer.config_mediumAnimTime)); } @Override public void requestDisallowInterceptTouchEvent(boolean b) { // Nope. } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (!isEnabled() || targetView == null || headerView == null || headerInterface == null || scrollManager == null || canChildScrollUp()) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (MotionEventCompat.getActionMasked(ev)) { case MotionEvent.ACTION_DOWN: downMotionY = lastMotionY = ev.getY(); activePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; break; case MotionEvent.ACTION_MOVE: if (activePointerId == INVALID_POINTER) { Log.e(NAME, "Got ACTION_MOVE event but don't have an active pointer id."); return false; } final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); if (pointerIndex < 0) { Log.e(NAME, "Got ACTION_MOVE event but have an invalid active pointer id."); return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); final float yDiff = y - downMotionY; if (yDiff > touchSlop) { lastMotionY = y; downMotionY = y; mIsBeingDragged = true; scrollManager.abort(); // ? } break; case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: activePointerId = INVALID_POINTER; mIsBeingDragged = false; break; } return mIsBeingDragged; } @Override public boolean onTouchEvent(MotionEvent ev) { if (!isEnabled() || targetView == null || headerView == null || headerInterface == null || scrollManager == null || canChildScrollUp()) { // Fail fast if we're not in a state where a swipe is possible return false; } switch (MotionEventCompat.getActionMasked(ev)) { case MotionEvent.ACTION_DOWN: { downMotionY = ev.getY(); lastMotionY = ev.getY(); activePointerId = MotionEventCompat.getPointerId(ev, 0); mIsBeingDragged = false; break; } case MotionEvent.ACTION_MOVE: { final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); if (pointerIndex < 0) { Log.e(NAME, "Got ACTION_MOVE event but have an invalid active pointer id."); return false; } final float y = MotionEventCompat.getY(ev, pointerIndex); final float yDiff = y - downMotionY; if (!mIsBeingDragged && yDiff > touchSlop) { lastMotionY = y; downMotionY = y; mIsBeingDragged = true; scrollManager.abort(); // ? } int addOffset = (int) ((y - lastMotionY) * elasticForce); updateOffset(currentOffset + addOffset, true, true); lastMotionY = y; break; } case MotionEventCompat.ACTION_POINTER_DOWN: { final int index = MotionEventCompat.getActionIndex(ev); lastMotionY = MotionEventCompat.getY(ev, index); activePointerId = MotionEventCompat.getPointerId(ev, index); break; } case MotionEventCompat.ACTION_POINTER_UP: { onSecondaryPointerUp(ev); break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { mIsBeingDragged = false; activePointerId = INVALID_POINTER; // ??? if (headerInterface != null && headerInterface.getStatus() == PullRefreshHeader.Status.WAIT_REFRESH) { startRefresh(); } else { scrollManager.startScroll(-1, true); } return false; } } return true; } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); } private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = MotionEventCompat.getActionIndex(ev); final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); if (pointerId == activePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; lastMotionY = MotionEventCompat.getY(ev, newPointerIndex); activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); } } /** * @return Whether it is possible for the child view of this layout to * scroll up. Override this if the child view is a custom view. */ private boolean canChildScrollUp() { if (android.os.Build.VERSION.SDK_INT < 14) { if (targetView instanceof AbsListView) { final AbsListView absListView = (AbsListView) targetView; return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0).getTop() < absListView.getPaddingTop()); } else { return targetView.getScrollY() > 0; } } else { return ViewCompat.canScrollVertically(targetView, -1); } } @Override public int getCurrentOffset() { return currentOffset; } @Override public int getOriginalOffset() { return originalOffset; } @Override public void setOriginalOffset(int newOriginalOffset) { this.originalOffset = newOriginalOffset; } @Override public void setTargetViewHeightDecrement(int newTargetViewHeightDecrement) { this.targetViewHeightDecrement = newTargetViewHeightDecrement; } /** * ??? * @param newOffset ? * @param preventOverOriginalOffset true??false? * @param manualSliding ????????? */ @Override public void updateOffset(int newOffset, boolean preventOverOriginalOffset, boolean manualSliding) { // ?? if (preventOverOriginalOffset) { if (newOffset < originalOffset) { newOffset = originalOffset; } } else { if (newOffset > originalOffset) { newOffset = originalOffset; } } // ??? currentOffset = newOffset; requestLayout(); // ???HeaderView?? // ?????? if (headerInterface != null && onRefreshListener != null) { // HeaderView? int distance = Math.abs(currentOffset - getPaddingTop()); headerInterface.onScroll(distance); // ?????? if (headerInterface.getStatus() != PullRefreshHeader.Status.REFRESHING && manualSliding) { if (distance >= headerInterface.getTriggerHeight()) { headerInterface.setStatus(PullRefreshHeader.Status.WAIT_REFRESH); headerInterface.onToWaitRefresh(); } else { headerInterface.setStatus(PullRefreshHeader.Status.NORMAL); headerInterface.onToNormal(); } } } } @Override public View getView() { return this; } /** * Class??Class */ public void setPullRefreshHeaderClass(Class<? extends PullRefreshHeader> pullRefreshHeaderClass) { this.pullRefreshHeaderClass = pullRefreshHeaderClass; } /** * ? */ public void setAnimationDuration(int animationDuration) { scrollManager.setAnimationDuration(animationDuration); } /** * ? */ public void setOnRefreshListener(OnRefreshListener listener) { onRefreshListener = listener; } /** * * @param elasticForce ?[0.0f-1.0f]??0.5f */ public void setElasticForce(float elasticForce) { this.elasticForce = elasticForce; } /** * ? */ public boolean isRefreshing() { return headerInterface != null && headerInterface.getStatus() == PullRefreshHeader.Status.REFRESHING; } /** * * @return true?false??? */ public boolean startRefresh() { if (!ready) { waitRefresh = true; return true; } if (headerInterface == null || onRefreshListener == null || headerInterface.getStatus() == PullRefreshHeader.Status.REFRESHING) { return false; } headerInterface.setStatus(PullRefreshHeader.Status.WAIT_REFRESH); scrollManager.startScroll(getPaddingTop() + headerInterface.getTriggerHeight(), true); return true; } /** * ? * @return true?false? */ public boolean stopRefresh() { if (headerInterface == null || headerInterface.getStatus() != PullRefreshHeader.Status.REFRESHING) { return false; } headerInterface.setStatus(PullRefreshHeader.Status.NORMAL); headerInterface.onToNormal(); scrollManager.startScroll(getPaddingTop(), false); return true; } @Override public void onScrollEnd() { if (headerInterface == null) { return; } if (headerInterface.getStatus() == PullRefreshHeader.Status.WAIT_REFRESH) { headerInterface.setStatus(PullRefreshHeader.Status.REFRESHING); headerInterface.onToRefreshing(); onRefreshListener.onRefresh(); } } public interface OnRefreshListener { public void onRefresh(); } public interface PullRefreshHeader { public void onScroll(int distance); public void onToRefreshing(); public void onToNormal(); public void onToWaitRefresh(); public int getTriggerHeight(); public Status getStatus(); public void setStatus(Status status); public enum Status { /** * */ NORMAL, /** * */ WAIT_REFRESH, /** * */ REFRESHING, } } } class ScrollManager implements Runnable { private int animationDuration; private Bridge bridge; private Scroller scroller; private ScrollListener scrollListener; private boolean rollback; private boolean running; public ScrollManager(Context context, Bridge bridge, ScrollListener scrollListener) { this.animationDuration = context.getResources().getInteger(android.R.integer.config_mediumAnimTime); this.bridge = bridge; this.scrollListener = scrollListener; this.scroller = new Scroller(context, new DecelerateInterpolator()); } /** * * @param newOriginalOffset ??? * @param diminish ??TargetView????????????? */ public boolean startScroll(int newOriginalOffset, boolean diminish) { abort(); int startX = bridge.getCurrentOffset(); int endX = bridge.getOriginalOffset(); int startY = 0; int endY = 0; if (newOriginalOffset != bridge.getOriginalOffset() && newOriginalOffset >= 0) { if (diminish) { startY = 0; endY = Math.abs(newOriginalOffset - bridge.getOriginalOffset()); } else { startY = Math.abs(newOriginalOffset - bridge.getOriginalOffset()); endY = 0; } bridge.setOriginalOffset(newOriginalOffset); endX = newOriginalOffset; } running = true; rollback = startX > endX; scroller.startScroll(startX, startY, endX - startX, endY - startY, animationDuration); bridge.getView().post(this); return true; } @Override public void run() { if (!running) { return; } // ? if (!scroller.computeScrollOffset()) { running = false; if (scrollListener != null) { scrollListener.onScrollEnd(); } return; } // bridge.setTargetViewHeightDecrement(scroller.getCurrY()); bridge.updateOffset(scroller.getCurrX(), rollback, false); bridge.getView().post(this); } public void abort() { if (!scroller.isFinished()) { scroller.abortAnimation(); } bridge.getView().removeCallbacks(this); running = false; } public void setAnimationDuration(int animationDuration) { this.animationDuration = animationDuration; } public interface Bridge { public int getCurrentOffset(); public int getOriginalOffset(); public void setOriginalOffset(int newOriginalOffset); public void setTargetViewHeightDecrement(int newTargetViewHeightDecrement); public void updateOffset(int newCurrentOffset, boolean rollback, boolean callbackHeader); public View getView(); } public interface ScrollListener { public void onScrollEnd(); } }