android 控件 下拉刷新 phoenix 带源码分析

向纳什致敬,凤凰城永远的英雄!phoenix

github的项目地址:https://github.com/Yalantis/Phoenix

Yalantis 实现了两个动画下拉刷新,

Yalantis 致力于提供世界一流的 Android 和 iOS 应用开发服务,因一些

动画很棒的开源库为大家所熟知

Phoenix

Taurus

Phoenix-Android 旨在提供一个简单的可定制的下拉刷新功能。

<com.hankkin.AnimationPullToRefreshDemo.PullToRefreshView  
        android:id="@+id/pull_to_refresh"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent">  
   
        <ListView  
            android:id="@+id/list_view"  
            android:divider="@null"  
            android:dividerHeight="0dp"  
            android:fadingEdge="none"  
            android:layout_width="match_parent"  
            android:layout_height="match_parent" />  
   
    </com.hankkin.AnimationPullToRefreshDemo.PullToRefreshView>  

由此我们可以知道,在这个下拉刷新并不是重写了listview,而是在listview的外面套了一层布局,也就是说listview被添加到了Phoenix上,那么我们就能知道Phoenix其实就是一个viewgroup,到这里就差不多知道要重写那几个方法了,自定义viewgroup的话也就onlayout、onmeasure、ontouchevent(如果是自定义view的话一般就重写onmeasure、ondraw、ontouchevent),整理到这里我们就可以来看看那源码了。

源码分析

构造方法

onmeasure

onlayout

头部动画效果实现

构造方法

首先看他的构造方法

public PullToRefreshView(Context context, AttributeSet attrs) {
	super(context, attrs);
	TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshView);
	final int type = a.getInteger(R.styleable.RefreshView_type, STYLE_SUN);
	a.recycle();

	mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
	mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
	mTotalDragDistance = Utils.convertDpToPixel(context, DRAG_MAX_DISTANCE);

	mRefreshView = new ImageView(context);

	setRefreshStyle(type);

	addView(mRefreshView);
	//保证ondraw会执行,如果是true的话ondraw不会执行
	setWillNotDraw(false);
	ViewCompat.setChildrenDrawingOrderEnabled(this, true);
}

public void setRefreshStyle(int type) {
	setRefreshing(false);
	switch (type) {
		case STYLE_SUN:
			mBaseRefreshView = new SunRefreshView(getContext(), this);
			break;
		default:
			throw new InvalidParameterException("Type does not exist");
	}
	mRefreshView.setImageDrawable(mBaseRefreshView);
}

在这里setRefreshStyle其实就可以直接看成是给头部的imageview设置显示的内容,然后将这个imageview添加到viewgroup中,另外的就是一写参数的初始化。简单的说就是在这个已经包了一个listview的viewgroup中再添加一个imageview。

onMeasure

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);

	ensureTarget();
	if (mTarget == null)
		return;

	widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingRight() - getPaddingLeft(), MeasureSpec.EXACTLY);
	heightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY);
	mTarget.measure(widthMeasureSpec, heightMeasureSpec);
	mRefreshView.measure(widthMeasureSpec, heightMeasureSpec);
}

private void ensureTarget() {
	if (mTarget != null)
		return;
	if (getChildCount() > 0) {
		for (int i = 0; i < getChildCount(); i++) {
			View child = getChildAt(i);
			if (child != mRefreshView) {
				mTarget = child;
				mTargetPaddingBottom = mTarget.getPaddingBottom();
				mTargetPaddingLeft = mTarget.getPaddingLeft();
				mTargetPaddingRight = mTarget.getPaddingRight();
				mTargetPaddingTop = mTarget.getPaddingTop();
			}
		}
	}
}

一开始mTarget 是空的,然后到getChildCount方法,想一下这个时候这个viewgroup中也就两个孩子,一个imageview,一个listview,ensureTarget的作用就是把listview的实例赋值给mTarget,以及给几个padding赋值,随后在onmeasure中设置imageview和listview与外层的viewgroup一样大小。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {

	ensureTarget();
	if (mTarget == null)
		return;

	int height = getMeasuredHeight();
	int width = getMeasuredWidth();
	int left = getPaddingLeft();
	int top = getPaddingTop();
	int right = getPaddingRight();
	int bottom = getPaddingBottom();

	mTarget.layout(left, top + mCurrentOffsetTop, left + width - right, top + height - bottom + mCurrentOffsetTop);
	mRefreshView.layout(left, top, left + width - right, top + height - bottom);
}

imageview和listview放在相同的位置。

ontouchevent和onInterceptTouchEvent

如果不知道上面两个方法的关系,可以去看看另一篇文章(http://blog.csdn.net/u012806692/article/details/50820070),首先重写的是拦截的方法

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

	if (!isEnabled() || canChildScrollUp() || mRefreshing) {
		return false;
	}

	final int action = MotionEventCompat.getActionMasked(ev);

	switch (action) {
		case MotionEvent.ACTION_DOWN:
			setTargetOffsetTop(0, true);
			mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
			mIsBeingDragged = false;
			final float initialMotionY = getMotionEventY(ev, mActivePointerId);
			if (initialMotionY == -1) {
				return false;
			}
			mInitialMotionY = initialMotionY;
			break;
		case MotionEvent.ACTION_MOVE:
			if (mActivePointerId == INVALID_POINTER) {
				return false;
			}
			final float y = getMotionEventY(ev, mActivePointerId);
			if (y == -1) {
				return false;
			}
			final float yDiff = y - mInitialMotionY;
			if (yDiff > mTouchSlop && !mIsBeingDragged) {
				mIsBeingDragged = true;
			}
			break;
		case MotionEvent.ACTION_UP:
		case MotionEvent.ACTION_CANCEL:
			mIsBeingDragged = false;
			mActivePointerId = INVALID_POINTER;
			break;
		case MotionEventCompat.ACTION_POINTER_UP:
			onSecondaryPointerUp(ev);
			break;
	}

	return mIsBeingDragged;
}

