[置顶] 一步步实现带动画效果的下拉刷新

时间:2023-02-08 19:58:00

先看效果

[置顶]        一步步实现带动画效果的下拉刷新

分析
1.先要在listview的头部加上一个布局,布局中包含一个文本控件一个图片2.这个图片控件会随着下拉的过程做一个缩放3.整个下拉刷新过程分三步:第一步:下拉未超过布局的原始高度,图片做缩放动作,文字显示下拉刷新第二步:下拉超过布局的原始高度,图片大小保持不变,文字显示松开刷新第三步:松手后,如果当前位置在原始高度的上方,不进行刷新,直接回弹;如果在下方,执行刷新任务,并播放动画效果,完成后回弹 
实战
1.我们需要一个能随着滑动改变自身大小的自定义控件,继承view是个不错的选择2.通过画布的缩放来控制图片的大小,同时别忘了处理padding 
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//考虑padding的影响
int leftPadding = getPaddingLeft();
int topPadding = getPaddingTop();
int rightPadding = getPaddingRight();
int bottomPadding = getPaddingBottom();

int lastWidth = getMeasuredWidth() - leftPadding - rightPadding;
int lastHeight = getMeasuredHeight() - topPadding - bottomPadding;

scaleBitmap = Bitmap.createScaledBitmap(initBitmap, lastWidth, lastHeight, true);

canvas.save();
//缩放画布
canvas.scale(mCurrentProgress, mCurrentProgress, lastWidth / 2 + leftPadding, lastHeight / 2 + topPadding);
//缩放图形,要写在画布缩放后边
canvas.drawBitmap(scaleBitmap, topPadding, leftPadding, null);

canvas.restore();
}
3.需要一个方法供外部调用,用于控制绘制控件的大小
public void setCurrentProgress(float currentProgress) {
mCurrentProgress = currentProgress;
postInvalidate();
}
4.完整的代码
public class ScaleView extends View {
private Bitmap initBitmap;
private Bitmap scaleBitmap;
private float mCurrentProgress = 1;
private int mWidth;
private int mHeight;

public ScaleView(Context context) {
super(context);
init(context);
}

public ScaleView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}

public ScaleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

private void init(Context context) {
initBitmap = Bitmap.createBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.bell));
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (widthMode == MeasureSpec.EXACTLY) {
mWidth = widthSize;
} else {
mWidth = initBitmap.getWidth();
}

if (heightMode == MeasureSpec.EXACTLY) {
mHeight = heightSize;
} else {
mHeight = initBitmap.getHeight();
}

setMeasuredDimension(mWidth, mHeight);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//考虑padding的影响
int leftPadding = getPaddingLeft();
int topPadding = getPaddingTop();
int rightPadding = getPaddingRight();
int bottomPadding = getPaddingBottom();

int lastWidth = getMeasuredWidth() - leftPadding - rightPadding;
int lastHeight = getMeasuredHeight() - topPadding - bottomPadding;

scaleBitmap = Bitmap.createScaledBitmap(initBitmap, lastWidth, lastHeight, true);

canvas.save();
//缩放画布
canvas.scale(mCurrentProgress, mCurrentProgress, lastWidth / 2 + leftPadding, lastHeight / 2 + topPadding);
//缩放图形,要写在画布缩放后边
canvas.drawBitmap(scaleBitmap, topPadding, leftPadding, null);

canvas.restore();
}

