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

来源:互联网 发布:图灵系列图书 知乎 编辑:程序博客网 时间:2024/05/22 03:22

向纳什致敬,凤凰城永远的英雄!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

@Overrideprotected 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一样大小。
@Overrideprotected 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),首先重写的是拦截的方法
@Overridepublic 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方法


@Overridepublic 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);    } });


0 0
原创粉丝点击