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