public void setCurrentProgress(float currentProgress) {
mCurrentProgress = currentProgress;
postInvalidate();
}
}
5.开始实现继承listview的PullRefreshListView。先加载布局
mHeadView = LayoutInflater.from(context).inflate(R.layout.item_headview, null, false);
loadMoreView = (ScaleView) mHeadView.findViewById(R.id.loadMoreView);
tv = (TextView) mHeadView.findViewById(R.id.tv);
addHeaderView(mHeadView);
6.根据下拉的距离,控制headview的状态。靠listView的paddingTop来控制headView的显示程度
@Override
public boolean onTouchEvent(MotionEvent ev) {
if(!refreshEnable || isAnimatoring)
{
return super.onTouchEvent(ev);
}

y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
//下拉,不超过原始的布局高度

if (mfirstVisibleItem == 0 && y > mLastY && offsetY < mHeadViewHeight) {
changState();
}
//上滑
if (mfirstVisibleItem == 0 && y < mLastY && offsetY > 0) {
changState();
}
break;
case MotionEvent.ACTION_UP:
int curPaddingTop = getPaddingTop();

if (curPaddingTop > 0) {
isAnimatoring = true;

refreshingState();

mObjectAnimator = startRefreshAnim(loadMoreView);
post(new Runnable() {
@Override
public void run() {
mOnPullRefreshListener.onRefresh();
}
});
} else {
resetState();
}
break;
}
mLastY = y;
return super.onTouchEvent(ev);
}
7.抖动的动画其实就是x方向的来回位移
private ObjectAnimator startRefreshAnim(ScaleView target) {
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(target, View.TRANSLATION_X, 0, 20, 0, -20, 0);
objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
objectAnimator.setInterpolator(new DecelerateInterpolator());
objectAnimator.setDuration(ANIM_DURATION);
objectAnimator.start();
return objectAnimator;
}
8.提供监听供调用刷新任务,同时提供任务完成的终止方法
/**
* 刷新完成
*/
public void complete()
{
mObjectAnimator.cancel();
resetState();
isAnimatoring = false;
}

/**
* 设置刷新回调监听
* @param onPullRefreshListener
*/
public void setOnPullRefreshListener(OnPullRefreshListener onPullRefreshListener) {
if (onPullRefreshListener == null) {
return;
}
this.mOnPullRefreshListener = onPullRefreshListener;
refreshEnable = true;
}

public interface OnPullRefreshListener {
void onRefresh();
}
9.完整的源码
public class PullRefreshListView extends ListView implements AbsListView.OnScrollListener {
private ScaleView loadMoreView;
private TextView tv;
private View mHeadView;
private int mHeadViewHeight;
private float mLastY, y, offsetY;
private int mfirstVisibleItem;
/**
* 动画播放时间
*/
private static final int ANIM_DURATION = 200;
/**
* 缩小滑动时对padding的影响
*/
private static final int RESISTANCE = 3;
/**
* 是否实现下拉刷新接口
*/
private boolean refreshEnable = false;
/**
* 是否在播放动画
*/
private boolean isAnimatoring = false;
/**
* 下拉刷新回调接口
*/
private OnPullRefreshListener mOnPullRefreshListener;
/**
* 刷新动画
*/
private ObjectAnimator mObjectAnimator;

public PullRefreshListView(Context context) {
super(context);
init(context);
}

public PullRefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}

public PullRefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

private void init(Context context) {
setOverScrollMode(OVER_SCROLL_NEVER);

//先把布局加载进来
mHeadView = LayoutInflater.from(context).inflate(R.layout.item_headview, null, false);
loadMoreView = (ScaleView) mHeadView.findViewById(R.id.loadMoreView);
tv = (TextView) mHeadView.findViewById(R.id.tv);
addHeaderView(mHeadView);

post(new Runnable() {
@Override
public void run() {
//把headView的高度取出来
mHeadViewHeight = mHeadView.getMeasuredHeight();
resetState();
}
});
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
if(!refreshEnable || isAnimatoring)
{
return super.onTouchEvent(ev);
}

y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
//下拉,最多下拉到2倍高度的位置
if (mfirstVisibleItem == 0 && y > mLastY && offsetY < 2 * mHeadViewHeight) {
changState();
}
//上滑
if (mfirstVisibleItem == 0 && y < mLastY && offsetY > 0) {
changState();
}
break;
case MotionEvent.ACTION_UP:
int curPaddingTop = getPaddingTop();

if (curPaddingTop > 0) {
isAnimatoring = true;

refreshingState();

mObjectAnimator = startRefreshAnim(loadMoreView);
post(new Runnable() {
@Override
public void run() {
mOnPullRefreshListener.onRefresh();
}
});
} else {
resetState();
}
break;
}
mLastY = y;
return super.onTouchEvent(ev);
}

