源代码:RefreshableView(https://github.com/wangjiegulu/RefreshableView)
分析:
我们的目的是不管什么控件,只要在xml中外面包一层标签,那这个标签下面的所有子标签所在的控件都被支持可以下拉刷新了。所以,我们可以使用ViewGroup来实现,这里我选择的是继承LinearLayout,当然其他的(FrameLayout等)也一样了。
因为根据手指下滑,需要有一个刷新的view被显示出来,所以这里需要添加一个子view(称为refreshHeaderView),并放置在最顶部,使用LinearLayout的好处是可以设置为VERTICAL,这样可以直接“this.addView(refreshHeaderView, 0);”搞定了。然后就要根据手指滑动的距离,动态地去改变refreshHeaderView的高度。同时检测是否到达了可以刷新的高度了,如果达到了,更新当前的刷新状态。手指放开时,根据之前移动的刷新状态,执行刷新或者回归正常状态。
RefreshableView代码如下:
/** * 下拉刷新控件 * Author: wangjie * Email: tiantian.china.2@gmail.com * Date: 12/13/14. */public class RefreshableView extends LinearLayout {private static final String TAG = RefreshableView.class.getSimpleName();public RefreshableView(Context context) {super(context);init(context);}public RefreshableView(Context context, AttributeSet attrs) {super(context, attrs);TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.refreshableView);for (int i = 0, len = a.length(); i < len; i++) {int attrIndex = a.getIndex(i);switch (attrIndex) {case R.styleable.refreshableView_interceptAllMoveEvents:interceptAllMoveEvents = a.getBoolean(i, false);break;}}a.recycle();init(context);}public RefreshableView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);init(context);}/** * 刷新状态 */public static final int STATE_REFRESH_NORMAL = 0x21;public static final int STATE_REFRESH_NOT_ARRIVED = 0x22;public static final int STATE_REFRESH_ARRIVED = 0x23;public static final int STATE_REFRESHING = 0x24;private int refreshState;// 刷新状态监听private RefreshableHelper refreshableHelper;public void setRefreshableHelper(RefreshableHelper refreshableHelper) {this.refreshableHelper = refreshableHelper;}private Context context;/** * 刷新的view */private View refreshHeaderView;/** * 刷新的view的真实高度 */private int originRefreshHeight;/** * 有效下拉刷新需要达到的高度 */private int refreshArrivedStateHeight;/** * 刷新时显示的高度 */private int refreshingHeight;/** * 正常未刷新高度 */private int refreshNormalHeight;/** * 默认不允许拦截(即,往子view传递事件),该属性只有在interceptAllMoveEvents为false的时候才有效 */private boolean disallowIntercept = true;/** * xml中可设置它的值为false,表示不把移动的事件传递给子控件 */private boolean interceptAllMoveEvents;private void init(Context context) {this.context = context;this.setOrientation(VERTICAL);//Log.d(TAG, "[init]originRefreshHeight: " + refreshHeaderView.getMeasuredHeight());}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);if (null != refreshableHelper) {refreshHeaderView = refreshableHelper.onInitRefreshHeaderView();}//refreshHeaderView = LayoutInflater.from(context).inflate(R.layout.refresh_head, null);if (null == refreshHeaderView) {Log.e(TAG, "refreshHeaderView is null!");return;}this.removeView(refreshHeaderView);this.addView(refreshHeaderView, 0);// 计算refreshHeadView尺寸int width = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);int expandSpec = View.MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, View.MeasureSpec.AT_MOST);refreshHeaderView.measure(width, expandSpec);Log.d(TAG, "[onSizeChanged]w: " + w + ", h: " + h);Log.d(TAG, "[onSizeChanged]oldw: " + oldw + ", oldh: " + oldh);Log.d(TAG, "[onSizeChanged]child counts: " + this.getChildCount());originRefreshHeight = refreshHeaderView.getMeasuredHeight();boolean isUseDefault = true;if (null != refreshableHelper) {isUseDefault = refreshableHelper.onInitRefreshHeight(originRefreshHeight);}// 初始化各个高度if (isUseDefault) {refreshArrivedStateHeight = originRefreshHeight;refreshingHeight = originRefreshHeight;refreshNormalHeight = 0;}Log.d(TAG, "[onSizeChanged]refreshHeaderView origin height: " + originRefreshHeight);changeViewHeight(refreshHeaderView, refreshNormalHeight);// 初始化为正常状态setRefreshState(STATE_REFRESH_NORMAL);}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.d(TAG, "[dispatchTouchEvent]ev action: " + ev.getAction());return super.dispatchTouchEvent(ev);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {Log.d(TAG, "[onInterceptTouchEvent]ev action: " + ev.getAction());if (!interceptAllMoveEvents) {return !disallowIntercept;}// 如果设置了拦截所有move事件,即interceptAllMoveEvents为trueif (MotionEvent.ACTION_MOVE == ev.getAction()) {return true;}return false;}@Overridepublic void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {if (this.disallowIntercept == disallowIntercept) {return;}this.disallowIntercept = disallowIntercept;super.requestDisallowInterceptTouchEvent(disallowIntercept);}private float downY = Float.MAX_VALUE;@Overridepublic boolean onTouchEvent(MotionEvent event) {//super.onTouchEvent(event);Log.d(TAG, "[onTouchEvent]ev action: " + event.getAction());switch (event.getAction()) {case MotionEvent.ACTION_DOWN:downY = event.getY();Log.d(TAG, "Down --> downY: " + downY);requestDisallowInterceptTouchEvent(true); // 保证事件可往下传递break;case MotionEvent.ACTION_MOVE:float curY = event.getY();float deltaY = curY - downY;// 是否是有效的往下拖动事件(则需要显示加载header)boolean isDropDownValidate = Float.MAX_VALUE != downY;/** * 修改拦截设置 * 如果是有效往下拖动事件,则事件需要在本ViewGroup中处理,所以需要拦截不往子控件传递,即不允许拦截设为false * 如果是有效往下拖动事件,则事件传递给子控件处理,所以不需要拦截,并往子控件传递,即不允许拦截设为true */requestDisallowInterceptTouchEvent(!isDropDownValidate);downY = curY;Log.d(TAG, "Move --> deltaY(curY - downY): " + deltaY);int curHeight = refreshHeaderView.getMeasuredHeight();int exceptHeight = curHeight + (int) (deltaY / 2);// 如果当前没有处在正在刷新状态,则更新刷新状态if (STATE_REFRESHING != refreshState) {if (curHeight >= refreshArrivedStateHeight) { // 达到可刷新状态setRefreshState(STATE_REFRESH_ARRIVED);} else { // 未达到可刷新状态setRefreshState(STATE_REFRESH_NOT_ARRIVED);}}if (isDropDownValidate) {changeViewHeight(refreshHeaderView, Math.max(refreshNormalHeight, exceptHeight));} else { // 防止从子控件修改拦截后引发的downY为Float.MAX_VALUE的问题changeViewHeight(refreshHeaderView, Math.max(curHeight, exceptHeight));}break;case MotionEvent.ACTION_UP:downY = Float.MAX_VALUE;Log.d(TAG, "Up --> downY: " + downY);requestDisallowInterceptTouchEvent(true); // 保证事件可往下传递// 如果是达到刷新状态,则设置正在刷新状态的高度if (STATE_REFRESH_ARRIVED == refreshState) { // 达到了刷新的状态startHeightAnimation(refreshHeaderView, refreshHeaderView.getMeasuredHeight(), refreshingHeight);setRefreshState(STATE_REFRESHING);} else if (STATE_REFRESHING == refreshState) { // 正在刷新的状态startHeightAnimation(refreshHeaderView, refreshHeaderView.getMeasuredHeight(), refreshingHeight);} else {// 执行动画后回归正常状态startHeightAnimation(refreshHeaderView, refreshHeaderView.getMeasuredHeight(), refreshNormalHeight, normalAnimatorListener);}break;case MotionEvent.ACTION_CANCEL:Log.d(TAG, "cancel");break;}return true;}/** * 刷新完毕后调用此方法 */public void onCompleteRefresh() {if (STATE_REFRESHING == refreshState) {setRefreshState(STATE_REFRESH_NORMAL);startHeightAnimation(refreshHeaderView, refreshHeaderView.getMeasuredHeight(), refreshNormalHeight);}}/** * 修改当前的刷新状态 * * @param expectRefreshState */private void setRefreshState(int expectRefreshState) {if (expectRefreshState != refreshState) {refreshState = expectRefreshState;if (null != refreshableHelper) {refreshableHelper.onRefreshStateChanged(refreshHeaderView, refreshState);}}}/** * 改变某控件的高度 * * @param view * @param height */private void changeViewHeight(View view, int height) {Log.d(TAG, "[changeViewHeight]change Height: " + height);ViewGroup.LayoutParams lp = view.getLayoutParams();lp.height = height;view.setLayoutParams(lp);}/** * 改变某控件的高度动画 * * @param view * @param fromHeight * @param toHeight */private void startHeightAnimation(final View view, int fromHeight, int toHeight) {startHeightAnimation(view, fromHeight, toHeight, null);}private void startHeightAnimation(final View view, int fromHeight, int toHeight, Animator.AnimatorListener animatorListener) {if (toHeight == view.getMeasuredHeight()) {return;}ValueAnimator heightAnimator = ValueAnimator.ofInt(fromHeight, toHeight);heightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {Integer value = (Integer) valueAnimator.getAnimatedValue();if (null == value) return;changeViewHeight(view, value);}});if (null != animatorListener) {heightAnimator.addListener(animatorListener);}heightAnimator.setInterpolator(new LinearInterpolator());heightAnimator.setDuration(300/*ms*/);heightAnimator.start();}AnimatorListenerAdapter normalAnimatorListener = new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {super.onAnimationEnd(animation);setRefreshState(STATE_REFRESH_NORMAL); // 回归正常状态}};public void setRefreshArrivedStateHeight(int refreshArrivedStateHeight) {this.refreshArrivedStateHeight = refreshArrivedStateHeight;}public void setRefreshingHeight(int refreshingHeight) {this.refreshingHeight = refreshingHeight;}public void setRefreshNormalHeight(int refreshNormalHeight) {this.refreshNormalHeight = refreshNormalHeight;}public int getOriginRefreshHeight() {return originRefreshHeight;}其中:
<com.wangjie.refreshableview.RefreshableViewxmlns:rv="http://schemas.android.com/apk/res/com.wangjie.refreshableview"android:id="@+id/main_refresh_view"android:layout_width="match_parent"android:layout_height="match_parent"rv:interceptAllMoveEvents="false"><com.wangjie.refreshableview.NestScrollView android:layout_width="match_parent" android:layout_height="wrap_content"android:fillViewport="true"><TextViewandroid:id="@+id/main_tv"android:layout_width="fill_parent"android:layout_height="wrap_content"android:gravity="center"android:padding="20dp"android:textSize="18sp"android:text="Drop Down For Refresh Drop Down For Refresh Drop Down For Refresh Drop Down For Refresh Drop Down For Refresh Drop Down For Refresh Drop Down For Refresh Drop Down For Refresh"/></com.wangjie.refreshableview.NestScrollView></com.wangjie.refreshableview.RefreshableView>如上,最外面是一个RefreshableView,然后里面是一个NestScrollView,NestScrollView里面是TextView,其中TextView中因为文字较多,所以使用NestScrollView来实现滚动(NestScrollView扩展自ScrollView,下面会讲到)。这个时候的逻辑应该是,当NestScrollView处于顶部的时候,手指向下滑动,这时这个touch事件应该交给RefreshableView处理;当手指向上滑动时,也就是ScrollView向下滚动,这时,需要把touch事件给RefreshableView来处理。
/** * Author: wangjie * Email: tiantian.china.2@gmail.com * Date: 12/13/14. */public class NestScrollView extends ScrollView {private static final String TAG = NestScrollView.class.getSimpleName();public NestScrollView(Context context) {super(context);}public NestScrollView(Context context, AttributeSet attrs) {super(context, attrs);}public NestScrollView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.d(TAG, "___[dispatchTouchEvent]ev action: " + ev.getAction());return super.dispatchTouchEvent(ev);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {super.onInterceptTouchEvent(ev);Log.d(TAG, "___[onInterceptTouchEvent]ev action: " + ev.getAction());if (MotionEvent.ACTION_MOVE == ev.getAction()) {return true;}return false;}float lastDownY;@Overridepublic boolean onTouchEvent(MotionEvent event) {super.onTouchEvent(event);switch (event.getAction()) {case MotionEvent.ACTION_DOWN:lastDownY = event.getY();parentRequestDisallowInterceptTouchEvent(true); // 保证事件可往下传递Log.d(TAG, "___Down");return true;//break;case MotionEvent.ACTION_MOVE:Log.d(TAG, "___move, this.getScrollY(): " + this.getScrollY());boolean isTop = event.getY() - lastDownY > 0 && this.getScrollY() == 0;if (isTop) { // 允许父控件拦截,即不允许父控件拦截设为falseparentRequestDisallowInterceptTouchEvent(false);return false;} else { // 不允许父控件拦截,即不允许父控件拦截设为trueparentRequestDisallowInterceptTouchEvent(true);return true;}//break;case MotionEvent.ACTION_UP:Log.d(TAG, "___up, this.getScrollY(): " + this.getScrollY());parentRequestDisallowInterceptTouchEvent(true); // 保证事件可往下传递break;case MotionEvent.ACTION_CANCEL:Log.d(TAG, "___cancel");break;}return false;}/** * 是否允许父控件拦截事件 * @param disallowIntercept */private void parentRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {ViewParent vp = getParent();if (null == vp) {return;}vp.requestDisallowInterceptTouchEvent(disallowIntercept);}}如上所示,也需要重写onInterceptTouchEvent()方法,它需要把除了MOVE事件外的所有事件传递下去,这样最里面的TextView才有OnClick等事件。