如果listview没有滑到最顶部或者还在加载刷新中就不执行之后的代码,直接返回false,否则记录按下位置的y坐标,注意这里还有多点触控的知识,这里不理解可以先不用管。到了action_move之后,如果开始滑动(也就是大于mTouchSlop )就拦截touch事件不传递给子view,直接执行自己的ontouchevent方法,这里我们先不管多点触控相关的直接简单理解下,接下来看ontouchevent事件 

@Override 
public boolean onTouchEvent(@NonNull MotionEvent ev) {
    if (!mIsBeingDragged) {
        return super.onTouchEvent(ev);
    }


    final int action = MotionEventCompat.getActionMasked(ev);


    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            if (pointerIndex < 0) {
                return false;
            }


            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float yDiff = y - mInitialMotionY;
            final float scrollTop = yDiff * DRAG_RATE;
            mCurrentDragPercent = scrollTop / mTotalDragDistance;
            if (mCurrentDragPercent < 0) {
                return false;
            }
            float boundedDragPercent = Math.min(1f, Math.abs(mCurrentDragPercent));
            float extraOS = Math.abs(scrollTop) - mTotalDragDistance;
            float slingshotDist = mTotalDragDistance;
            float tensionSlingshotPercent = Math.max(0,
                    Math.min(extraOS, slingshotDist * 2) / slingshotDist);
            float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
                    (tensionSlingshotPercent / 4), 2)) * 2f;
            float extraMove = (slingshotDist) * tensionPercent / 2;
            int targetY = (int) ((slingshotDist * boundedDragPercent) + extraMove);


            mBaseRefreshView.setPercent(mCurrentDragPercent, true);
            setTargetOffsetTop(targetY - mCurrentOffsetTop, true);
            break;
        }
        case MotionEventCompat.ACTION_POINTER_DOWN:
            final int index = MotionEventCompat.getActionIndex(ev);
            mActivePointerId = MotionEventCompat.getPointerId(ev, index);
            break;
        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL: {
            if (mActivePointerId == INVALID_POINTER) {
                return false;
            }
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float overScrollTop = (y - mInitialMotionY) * DRAG_RATE;
            mIsBeingDragged = false;
            if (overScrollTop > mTotalDragDistance) {
                setRefreshing(true, true);
            } else {
                mRefreshing = false;
                animateOffsetToStartPosition();
            }
            mActivePointerId = INVALID_POINTER;
            return false;
        }
    }


    return true;
}

代码比较多一步步看,上面我们已经执行到了move,在ontouchevent的move中计算了滑动的百分比,头部有个默认的最大值,看目前的滑动举例是他的百分之几,滑动距离超过最大值时取100%,在case action_move中的代码看着挺多,最重要的就最后的几句,设置头部的显示百分比,还有listview的偏移(相当于magin),随后主要看action_up,在抬起中判断滑动距离是否到了加载的那个指定距离,如果足够了就加载,不够就直接回到初始位置,大致流程就是这样。接下来看看imageview中的内容,就是一个

头部动画效果

其实就是imageview中的内容,一开始其实我们留下了一个问题,回想一下,在onlayout和onmeasure中我们设置的imageview的大小和显示位置和listview的是一样的,那么两个不就叠在一起了吗?接下来看下imageview的内容是什么就明白了。它的构造方法就不看了,就一堆变量的赋值,加载图片等等,直接看自定义view最重要的draw方法

@Override
public void draw(Canvas canvas) {
	if (mScreenWidth <= 0) return;

	final int saveCount = canvas.save();

	canvas.translate(0, mTop);
	canvas.clipRect(0, -mTop, mScreenWidth, mParent.getTotalDragDistance());

	drawSky(canvas);
	drawSun(canvas);
	drawTown(canvas);

	canvas.restoreToCount(saveCount);
}

其中将画布移动到了mtop的位置,再看之前初始化的时候将其赋值为mTop = -mParent.getTotalDragDistance();, 
canvas.clipRect(0, -mTop, mScreenWidth, mParent.getTotalDragDistance()); 
这里一开始是截取了一个高度为0的矩形,随着move慢慢变大,后面的draw就只能在这上面操作。重要的就讲完了,其他的包括怎么根据百分比来改变属性,这些其实可以自己发挥实现自己的效果。

示例代码:

mPullToRefreshView = (PullToRefreshView) findViewById(R.id.pull_to_refresh);
mPullToRefreshView.setOnRefreshListener(new PullToRefreshView.OnRefreshListener() {    
@Override
    public void onRefresh() {
        mPullToRefreshView.postDelayed(new Runnable() {           
 @Override
            public void run() {
                mPullToRefreshView.setRefreshing(false);
            }
        }, REFRESH_DELAY);
    }
 });

    原文作者:Android源码分析
    原文地址: https://blog.csdn.net/l331258747/article/details/51259823
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