/**
* 正在刷新的状态
*/
private void refreshingState() {
setHeadViewPadding(mHeadViewHeight);
setCurrentProgress(mHeadViewHeight);
offsetY = mHeadViewHeight;
tv.setText("正在刷新");
}

/**
* 将状态设置回原始状态
*/
private void resetState() {
offsetY = 0;
setHeadViewPadding(0);
setCurrentProgress(0);
}

/**
* 滑动时动态设置各个组件的状态
*/
private void changState() {
offsetY = offsetY + (y - mLastY) / RESISTANCE;
setHeadViewPadding((int) (offsetY));
//从二分之一的地方开始缩放,使缩放效果更明显
if (offsetY > mHeadViewHeight / 2) {
setCurrentProgress((offsetY - mHeadViewHeight / 2) * 2);
}
//设置字体状态
if (offsetY > mHeadViewHeight) {
tv.setText("松开刷新");
} else {
tv.setText("下拉刷新");
}
}

/**
* 播放刷新动画
*
* @param target
*/
private ObjectAnimator startRefreshAnim(ScaleView target) {
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(target, View.TRANSLATION_X, 0, 20, 0, -20, 0);
objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
objectAnimator.setInterpolator(new DecelerateInterpolator());
objectAnimator.setDuration(ANIM_DURATION);
objectAnimator.start();
return objectAnimator;
}

/**
* 根据滑动的距离设置图片的缩放
*
* @param offsetY
*/
private void setCurrentProgress(float offsetY) {
float scale = offsetY / mHeadViewHeight;
scale = scale > 1 ? 1 : scale;
loadMoreView.setCurrentProgress(scale);
}

/**
* 位移相对于隐藏headview原点
*
* @param offset
*/
private void setHeadViewPadding(int offset) {
setPadding(0, offset - mHeadViewHeight, 0, 0);
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
mfirstVisibleItem = firstVisibleItem;
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {

}

/**
* 刷新完成
*/
public void complete()
{
mObjectAnimator.cancel();
resetState();
isAnimatoring = false;
}

/**
* 设置刷新回调监听
* @param onPullRefreshListener
*/
public void setOnPullRefreshListener(OnPullRefreshListener onPullRefreshListener) {
if (onPullRefreshListener == null) {
return;
}
this.mOnPullRefreshListener = onPullRefreshListener;
refreshEnable = true;
}

public interface OnPullRefreshListener {
void onRefresh();
}
}
10.试一下
public class MainActivity extends AppCompatActivity implements PullRefreshListView.OnPullRefreshListener {
private List<String> mDatas;
private ArrayAdapter<String> mAdapter;
private PullRefreshListView mPullRefreshListView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

init();
}

private void init() {
try {
mPullRefreshListView = (PullRefreshListView) findViewById(R.id.pullRefreshListView);
initData();
mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mDatas);
mPullRefreshListView.setOnPullRefreshListener(this);
mPullRefreshListView.setAdapter(mAdapter);
} catch (Exception e) {
e.printStackTrace();
}
}

private void initData() {
mDatas = new ArrayList<>();
for (int i=0; i<20; i++)
{
mDatas.add(String.valueOf(i));
}
}

@Override
public void onRefresh() {
Toast.makeText(MainActivity.this, "success", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
try {
//模拟耗时任务
Thread.sleep(3000);

MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
//任务执行完毕
mPullRefreshListView.complete();
}
});

} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}

总结
1.headview的位置变化没有使用弹性滑动,可以完善2.可以在刷新阶段加入更多酷炫的动画3.上拉加载后边加上4.后边用RecyclerView来实现下 

源码地址:https://github.com/wolow3/PullRefreshListView