一、概述
本篇续 厦门之旅 的第二篇。这期间找工作真的心态几多变化,刚开始兴致高昂,信心满满,面试了几家不错的公司,结果都是因为工资问题不了了之。后面的连面试机会都没有了,每天在狭小的租房里面吃了睡,睡了玩,陌生的环境消磨这我的意志。我很讨厌消沉的自我,这边招 Android 开发并没有我以为的那么多,实在是太少了,想找到满意的工作更是难上加难。引用公众号【AndroidDeveloper】的一句话
愿意积极争取,肯努力上进的年轻人,运气不会太差
在我快要放弃在这边找工作的时候,收到了一家公司的面试通知,并顺利的拿到了 offer,他们公司旗下有一款 咪咕动漫 的产品。本篇我以自己的方式实现了 App 中列表的下拉刷新以及上拉加载。
二、效果展示
效果图一栏:
三、具体实现
1、图片资源
下载 咪咕动漫App ,修改成 .zip 格式并解压。获取到图片资源如下:
pull_down (下拉)
pull_end (释放刷新)
refreshing_01 (正在刷新图片_1)
refreshing_02 (正在刷新图片_2)
refreshing_03 (正在刷新图片_3)
正在刷新的帧动画:
<?xml version="1.0" encoding="utf-8"?>
<animation-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:duration="100" android:drawable="@drawable/refreshing_01" />
<item android:duration="100" android:drawable="@drawable/refreshing_02" />
<item android:duration="100" android:drawable="@drawable/refreshing_03" />
</animation-list>
属性 android:oneshot="false" 表示动画循环播放。 如果为 true,表示动画只播放一次停止在最后一帧上。
2、下拉刷新
原理浅析
下拉刷新作为一个单独控件添加到列表顶部,并且初始状态的高度为 0 ,随着手指触摸的偏移量高度而发生改变,并且在不同的状态之间来回切换。控件的四种状态:
STATE_NORMAL 下拉状态 (高度小于刷新的临界高度) 默认 40dp
STATE_RELEASE_TO_REFRESH 释放刷新状态 (高度大于刷新的临界高度)
STATE_REFRESHING 刷新状态 (高度大于刷新的临界高度,手指释放后的状态)
STATE_DONE 刷新完成状态
处了四种状态,还需要实现三个方法:
void onMove(float delta); 移动,参数 delta 两点之间的偏移量
boolean releaseAction(); 释放是否满足刷新状态
void refreshComplete(); 刷新完成
刷新控件(MiGuRefreshHeader)
MiGuRefreshHeader 控件继承 LinearLayout ,MiGuRefreshHeader 的构造方法:
public MiGuRefreshHeader(Context context) {
this(context, null);
}
public MiGuRefreshHeader(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MiGuRefreshHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
初始化 View,比较简单我这里就不在细讲,文章最后会附上源码:
private void initView() {
// 初始情况,设置下拉刷新view高度为0
mContainer = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.refresh_header, null);
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
lp.setMargins(0, 0, 0, 0);
this.setLayoutParams(lp);
this.setPadding(0, 0, 0, 0);
addView(mContainer, new LayoutParams(LayoutParams.MATCH_PARENT, 0));
setGravity(Gravity.BOTTOM);
//图片控件
mMiGuImageView = (ImageView) findViewById(R.id.iv_refresh);
//文本控件
mStatusTextView = (TextView) findViewById(R.id.tv_status);
//获取控件的默认高度方法一
measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mMeasureHeight = getMeasuredHeight();//获取控件高度 默认 40dp 由于测试机密度为 3 所以像素为 120px
}
获取控件的默认高度方式二:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mMeasureHeight = h;
}
不同状态下文本控件,图片控件的显示:
public void setState(int state) {
if (state == mState) return;
if (state == STATE_NORMAL) {
//下拉
mMiGuImageView.setImageResource(R.drawable.pull_down);
mStatusTextView.setText(R.string.pull_refresh);
} else if (state == STATE_RELEASE_TO_REFRESH) {
//释放
mMiGuImageView.setImageResource(R.drawable.pull_end);
mStatusTextView.setText(R.string.release_refresh);
} else if (state == STATE_REFRESHING) {
//刷新
mStatusTextView.setText(R.string.refreshing);
mMiGuImageView.setImageResource(R.drawable.refreshing);
mMiGuDrawable = (AnimationDrawable) mMiGuImageView.getDrawable();
//播放动画
mMiGuDrawable.start();
smoothScrollTo(mMeasureHeight);
} else if (state == STATE_DONE) {
//完成
if (mMiGuDrawable != null)
//停止动画
mMiGuDrawable.stop();
}
mState = state;
}
如果你需要替换文本或图片,请修改这里。
以下是3个方法的实现,onMove(float delta) 方法:
if (getVisibleHeight() > 0 || delta > 0) {
//控件滑动的距离
setVisibleHeight((int) delta + getVisibleHeight());
//处于释放刷新状态
if (mState <= STATE_RELEASE_TO_REFRESH) {
//判定距离是否大于刷新的临界值
if (getVisibleHeight() < mMeasureHeight) {
setState(STATE_NORMAL);
} else {
setState(STATE_RELEASE_TO_REFRESH);
}
}
}
releaseAction() 方法:
@Override
public boolean releaseAction() {
boolean isOnRefresh = false;
int height = getVisibleHeight();
if (height == 0) {
isOnRefresh = false;
}
if (getVisibleHeight() > mMeasureHeight && mState < STATE_REFRESHING) {
//刷新状态
setState(STATE_REFRESHING);
isOnRefresh = true;
}
if (mState == STATE_REFRESHING && height <= mMeasureHeight) {
//处于刷新状态,手指还在向上滑动
}
if (mState != STATE_REFRESHING) {
smoothScrollTo(0);
}
if (mState == STATE_REFRESHING) {
int destHeight = mMeasureHeight;
smoothScrollTo(destHeight);
}
return isOnRefresh;
}
刷新完成 refreshComplete() 方法:
@Override
public void refreshComplete() {
setState(STATE_DONE);
reset();
}
理解了刷新控件的四种状态,再来分析代码就比较容易了。接着我们处理 RecyclerView 的 onTouchEvent 方法获取 Y 轴的偏移量作为参数传入 onMove(float delta) 方法中。
MiGuRecyclerView
MiGuRecyclerView 继承 RecyclerView 控件。主要分析 onTouchEvent 方法,如果你对其他地方还有疑问,请留言。
注意:本文使用的 RecyclerView 基于RecyclerView 之通用适配,重写 setAdapter 方法添加头部刷新控件:
mRefreshAdapter.addHeaderView(mMiGuRefreshHeader, 0);
onTouchEvent 方法代码如下:
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mLastY == -1) {
mLastY = ev.getRawY();
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
if (mRefreshAdapter != null) {
final float deltaY = ev.getRawY() - mLastY;
mLastY = ev.getRawY();
if (isScrollTop && isOnTop() && pullRefreshEnabled && appbarState == AppBarStateChangeListener.State.EXPANDED) {
mMiGuRefreshHeader.onMove(deltaY / DRAG_RATE);
if (mMiGuRefreshHeader.getVisibleHeight() > 0 && mMiGuRefreshHeader.getState() < MiGuRefreshHeader.STATE_REFRESHING) {
return false;
}
}
}
break;
default:
if (mRefreshAdapter != null) {
mLastY = -1; // reset
if (isScrollTop && isOnTop() && pullRefreshEnabled && appbarState == AppBarStateChangeListener.State.EXPANDED) {
if (mMiGuRefreshHeader.releaseAction()) {
if (mRefreshListener != null) {
mRefreshListener.onRefresh();
}
}
}
}
break;
}
return super.onTouchEvent(ev);
}
ACTION_DOWN 手势获取相对于屏幕触摸点 Y 坐标
final float deltaY = ev.getRawY() - mLastY;
mLastY = ev.getRawY();
获取手势滑动两点的偏移量,isScrollTop 判定当前 RecyclerView 是否滑动到顶部。我采取了重写 onScrolled 方法通过:
findFirstCompletelyVisibleItemPosition();
来判定 isScrollTop 值,如果您有好的方案可以给我留言,万分感谢。
3、上拉加载
【咪咕动漫】采取的是播放帧动画,比较简单。实现方案你可以参考源码。
技术交流群欢迎你的加入
参考文献