PullToRefresh 这个库用的是很至多。github 今天主要分析一下源代码实现.
我们通过ListView的下拉刷新进行分析。其他的类似。
整个下拉刷新 父View是LinearLayout, 在LinearLayout加入了Header View ,Footer View,和ListView
PullToRefreshBase 是父类 扩展了 LinearLayout水平布局 假设我们使用ListView 须要观看子类 PullToRefreshAdapterViewBase -> PullToRefreshListView
初始化代码在PullToRefreshBase init方法中
重点代码:
// Refreshable View // By passing the attrs, we can add ListView/GridView params via XML mRefreshableView = createRefreshableView(context, attrs);//通过子类传入的View,ListView或者ScrollView等 addRefreshableView(context, mRefreshableView);//加入view到布局中 // We need to create now layouts now 创建Header和Footer视图,默认是INVISIBLE。要加入到父窗体 mHeaderLayout = createLoadingLayout(context, Mode.PULL_FROM_START, a); mFooterLayout = createLoadingLayout(context, Mode.PULL_FROM_END, a); handleStyledAttributes(a);//加入loadingView效果。这里是把View加入到ListView HeaderView里面去 updateUIForMode(); //把布局加入到父View中
protected void updateUIForMode() { final LinearLayout.LayoutParams lp = getLoadingLayoutLayoutParams(); // Remove Header, and then add Header Loading View again if needed if (this == mHeaderLayout.getParent()) { removeView(mHeaderLayout); } if (mMode.showHeaderLoadingLayout()) { addViewInternal(mHeaderLayout, 0, lp);//增加View到LinearLayout } // Remove Footer, and then add Footer Loading View again if needed if (this == mFooterLayout.getParent()) { removeView(mFooterLayout); } if (mMode.showFooterLoadingLayout()) { addViewInternal(mFooterLayout, lp);//增加View到LinearLayout } // Hide Loading Views refreshLoadingViewsSize();//把headerView隐藏起来,其有用的是padding的方式 设置为负值 就到屏幕顶部的外面了 // If we're not using Mode.BOTH, set mCurrentMode to mMode, otherwise // set it to pull down mCurrentMode = (mMode != Mode.BOTH) ?mMode : Mode.PULL_FROM_START; }
//这里有2个LoadingView,一个是增加到LinearLayout中去了,另一个是增加到ListView本身的Header里面
看看handleStyledAttributes方法 定位到子类复写的地方
FrameLayout frame = new FrameLayout(getContext());
mHeaderLoadingView = createLoadingLayout(getContext(), Mode.PULL_FROM_START, a);
mHeaderLoadingView.setVisibility(View.GONE);
frame.addView(mHeaderLoadingView, lp);
mRefreshableView.addHeaderView(frame, null, false);//加入LoadingView到ListView Header上
//headerView一共同拥有2个LoadingView,一个是被增加到LinearLayout一个是被增加到ListView的HeaderView
addViewInternal方法就是增加到LinearLayout父类中
看看LoadingLayout 有2种 FlipLoadingLayout 和 RotateLoadingLayout 一般我们用旋转的载入动画
左边一个旋转图片,右边是文字和时间提示
第一个LoadingLayout主要显示 :下拉刷新,放开以刷新
第二个LoadingLayout显示松手后的文字:正在加载...
结构是这样
当UI初始化好,以下看看onTouch 下拉捕获事件
public final boolean onTouchEvent(MotionEvent event) { if (!isPullToRefreshEnabled()) { return false; } // If we're refreshing, and the flag is set. Eat the event if (!mScrollingWhileRefreshingEnabled && isRefreshing()) { return true; } if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) { return false; } switch (event.getAction()) { case MotionEvent.ACTION_MOVE: { if (mIsBeingDragged) { mLastMotionY = event.getY(); mLastMotionX = event.getX(); pullEvent();//開始下拉,移动 return true; } break; } case MotionEvent.ACTION_DOWN: { if (isReadyForPull()) {//按下 開始下拉 mLastMotionY = mInitialMotionY = event.getY(); mLastMotionX = mInitialMotionX = event.getX(); return true; } break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: { //停止下拉的时候 if (mIsBeingDragged) { mIsBeingDragged = false; if (mState == State.RELEASE_TO_REFRESH && (null != mOnRefreshListener || null != mOnRefreshListener2)) { setState(State.REFRESHING, true);//放下手指開始回调,运行我们的回调任务 return true; } // If we're already refreshing, just scroll back to the top if (isRefreshing()) { smoothScrollTo(0); return true; } // If we haven't returned by here, then we're not in a state // to pull, so just reset setState(State.RESET); //恢复到原来的UI状态 return true; } break; } } return false; }
看看pullEvent方法 private void pullEvent() { final int newScrollValue; final int itemDimension; final float initialMotionValue, lastMotionValue; switch (getPullToRefreshScrollDirection()) { case HORIZONTAL: initialMotionValue = mInitialMotionX; lastMotionValue = mLastMotionX; break; case VERTICAL: default: initialMotionValue = mInitialMotionY; lastMotionValue = mLastMotionY; break; } //计算下拉移动了多少 switch (mCurrentMode) { case PULL_FROM_END://上拉 newScrollValue = Math.round(Math.max(initialMotionValue - lastMotionValue, 0) / FRICTION); itemDimension = getFooterSize(); break; case PULL_FROM_START://下拉 default: newScrollValue = Math.round(Math.min(initialMotionValue - lastMotionValue, 0) / FRICTION); itemDimension = getHeaderSize(); break; } //显示HeaderView 得到移动的值。能够让LoadingView显示出来 setHeaderScroll(newScrollValue); if (newScrollValue != 0 && !isRefreshing()) { float scale = Math.abs(newScrollValue) / (float) itemDimension; switch (mCurrentMode) { case PULL_FROM_END: mFooterLayout.onPull(scale); break; case PULL_FROM_START: default: mHeaderLayout.onPull(scale);//旋转左边的载入图片,显示文字和图片 这个地方终于会运行LoadingLayout中的 onPullImpl方法 break; } //更新状态 包含2中 释放按下触摸,还有就是 没释放手的触摸 if (mState != State.PULL_TO_REFRESH && itemDimension >= Math.abs(newScrollValue)) { setState(State.PULL_TO_REFRESH); } else if (mState == State.PULL_TO_REFRESH && itemDimension < Math.abs(newScrollValue)) { setState(State.RELEASE_TO_REFRESH);//下拉松手 能够松手了 } } }
再看看setHeaderScroll方法代码 protected final void setHeaderScroll(int value) { if (DEBUG) { Log.d(LOG_TAG, "setHeaderScroll: " + value); } if (DEBUG) { Log.d(LOG_TAG, "setHeaderScroll:" + value ); } // Clamp value to with pull scroll range final int maximumPullScroll = getMaximumPullScroll(); value = Math.min(maximumPullScroll, Math.max(-maximumPullScroll, value)); if (mLayoutVisibilityChangesEnabled) { if (value < 0) { //有位移才显示 mHeaderLayout.setVisibility(View.VISIBLE); } else if (value > 0) { <span style="font-family: Arial, Helvetica, sans-serif;">//有位移才显示</span> mFooterLayout.setVisibility(View.VISIBLE); } else { mHeaderLayout.setVisibility(View.INVISIBLE); mFooterLayout.setVisibility(View.INVISIBLE); } } if (USE_HW_LAYERS) { /** * Use a Hardware Layer on the Refreshable View if we've scrolled at * all. We don't use them on the Header/Footer Views as they change * often, which would negate any HW layer performance boost. */ ViewCompat.setLayerType(mRefreshableViewWrapper, value != 0 ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE); } //回到最原始的scrollTo 最经常使用的 移动布局 switch (getPullToRefreshScrollDirection()) { case VERTICAL: scrollTo(0, value); break; case HORIZONTAL: scrollTo(value, 0); break; } }
setState(State.REFRESHING, true);//拉倒最顶部 松手,会运行onRefreshing方法。回调我们实现的任务接口 也就是OnRefreshListener
protected void onRefreshing(final boolean doScroll) { if (mMode.showHeaderLoadingLayout()) { mHeaderLayout.refreshing(); } if (mMode.showFooterLoadingLayout()) { mFooterLayout.refreshing(); } if (doScroll) { if (mShowViewWhileRefreshing) { // Call Refresh Listener when the Scroll has finished OnSmoothScrollFinishedListener listener = new OnSmoothScrollFinishedListener() { @Override public void onSmoothScrollFinished() { callRefreshListener();//回调接口运行 } }; switch (mCurrentMode) { case MANUAL_REFRESH_ONLY: case PULL_FROM_END: smoothScrollTo(getFooterSize(), listener); break; default: case PULL_FROM_START: smoothScrollTo(-getHeaderSize(), listener); break; } } else { smoothScrollTo(0);//回到原来的位置 } } else { // We're not scrolling, so just call Refresh Listener now callRefreshListener();//回调接口运行 } }
private void callRefreshListener() { if (null != mOnRefreshListener) { mOnRefreshListener.onRefresh(this);//回调 } else if (null != mOnRefreshListener2) { //这个是上拉,下拉都能够的情况,使用 onRefreshListener2 if (mCurrentMode == Mode.PULL_FROM_START) { mOnRefreshListener2.onPullDownToRefresh(this); } else if (mCurrentMode == Mode.PULL_FROM_END) { mOnRefreshListener2.onPullUpToRefresh(this); } } }
总结:状态包含下拉刷新。松手刷新,正在刷新,Loading隐藏。移动UI还是用的scrollTo最主要的代码. 动画部分能够看LoadingLayout的2个子类
基本的就这些。还有非常多细节没有分析。若有问题请指出谢谢。