1.案例演示
2.涉及到的知识点
1.自定义控件通用接口封装
2.安卓手势分发 GestureDetector 拦截处理
3.自定义控件之onLayout布局
4.借助Scroller实现视图的自动滚动
3.整个控件基础类图结构
4.主要代码
1.首先定义 HiRefreshLayout
的 通用接口
下拉刷新控件:
1.可设置是否下拉刷新时禁止滚动
2.刷新完成时状态更新,及派发相应的监听事件
3.提供给使用者设置监听器以获取控件的不同状态回调
4.使用者可自行设置 刷新要显示的视图,只要继承自 HiOverView 抽象类即可
public interface HiRefresh {
/**
* 刷新时是否禁止滚动
*
* @param disableRefreshScroll 否禁止滚动
*/
void setDisableRefreshScroll(boolean disableRefreshScroll);
/**
* 刷新完成
*/
void refreshFinished();
/**
* 设置下拉刷新的监听器
*
* @param hiRefreshListener 刷新的监听器
*/
void setRefreshListener(HiRefresh.HiRefreshListener hiRefreshListener);
/**
* 设置下拉刷新的视图
*
* @param hiOverView 下拉刷新的视图
*/
void setRefreshOverView(HiOverView hiOverView);
interface HiRefreshListener {
void onRefresh();
boolean enableRefresh();
}
}
2.Header 控件的基本属性
下拉过程中 Header 的不同状态,更新状态和视图显示。
松开手后,根据当前 Header 的状态来做相应的逻辑处理
/**
* 下拉刷新的Overlay视图,可以重载这个类来定义自己的Overlay
*/
public abstract class HiOverView extends FrameLayout {
public enum HiRefreshState {
/**
* 初始态
*/
STATE_INIT,
/**
* Header展示的状态
*/
STATE_VISIBLE,
/**
* 超出可刷新距离的状态
*/
STATE_OVER,
/**
* 刷新中的状态
*/
STATE_REFRESH,
/**
* 超出刷新位置松开手后的状态
*/
STATE_OVER_RELEASE
}
protected HiRefreshState mState = HiRefreshState.STATE_INIT;
/**
* 触发下拉刷新 需要的最小高度
*/
public int mPullRefreshHeight;
/**
* 最小阻尼
*/
public float minDamp = 1.6f;
/**
* 最大阻尼
*/
public float maxDamp = 2.2f;
public HiOverView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
preInit();
}
public HiOverView(Context context, AttributeSet attrs) {
super(context, attrs);
preInit();
}
public HiOverView(Context context) {
super(context);
preInit();
}
protected void preInit() {
mPullRefreshHeight = HiDisplayUtil.dp2px(66, getResources());
init();
}
/**
* 初始化
*/
public abstract void init();
protected abstract void onScroll(int scrollY, int pullRefreshHeight);
/**
* 显示Overlay
*/
protected abstract void onVisible();
/**
* 超过Overlay,释放就会加载
*/
public abstract void onOver();
/**
* 开始加载
*/
public abstract void onRefresh();
/**
* 加载完成
*/
public abstract void onFinish();
/**
* 设置状态
*
* @param state 状态
*/
public void setState(HiRefreshState state) {
mState = state;
}
/**
* 获取状态
*
* @return 状态
*/
public HiRefreshState getState() {
return mState;
}
}
3.下拉刷新控件
/**
* author : shengping.tian
* time : 2021/07/30
* desc : 下拉刷新View
* version: 1.0
*/
public class HiRefreshLayout extends FrameLayout implements HiRefresh {
private static final String TAG = HiRefreshLayout.class.getSimpleName();
//当前刷新状态
private HiOverView.HiRefreshState mState;
//手势监听
private GestureDetector mGestureDetector;
//自动滚动
private AutoScroller mAutoScroller;
//刷新结果回调
private HiRefresh.HiRefreshListener mHiRefreshListener;
//刷新显示的布局
protected HiOverView mHiOverView;
private int mLastY;
//刷新时是否禁止滚动
private boolean disableRefreshScroll;
public HiRefreshLayout(@NonNull Context context) {
this(context, null);
}
public HiRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public HiRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mGestureDetector = new GestureDetector(getContext(), hiGestureDetector);
mAutoScroller = new AutoScroller();
}
@Override
public void setDisableRefreshScroll(boolean disableRefreshScroll) {
this.disableRefreshScroll = disableRefreshScroll;
}
@Override
public void refreshFinished() {
final View head = getChildAt(0);
Log.i(TAG, "refreshFinished head-bottom:" + head.getBottom());
mHiOverView.onFinish();
mHiOverView.setState(HiOverView.HiRefreshState.STATE_INIT);
final int bottom = head.getBottom();
if (bottom > 0) {
//下over pull 200,height 100
// bottom =100 ,height 100
recover(bottom);
}
mState = HiOverView.HiRefreshState.STATE_INIT;
}
@Override
public void setRefreshListener(HiRefreshListener hiRefreshListener) {
mHiRefreshListener = hiRefreshListener;
}
/**
* 设置下拉刷新的视图
*
* @param hiOverView
*/
@Override
public void setRefreshOverView(HiOverView hiOverView) {
if (this.mHiOverView != null) {
removeView(mHiOverView);
}
this.mHiOverView = hiOverView;
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
addView(mHiOverView, 0, params);
}
/**
* 手指往下滑动, distanceY 为负数
*/
HiGestureDetector hiGestureDetector = new HiGestureDetector() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.e(TAG, "onScroll distanceY:" + distanceY);
if (Math.abs(distanceX) > Math.abs(distanceY) || mHiRefreshListener != null && !mHiRefreshListener.enableRefresh()) {
//横向滑动距离大于纵向滑动距离 不处理,或者设置不支持下拉刷新 enableRefresh,则该事件统一不处理,返回 false
return false;
}
if (disableRefreshScroll && mState == HiOverView.HiRefreshState.STATE_REFRESH) {
//刷新时候是否禁止滑动,并且当前状态是刷新状态,拦截事件
return true;
}
View head = getChildAt(0);
//找到第一个可以滑动的View
View child = HiScrollUtil.findScrollableChild(HiRefreshLayout.this);
if (HiScrollUtil.childScrolled(child)) {
//如果列表发生了滚动则不处理
return false;
}
//没有刷新或没有达到可以刷新的距离,且头部已经划出或下拉
if ((mState != HiOverView.HiRefreshState.STATE_REFRESH || head.getBottom() <= mHiOverView.mPullRefreshHeight) && (head.getBottom() > 0 || distanceY <= 0.0F)) {
//还在滑动中
if (mState != HiOverView.HiRefreshState.STATE_OVER_RELEASE) {
int speed;
//阻尼计算
if (child.getTop() < mHiOverView.mPullRefreshHeight) {
speed = (int) (mLastY / mHiOverView.minDamp);
} else {
speed = (int) (mLastY / mHiOverView.maxDamp);
}
//如果是正在刷新状态,则不允许在滑动的时候改变状态
boolean bool = moveDown(speed, true);
mLastY = (int) (-distanceY);
return bool;
} else {
return false;
}
} else {
return false;
}
}
};
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//事件分发处理
if (!mAutoScroller.isFinished()) {
return false;
}
View head = getChildAt(0);
if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL
|| ev.getAction() == MotionEvent.ACTION_POINTER_INDEX_MASK) {//松开手
if (head.getBottom() > 0) {
if (mState != HiOverView.HiRefreshState.STATE_REFRESH) {//正在刷新
recover(head.getBottom());
return false;
}
}
mLastY = 0;
}
boolean consumed = mGestureDetector.onTouchEvent(ev);
Log.i(TAG, "gesture consumed:" + consumed);
if ((consumed || (mState != HiOverView.HiRefreshState.STATE_INIT && mState != HiOverView.HiRefreshState.STATE_REFRESH)) && head.getBottom() != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);//让父类接受不到真实的事件
return super.dispatchTouchEvent(ev);
}
if (consumed) {
return true;
} else {
return super.dispatchTouchEvent(ev);
}
}
/**
* 定义 Head的位置
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//定义head和child的排列位置
View head = getChildAt(0);
View child = getChildAt(1);
if (head != null && child != null) {
Log.i(TAG, "onLayout head-height:" + head.getMeasuredHeight());
int childTop = child.getTop();
if (mState == HiOverView.HiRefreshState.STATE_REFRESH) {
head.layout(0, mHiOverView.mPullRefreshHeight - head.getMeasuredHeight(), right, mHiOverView.mPullRefreshHeight);
child.layout(0, mHiOverView.mPullRefreshHeight, right, mHiOverView.mPullRefreshHeight + child.getMeasuredHeight());
} else {
//left,top,right,bottom
head.layout(0, childTop - head.getMeasuredHeight(), right, childTop);
child.layout(0, childTop, right, childTop + child.getMeasuredHeight());
}
View other;
//让HiRefreshLayout节点下两个以上的child能够不跟随手势移动以实现一些特殊效果,如悬浮的效果
for (int i = 2; i < getChildCount(); ++i) {
other = getChildAt(i);
other.layout(0, top, right, bottom);
}
Log.i(TAG, "onLayout head-bottom:" + head.getBottom());
}
}
private void recover(int dis) {//dis =200 200-100
if (mHiRefreshListener != null && dis > mHiOverView.mPullRefreshHeight) {
mAutoScroller.recover(dis - mHiOverView.mPullRefreshHeight);
mState = HiOverView.HiRefreshState.STATE_OVER_RELEASE;
} else {
mAutoScroller.recover(dis);
}
}
/**
* 根据偏移量移动header与child
*
* @param offsetY 偏移量
* @param nonAuto 是否非自动滚动触发
*/
private boolean moveDown(int offsetY, boolean nonAuto) {
Log.i(TAG,"moveDown nonAuto = " + nonAuto);
//todo 需要添加默认的 Header,以及解决界面空布局出现下拉刷新异常
View head = getChildAt(0);
View child = getChildAt(1);
int childTop = child.getTop() + offsetY;
Log.i(TAG, "moveDown head-bottom:" + head.getBottom() + ",child.getTop():" + child.getTop() + ",offsetY:" + offsetY);
if (childTop <= 0) {
offsetY = -child.getTop();
//移动head与child的位置,到原始位置
head.offsetTopAndBottom(offsetY);
child.offsetTopAndBottom(offsetY);
if (mState != HiOverView.HiRefreshState.STATE_REFRESH) {
mState = HiOverView.HiRefreshState.STATE_INIT;
}
} else if (mState == HiOverView.HiRefreshState.STATE_REFRESH && childTop > mHiOverView.mPullRefreshHeight) {
//如果正在下拉刷新中,禁止继续下拉
return false;
} else if (childTop <= mHiOverView.mPullRefreshHeight) {//还没超出设定的刷新距离
if (mHiOverView.getState() != HiOverView.HiRefreshState.STATE_VISIBLE && nonAuto) {//头部开始显示
mHiOverView.onVisible();
mHiOverView.setState(HiOverView.HiRefreshState.STATE_VISIBLE);
mState = HiOverView.HiRefreshState.STATE_VISIBLE;
}
head.offsetTopAndBottom(offsetY);
child.offsetTopAndBottom(offsetY);
if (childTop == mHiOverView.mPullRefreshHeight && mState == HiOverView.HiRefreshState.STATE_OVER_RELEASE) {
Log.i(TAG, "refresh,childTop:" + childTop);
refresh();
}
} else {
if (mHiOverView.getState() != HiOverView.HiRefreshState.STATE_OVER && nonAuto) {
//超出刷新位置
mHiOverView.onOver();
mHiOverView.setState(HiOverView.HiRefreshState.STATE_OVER);
}
head.offsetTopAndBottom(offsetY);
child.offsetTopAndBottom(offsetY);
}
if (mHiOverView != null) {
mHiOverView.onScroll(head.getBottom(), mHiOverView.mPullRefreshHeight);
}
return true;
}
/**
* 刷新
*/
private void refresh() {
if (mHiRefreshListener != null) {
mState = HiOverView.HiRefreshState.STATE_REFRESH;
mHiOverView.onRefresh();
mHiOverView.setState(HiOverView.HiRefreshState.STATE_REFRESH);
mHiRefreshListener.onRefresh();
}
}
/**
* 借助Scroller实现视图的自动滚动
* https://juejin.im/post/5c7f4f0351882562ed516ab6
*/
private class AutoScroller implements Runnable {
private Scroller mScroller;
private int mLastY;
private boolean mIsFinished;
AutoScroller() {
mScroller = new Scroller(getContext(), new LinearInterpolator());
mIsFinished = true;
}
@Override
public void run() {
//当您想知道新位置时调用此方法。 如果返回 true,则动画尚未完成
if (mScroller.computeScrollOffset()) {
moveDown(mLastY - mScroller.getCurrY(), false);
mLastY = mScroller.getCurrY();
post(this);
} else {
//移除回调
removeCallbacks(this);
mIsFinished = true;
}
}
/**
* y方向滚动
*
* @param dis 距离
*/
void recover(int dis) {
if (dis <= 0) return;
removeCallbacks(this);
mLastY = 0;
mIsFinished = false;
mScroller.startScroll(0, 0, 0, dis, 300);
post(this);
}
boolean isFinished() {
return mIsFinished;
}
}
}
4.自定义 HiOverView 头部视图
1.定义个显示文本样式的头部
public class HiTextOverView extends HiOverView{
private TextView mText;
private View mRotateView;
public HiTextOverView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public HiTextOverView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HiTextOverView(Context context) {
super(context);
}
@Override
public void init() {
LayoutInflater.from(getContext()).inflate(R.layout.hi_refresh_overview, this, true);
mText = findViewById(R.id.text);
mRotateView = findViewById(R.id.iv_rotate);
}
@Override
protected void onScroll(int scrollY, int pullRefreshHeight) {
}
@Override
public void onVisible() {
mText.setText("下拉刷新");
}
@Override
public void onOver() {
mText.setText("松开刷新");
}
@Override
public void onRefresh() {
mText.setText("正在刷新...");
Animation operatingAnim = AnimationUtils.loadAnimation(getContext(), R.anim.rotate_anim);
LinearInterpolator lin = new LinearInterpolator();
operatingAnim.setInterpolator(lin);
mRotateView.startAnimation(operatingAnim);
}
@Override
public void onFinish() {
mRotateView.clearAnimation();
}
}
布局视图
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/refresh_area"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_rotate"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="10dp"
android:src="@drawable/rotate_daisy" />
<TextView
android:id="@+id/text"
android:visibility="visible"
android:layout_marginBottom="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</merge>
2.自定义动画 Header
package com.tsp.android.test.refresh;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import com.airbnb.lottie.LottieAnimationView;
import com.tsp.android.hiui.refresh.HiOverView;
import com.tsp.android.jgs.R;
public class HiLottieOverView extends HiOverView {
private LottieAnimationView pullAnimationView;
public HiLottieOverView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public HiLottieOverView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HiLottieOverView(Context context) {
super(context);
}
@Override
public void init() {
LayoutInflater.from(getContext()).inflate(R.layout.lottie_overview, this, true);
pullAnimationView = findViewById(R.id.pull_animation);
pullAnimationView.setAnimation("loading_wave.json");
}
@Override
protected void onScroll(int scrollY, int pullRefreshHeight) {
}
@Override
public void onVisible() {
}
@Override
public void onOver() {
}
@Override
public void onRefresh() {
pullAnimationView.setSpeed(2);
pullAnimationView.playAnimation();
}
@Override
public void onFinish() {
pullAnimationView.setProgress(0f);
pullAnimationView.cancelAnimation();
}
}
xml布局:
<?xml version="1.0" encoding="UTF-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/refresh_overView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/refresh_area"
android:layout_width="match_parent"
android:layout_height="66dp"
android:layout_gravity="bottom"
android:gravity="bottom|center_horizontal"
android:orientation="vertical">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/pull_animation"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:lottie_autoPlay="false"
app:lottie_loop="true"/>
</LinearLayout>
</merge>
注意 LottieAnimationView 需要引入 implementation “com.airbnb.android:lottie:3.3.0” 库
5.具体使用
1.在布局中引入下拉刷新视图
<?xml version="1.0" encoding="utf-8"?>
<com.tsp.android.hiui.refresh.HiRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/hiRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.tsp.android.test.refresh.RefreshDemoActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.tsp.android.hiui.refresh.HiRefreshLayout>
2.Activity中加载视图
class RefreshDemoActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_refresh_demo)
val refreshLayout = findViewById<HiRefreshLayout>(R.id.hiRefresh)
val xOverView = HiTextOverView(this)
// val lottieOverView = HiLottieOverView(this)
refreshLayout.setRefreshOverView(xOverView)
refreshLayout.setRefreshListener(object : HiRefresh.HiRefreshListener {
override fun onRefresh() {
Handler(Looper.getMainLooper()).postDelayed({
refreshLayout.refreshFinished()
}, 1000)
}
override fun enableRefresh(): Boolean {
return true
}
})
refreshLayout.setDisableRefreshScroll(false)
initView()
}
private fun initView() {
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val data = arrayOf("RefreshItem", "RefreshItem", "RefreshItem", "RefreshItem", "RefreshItem", "RefreshItem", "RefreshItem")
val layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
val mAdapter = MyAdapter(data)
recyclerView.adapter = mAdapter
}
class MyAdapter(private val mDataset: Array<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
class MyViewHolder(v: View) : RecyclerView.ViewHolder(v) {
// each data item is just a string in this case
var textView: TextView
init {
textView = v.findViewById(R.id.tv_title)
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): MyViewHolder {
val v = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return MyViewHolder(v)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.textView.text = mDataset[position]
holder.itemView.setOnClickListener {
Log.d("MyAdapter","position:$position")
}
}
override fun getItemCount(): Int {
return mDataset.size
}
}
}