前言:不知道还能这样坚持android多久啊,不管未来怎样,努力总还是会有收获的,h5之余还是要练练android的,不然就真忘记啦~~哈哈!!
今天要研究的是android v4自带的下拉刷新SwipeRefreshLayout,本来很久很久以前就想去研究一下它的,总是说自己没时间,其实时间挤挤还是有的,不废话了,先看一下最后用SwipeRefreshLayout实现的仿微信下拉刷新的效果图:
效果图看不出来那个美女拖动的效果,真实效果是有拖动的。
总体来说效果还是不错的,我们来看看源码然后一步一步实现其效果吧。
首先我们找到android.support.v4.widget.SwipeRefreshLayout这个类,用法呢很简单,就是在viewgroup外面套一层就可以了:
比如这样的:
<android.support.v4.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/id_swipe"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
></LinearLayout>
</android.support.v4.widget.SwipeRefreshLayout>
然后你就可以控制刷新了,刷新的回调:
swipe= (SwipeRefreshLayout) findViewById(R.id.id_swipe);
swipe.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
swipe.setRefreshing(false);
}
},3000);
}
});
重置刷新状态(控制刷新动作):
swipe.setRefreshing(false);
是不是soeasy呢?
其实现的原理是:
- 在onInterceptTouchEvent事件中做事件的拦截,当子控件到达顶部位置的时候并且滑动的距离大于最小滑动距离,则拦截子控件事件,自己处理。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
//当子控件滑动了,或者刷新指示器view正在回到初始状态的时候
//不拦截事件
if (!isEnabled() || mReturningToStart || canChildScrollUp()
|| mRefreshing || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
switch (action) {
....
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return false;
}
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
final float y = ev.getY(pointerIndex);
startDragging(y);
break;
.....
}
我们看到,在判断之前都会调用一个ensureTarget方法:
private void ensureTarget() {
//找到我们的targetview,也就是第一个子控件
if (mTarget == null) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (!child.equals(mCircleView)) {
mTarget = child;
break;
}
}
然后看到一个canChildScrollUp方法:
主要就是判断是否当前targetview可以滑动(没有到达顶部)
public boolean canChildScrollUp() {
if (mChildScrollUpCallback != null) {
return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
}
if (android.os.Build.VERSION.SDK_INT < 14) {
if (mTarget instanceof AbsListView) {
final AbsListView absListView = (AbsListView) mTarget;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(mTarget, -1);
}
}
因为我们知道,只有控件到达顶部后,我们才能下拉刷新,这个方法是判断控件没有到达顶部的,所以返回true就拦截掉子控件的事件。
然后就是监听手指的滑动:
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return false;
}
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
final float y = ev.getY(pointerIndex);
startDragging(y);
break;
@SuppressLint("NewApi")
private void startDragging(float y) {
final float yDiff = y - mInitialDownY;
//当滑动的距离>系统最小滑动距离,就认为开始下拉刷新了
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;
mIsBeingDragged = true;
mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
}
}
都知道android的事件传递机制,当父控件的onInterceptTouchEvent返回true之后,事件将会交给父控件自己处理,也就是会调用onTouchEvent方法:
2、监听onTouchEvent方法,根据手指滑动的距离,改变indicatorview的位置
@Override
public boolean onTouchEvent(MotionEvent ev) {
....
case MotionEvent.ACTION_MOVE: {
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
return false;
}
final float y = ev.getY(pointerIndex);
startDragging(y);
if (mIsBeingDragged) {
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
if (overscrollTop > 0) {
//根据手指移动的距离改变指示器view的位置
moveSpinner(overscrollTop);
setHeaderMarginOffset(overscrollTop/2);
} else {
return false;
}
}
break;
}
....
}
3、在onTouchEvent方法中,当手指抬起的时候,我们就判断指示器view移动的位置,当大于等于刷新位置的时候,此时开始刷新并回调。
case MotionEvent.ACTION_UP: {
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
return false;
}
if (mIsBeingDragged) {
final float y = ev.getY(pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
mIsBeingDragged = false;
//判断手指滑动的距离是否大于刷新距离,大于等于则刷新否则回到原位
finishSpinner(overscrollTop);
startHeaderBackAni();
mProgress.stop();
}
mActivePointerId = INVALID_POINTER;
return false;
}
private void finishSpinner(float overscrollTop) {
if (overscrollTop > mTotalDragDistance) {
setRefreshing(true, true /* notify */);
} else {
// cancel refresh
mRefreshing = false;
mProgress.setProgressRotation(0);
Animation.AnimationListener listener = null;
if (!mScale) {
listener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (!mScale) {
startScaleDownAnimation(null);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
};
}
animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
}
}
好吧!!整个流程下来还是非常容易的,下面让我们来分解下SwipeRefreshLayout,为了更好的研究源码,我们直接从v4中拖一份出来:
当然会有些报错,直接修改掉就可以了,相信这个能力你还是有的。
见过SwipeRefreshLayout效果的都知道,我们下拉刷新的时候,会出现一个进度条,这个进度条就是MaterialProgressDrawable,跟我们5.0以上的手机的progressbar的效果是一样的哈~~ 但是如果需要兼容5.0以前的手机,progressbar就有点鸡肋了,既然MaterialProgressDrawable有一个这样的效果,并且还兼容低版本,好吧! 我们试着搞一下它哈,其实最终的效果也就是我们最开始效果图的屏幕中间的效果图:
好吧,我已经拖出来了,要去拷的童鞋可以去我的git项目中拖哈:
实现原理很简单:就是动态的修改弧形的起始角度跟扫过的角度,然后动态的修改canvas的旋转角度,我就不详解啦~~
用法很简单:
直接new一个对象,然后设置给ImageView,最后开启动画就可以了:
MaterialProgressDrawable drawable1=new MaterialProgressDrawable(this,container);
//旋转第一遍的时候是否显示箭头
drawable1.showArrow(true);
//设置箭头的缩放
drawable1.setArrowScale(1);
//设置进度条每次旋转的颜色
drawable1.setColorSchemeColors(Color.RED,Color.GREEN);
//设置drawable的透明度
drawable1.setAlpha(255);
//把drawable设置给imageview
mShopCart.setImageDrawable(drawable1);
drawable1.start();
好哈!!穿插的内容哈~~~当然,小伙伴也可以直接拖走,因为毕竟是系统的控件,兼容性无可挑剔啊,比一些第三方库强多了,这算不算SwipeRefreshLayout给我们带来的福利呢?
好啦~回到我们的SwipeRefreshLayout,当SwipeRefreshLayout创建的时候,我们需要创建一个指示器view(ImageView),也就是我们改造过后的微信小图标:
public SwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
。。。
createProgressView();
。。。
然后我们需要把创建的MaterialProgressDrawable设置给这个指示器view(ImageView):
private void createProgressView() {
mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT);
mProgress = new WechatProgressDrawable(getContext(), this);
mCircleView.setImageDrawable(mProgress);
mCircleView.setVisibility(View.GONE);
addView(mCircleView);
}
最后,指示器view是有了,我们需要把它放入SwipeRefreshLayout的正确位置(onLayout):
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
final View child = mTarget;
final int childLeft = getPaddingLeft();
final int childTop = getPaddingTop();
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
int circleWidth = mCircleView.getMeasuredWidth();
int circleHeight = mCircleView.getMeasuredHeight();
//仿微信的下拉刷新小图片摆放的位置为(父布局最左边100px的位置)
mCircleView.layout((100), mCurrentTargetOffsetTop,
(100+circleWidth), mCurrentTargetOffsetTop + circleHeight);
}
好啦!!!SwipeRefreshLayout的基本流程差不多就讲完啦~当然SwipeRefreshLayout远不止我讲的这些内容哈~ 我讲的只是核心部分,具体的细节部分小伙伴还是需要自己去看看源码的啦!!!
本节就先到这里了,下一节我们就来用SwipeRefreshLayout实现一个仿微信下拉刷新的效果(看完这一节想必小伙伴也有点感觉了哈,可以自己先尝试的做一下,相信自己能行的)。