需求分析:
设计一个下拉刷新组件,能够提供通用的API,并且支持自定义Head。关于这块其实在之前https://www.cnblogs.com/webor2006/p/7989766.html已经有练习过,不过这次是从构造的角度再来重新对它进行一个从无到有的完整梳理。
成果展示:
其中下拉刷新的头部可以动态进行替换,基本上通用下拉刷新都会支持滴,比如这种下拉样式:
疑难点分析:
- 手势的处理
- 事件的分发
- 视图的移动与自动滚动
- 状态的管理
- 头部的可定制
架构纵览:
还是跟之前一样,纵览一下其整个咱们要实现的下拉刷新组件的整体架构类图:
依然是当个图片先瞅瞅,随着下面的一点点实现再来理解它。
具体实现:
新建包名:
此时由于咱们要实现的又是属于hi-ui库中的一员,所以又回到hi-ui的工程中来:
然后这里面再建一个包名,将所有相关的代码都存于此:
HiRefresh接口定义:
对于一个通用框架的设计面向接口编程基本是核心,所以接下来定义它:
1、首先定义一个是否可以禁止滑动的接口:
package org.devio.hi.ui.refresh;
/**
* HiRefresh对外的通用接口
*/
public interface HiRefresh {
/**
* 刷新时是否禁止滚动
*
* @param disableRefreshScroll 否禁止滚动
*/
void setDisableRefreshScroll(boolean disableRefreshScroll);
}
因为有时候可能咱们不想让列表进行下拉刷新。
2、定义刷新完成的接口,此时是需要由用户来调用的:
package org.devio.hi.ui.refresh;
/**
* HiRefresh对外的通用接口
*/
public interface HiRefresh {
/**
* 刷新时是否禁止滚动
*
* @param disableRefreshScroll 否禁止滚动
*/
void setDisableRefreshScroll(boolean disableRefreshScroll);
/**
* 刷新完成
*/
void refreshFinished();
}
3、 定义设置下拉刷新的监听:
package org.devio.hi.ui.refresh;
/**
* HiRefresh对外的通用接口
*/
public interface HiRefresh {
/**
* 刷新时是否禁止滚动
*
* @param disableRefreshScroll 否禁止滚动
*/
void setDisableRefreshScroll(boolean disableRefreshScroll);
/**
* 刷新完成
*/
void refreshFinished();
/**
* 设置下拉刷新的监听器
*
* @param hiRefreshListener 刷新的监听器
*/
void setRefreshListener(HiRefresh.HiRefreshListener hiRefreshListener);
interface HiRefreshListener {
void onRefresh();
boolean enableRefresh();
}
}
4、定义设置下拉刷新的视图:
所以接下来则来定义它。
HiOverView抽象类定义:
既然它是一个通用的下拉刷新的View,则这里让它继承一下FrameLayout,如下:
定义状态枚举:
对于下拉刷新的这个头部需要根据下拉的状态进行相应View的改变,所以这里先来对涉及到的状态进行一下定义:
定义三个成员变量:
1、触发下拉刷新,需要的最小高度:也就是下拉刷新时头部显示的最小高度。
2、由于下拉时应该是越往下越难滑,所以需要有阻尼效果,先定好阻尼的值:
进行初始化:
定义一些抽象的方法:待之后子类根据需要来实现
package org.devio.hi.ui.refresh;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.devio.hi.library.util.HiDisplayUtil;
/**
* 下拉刷新的Overlay视图,可以重载这个类来定义自己的Overlay
*/
public abstract class HiOverView extends FrameLayout {
public enum HiRefreshState {
/**
* 初始态
*/
STATE_INIT,
/**
* Header展示的状态
*/
STATE_VISIBLE,
/**
* 超出可刷新距离的状态
*/
STATE_OVER,
/**
* 刷新中的状态
*/
STATE_REFRESH,
/**
* 超出刷新位置松开手后的状态
*/
STATE_OVER_RELEASE
}
private HiRefreshState hiRefreshState = HiRefreshState.STATE_INIT;
/**
* 触发下拉刷新 需要的最小高度
**/
public int pullRefreshHeight;
/**
* 最小阻尼
**/
public float minDamp = 1.6f;
/**
* 最大阻尼
**/
public float maxDamp = 2.2f;
public HiRefreshState getHiRefreshState() {
return hiRefreshState;
}
public void setHiRefreshState(HiRefreshState hiRefreshState) {
this.hiRefreshState = hiRefreshState;
}
public HiOverView(@NonNull Context context) {
this(context, null);
}
public HiOverView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, -1);
}
public HiOverView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
preInit();
}
protected void preInit() {
pullRefreshHeight = HiDisplayUtil.dp2px(66, getResources());
init();
}
protected 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();
}
定义HiRefreshLayout容器控件:
这个是最核心的一个类,它则是一个刷新的容器类,而我们下拉的内容则是由它来进行包裹的,这里先来实现,待到时用时就知道了。
实现HiRefresh接口:
定义成员变量:
初始化:
这里首先先初始化手势监听器:
那对于上面的代码需要优化一下,避免冗余不用的回调方法,则咱们需要对这个监听包装一下既可,如下:
package org.devio.hi.ui.refresh;
import android.view.GestureDetector;
import android.view.MotionEvent;
public class HiGestureDetector implements GestureDetector.OnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}
接下来咱们则可以用它来按需重写我们所需要的方法既可:
处理触摸事件:
这块是最复杂的,下面一点点来处理。
1、处理没到达到刷新的高度就松开手指的情况:
先看一下预期的效果:
下面来处理下:
而滚动则需要用到Scroller,这里做一层包装,具体如何包装是有一些技巧的,详细的这里可以参考博主:https://juejin.im/post/5c7f4f0351882562ed516ab6,下面来实现一下:
接下来则需要再加一个对外调用的方法:
好,接下来则可以使用它来进行我们想要的滚动了,先定义一个成员变量:
2、处理达到刷新的高度就松开手指的情况:
看效果:
可以看到此时得滚动还原到刷新的那个高度,所以:
3、处理手指木有松开滑动的触摸逻辑:
此时则需要将这些事件交由给我们的手势处理器来弄,这样能避免我们自己来写各种触摸的事件了:
另外得根据是否消费了这些事件,做如下的处理:
接下来则需要来处理手势的滑动监听逻辑了:
首先做一些异常的判断:
HiGestureDetector hiGestureDetector = new HiGestureDetector() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float disX, float disY) {
if (Math.abs(disX) > Math.abs(disY) || hiRefreshListener != null && !hiRefreshListener.enableRefresh()) {
//横向滑动,或刷新被禁止则不处理
return false;
}
if (disableRefreshScroll && hiRefreshState == HiOverView.HiRefreshState.STATE_REFRESH) {
//刷新时禁止滑动
return true;
}
return false;
}
};
接下来则需要判断里面的内容区域中的列表是否发生滚动了,如果发生则咱们的下拉控件不应该处理事件,如下:
这个工具类的逻辑就不过多解释了,贴出来:
package org.devio.hi.ui.refresh;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.devio.hi.library.log.HiLog;
public class HiScrollUtil {
/**
* 判断child是否发生了滚动
*
* @param child
* @return true 发生了滚动
*/
public static boolean childScrolled(@NonNull View child) {
if (child instanceof AdapterView) {
AdapterView adapterView = (AdapterView) child;
if (adapterView.getFirstVisiblePosition() != 0
|| adapterView.getFirstVisiblePosition() == 0 && adapterView.getChildAt(0) != null
&& adapterView.getChildAt(0).getTop() < 0) {
return true;
}
} else if (child.getScrollY() > 0) {
return true;
}
if (child instanceof RecyclerView) {
RecyclerView recyclerView = (RecyclerView) child;
View view = recyclerView.getChildAt(0);
int firstPosition = recyclerView.getChildAdapterPosition(view);
HiLog.d("----:top", view.getTop() + "");
return firstPosition != 0 || view.getTop() != 0;
}
return false;
}
/**
* 查找可以滚动的child
*
* @return 可以滚动的child
*/
public static View findScrollableChild(@NonNull ViewGroup viewGroup) {
View child = viewGroup.getChildAt(1);
if (child instanceof RecyclerView || child instanceof AdapterView) {
return child;
}
if (child instanceof ViewGroup) {//往下多找一层
View tempChild = ((ViewGroup) child).getChildAt(0);
if (tempChild instanceof RecyclerView || tempChild instanceof AdapterView) {
child = tempChild;
}
}
return child;
}
}
从判断逻辑可以看出,最多列表只能是嵌2层,对于列表里面套列表再套列表的,咱们这边是判断不了的,这点需要注意!!!!
各种异常都已经判断之后,接下来则说明用户没松手是需要下拉滑动的,具体逻辑如下:
HiGestureDetector hiGestureDetector = new HiGestureDetector() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float disX, float disY) {
if (Math.abs(disX) > Math.abs(disY) || hiRefreshListener != null && !hiRefreshListener.enableRefresh()) {
//横向滑动,或刷新被禁止则不处理
return false;
}
if (disableRefreshScroll && hiRefreshState == HiOverView.HiRefreshState.STATE_REFRESH) {
//刷新时禁止滑动
return true;
}
View head = getChildAt(0);
View child = HiScrollUtil.findScrollableChild(HiRefreshLayout.this);
if (HiScrollUtil.childScrolled(child)) {
//如果列表发生了滚动则不处理
return false;
}
if ((hiRefreshState != HiOverView.HiRefreshState.STATE_REFRESH || head.getBottom() <= hiOverView.pullRefreshHeight) && (head.getBottom() > 0 || disY <= 0.0F)) {
//还在滑动中
if (hiRefreshState != HiOverView.HiRefreshState.STATE_OVER_RELEASE) {
int speed;
//阻尼计算
if (child.getTop() < hiOverView.pullRefreshHeight) {
speed = (int) (lastY / hiOverView.minDamp);
} else {
speed = (int) (lastY / hiOverView.maxDamp);
}
//如果是正在刷新状态,则不允许在滑动的时候改变状态
boolean bool = moveDown(speed, true);
lastY = (int) (-disY);
return bool;
} else {
return false;
}
} else {
return false;
}
}
};
/**
* 根据偏移量移动header与child
*
* @param offsetY 偏移量
* @param nonAuto 是否非自动滚动触发
* @return
*/
private boolean moveDown(int offsetY, boolean nonAuto) {
//TODO
return false;
}
4、集中处理moveDown()移动方法:
这块的逻辑条件比较多,需要耐心:
/**
* 根据偏移量移动header与child
*
* @param offsetY 偏移量
* @param nonAuto 是否非自动滚动触发
* @return
*/
private boolean moveDown(int offsetY, boolean nonAuto) {
View head = getChildAt(0);
View child = getChildAt(1);
int childTop = child.getTop() + offsetY;
if (childTop <= 0) {//异常情况的补充
HiLog.i(TAG, "childTop<=0,state" + hiRefreshState);
offsetY = -child.getTop();
//移动head与child的位置,到原始位置
head.offsetTopAndBottom(offsetY);
child.offsetTopAndBottom(offsetY);
if (hiRefreshState != HiOverView.HiRefreshState.STATE_REFRESH) {
hiRefreshState = HiOverView.HiRefreshState.STATE_INIT;
}
}
return false;
}
继续判断,如果已经处于刷新中了,此时则不允许再进行滚动了,如下:
private boolean moveDown(int offsetY, boolean nonAuto) {
View head = getChildAt(0);
View child = getChildAt(1);
int childTop = child.getTop() + offsetY;
if (childTop <= 0) {//异常情况的补充
HiLog.i(TAG, "childTop<=0,state" + hiRefreshState);
offsetY = -child.getTop();
//移动head与child的位置,到原始位置
head.offsetTopAndBottom(offsetY);
child.offsetTopAndBottom(offsetY);
if (hiRefreshState != HiOverView.HiRefreshState.STATE_REFRESH) {
hiRefreshState = HiOverView.HiRefreshState.STATE_INIT;
}
} else if (hiRefreshState == HiOverView.HiRefreshState.STATE_REFRESH && childTop > hiOverView.pullRefreshHeight) {
//如果正在下拉刷新中,禁止继续下拉
return false;
}
return false;
}
继续,接一个条件:
private boolean moveDown(int offsetY, boolean nonAuto) {
View head = getChildAt(0);
View child = getChildAt(1);
int childTop = child.getTop() + offsetY;
if (childTop <= 0) {//异常情况的补充
HiLog.i(TAG, "childTop<=0,state" + hiRefreshState);
offsetY = -child.getTop();
//移动head与child的位置,到原始位置
head.offsetTopAndBottom(offsetY);
child.offsetTopAndBottom(offsetY);
if (hiRefreshState != HiOverView.HiRefreshState.STATE_REFRESH) {
hiRefreshState = HiOverView.HiRefreshState.STATE_INIT;
}
} else if (hiRefreshState == HiOverView.HiRefreshState.STATE_REFRESH && childTop > hiOverView.pullRefreshHeight) {
//如果正在下拉刷新中,禁止继续下拉
return false;
} else if (childTop <= hiOverView.pullRefreshHeight) {//还没超出设定的刷新距离
if (hiOverView.getHiRefreshState() != HiOverView.HiRefreshState.STATE_VISIBLE && nonAuto) {//头部开始显示
hiOverView.onVisible();
hiOverView.setHiRefreshState(HiOverView.HiRefreshState.STATE_VISIBLE);
hiRefreshState = HiOverView.HiRefreshState.STATE_VISIBLE;
}
head.offsetTopAndBottom(offsetY);
child.offsetTopAndBottom(offsetY);
if (childTop == hiOverView.pullRefreshHeight && hiRefreshState == HiOverView.HiRefreshState.STATE_OVER_RELEASE) {
HiLog.i(TAG, "refresh,childTop:" + childTop);
refresh();
}
}
return false;
}
具体代码就不一一解释了,可以自己体会一下场景,还有最后一个条件:
private boolean moveDown(int offsetY, boolean nonAuto) {
View head = getChildAt(0);
View child = getChildAt(1);
int childTop = child.getTop() + offsetY;
if (childTop <= 0) {//异常情况的补充
HiLog.i(TAG, "childTop<=0,state" + hiRefreshState);
offsetY = -child.getTop();
//移动head与child的位置,到原始位置
head.offsetTopAndBottom(offsetY);
child.offsetTopAndBottom(offsetY);
if (hiRefreshState != HiOverView.HiRefreshState.STATE_REFRESH) {
hiRefreshState = HiOverView.HiRefreshState.STATE_INIT;
}
} else if (hiRefreshState == HiOverView.HiRefreshState.STATE_REFRESH && childTop > hiOverView.pullRefreshHeight) {
//如果正在下拉刷新中,禁止继续下拉
return false;
} else if (childTop <= hiOverView.pullRefreshHeight) {//还没超出设定的刷新距离
if (hiOverView.getHiRefreshState() != HiOverView.HiRefreshState.STATE_VISIBLE && nonAuto) {//头部开始显示
hiOverView.onVisible();
hiOverView.setHiRefreshState(HiOverView.HiRefreshState.STATE_VISIBLE);
hiRefreshState = HiOverView.HiRefreshState.STATE_VISIBLE;
}
head.offsetTopAndBottom(offsetY);
child.offsetTopAndBottom(offsetY);
if (childTop == hiOverView.pullRefreshHeight && hiRefreshState == HiOverView.HiRefreshState.STATE_OVER_RELEASE) {
HiLog.i(TAG, "refresh,childTop:" + childTop);
refresh();
}
} else {
if (hiOverView.getHiRefreshState() != HiOverView.HiRefreshState.STATE_OVER && nonAuto) {
//超出刷新位置
hiOverView.onOver();
hiOverView.setHiRefreshState(HiOverView.HiRefreshState.STATE_OVER);
}
head.offsetTopAndBottom(offsetY);
child.offsetTopAndBottom(offsetY);
}
return false;
}
最后,需要回调一下接口,如下:
最后收尾工作:
基本上核心的逻辑都处理完了,接下来则来看一下还剩哪些方法木有实现,则将其完善一下:
这个还没调用滚动方法,咱们已经封装好了,调用一下:
接下来还有这几个TODO的方法:
实现一下:
还剩两个TODO,下面最后完善一下:
最后还有一个小优化:
实现2种样式的头部:
带文本的样式:
效果:
实现:
先准备布局:
<?xml version="1.0" encoding="UTF-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/refresh_overView"
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:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:visibility="visible" />
</LinearLayout>
</merge>
其中用到了一个loading图片:
rotate_daisy.png:
接下来则来新建一个类来实现咱们的HiOverView抽象的头部视图:
其逻辑也比较简单,就不过多说明,直接贴代码了:
package org.devio.hi.ui.refresh;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
import android.widget.TextView;
import org.devio.hi.ui.R;
/**
* 普通的刷新头部视图
*/
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_text_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"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android">
<rotate
android:drawable="@drawable/rotate_daisy"
android:duration="700"
android:fromDegrees="0"
android:pivotX="50%"
android:pivotY="50%"
android:repeatCount="-1"
android:toDegrees="360" />
</rotate>
带几个小点的loading样式:
效果:
实现:
有了模板接口,实现起来也非常之简单啦,这种效果需要依赖于一个三方库,如下:
另外需要在assets中增加一个json文件:
{
"v": "4.6.8",
"fr": 29.9700012207031,
"ip": 0,
"op": 40.0000016292334,
"w": 256,
"h": 256,
"nm": "Comp 1",
"ddd": 0,
"assets": [],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "Shape Layer 3",
"ks": {
"o": {
"a": 0,
"k": 100
},
"r": {
"a": 0,
"k": 0
},
"p": {
"a": 1,
"k": [
{
"i": {
"x": 0.667,
"y": 1
},
"o": {
"x": 0.333,
"y": 0
},
"n": "0p667_1_0p333_0",
"t": 20,
"s": [
208.6,
127.969,
0
],
"e": [
208.6,
88,
0
],
"to": [
0,
-6.66145849227905,
0
],
"ti": [
0,
-0.00520833348855,
0
]
},
{
"i": {
"x": 0.667,
"y": 1
},
"o": {
"x": 0.333,
"y": 0
},
"n": "0p667_1_0p333_0",
"t": 30,
"s": [
208.6,
88,
0
],
"e": [
208.6,
128,
0
],
"to": [
0,
0.00520833348855,
0
],
"ti": [
0,
-6.66666650772095,
0
]
},
{
"t": 40.0000016292334
}
]
},
"a": {
"a": 0,
"k": [
-70,
-0.5,
0
]
},
"s": {
"a": 0,
"k": [
75,
75,
100
]
}
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"d": 1,
"ty": "el",
"s": {
"a": 0,
"k": [
33.75,
34.5
]
},
"p": {
"a": 0,
"k": [
0,
0
]
},
"nm": "Ellipse Path 1",
"mn": "ADBE Vector Shape - Ellipse"
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [
0.9843137,
0.5490196,
0,
1
]
},
"o": {
"a": 0,
"k": 100
},
"r": 1,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill"
},
{
"ty": "tr",
"p": {
"a": 0,
"k": [
-70.125,
-0.5
],
"ix": 2
},
"a": {
"a": 0,
"k": [
0,
0
],
"ix": 1
},
"s": {
"a": 0,
"k": [
100,
100
],
"ix": 3
},
"r": {
"a": 0,
"k": 0,
"ix": 6
},
"o": {
"a": 0,
"k": 100,
"ix": 7
},
"sk": {
"a": 0,
"k": 0,
"ix": 4
},
"sa": {
"a": 0,
"k": 0,
"ix": 5
},
"nm": "Transform"
}
],
"nm": "Ellipse 1",
"np": 3,
"cix": 2,
"ix": 1,
"mn": "ADBE Vector Group"
}
],
"ip": 0,
"op": 300.00001221925,
"st": 0,
"bm": 0,
"sr": 1
},
{
"ddd": 0,
"ind": 2,
"ty": 4,
"nm": "Shape Layer 2",
"ks": {
"o": {
"a": 0,
"k": 100
},
"r": {
"a": 0,
"k": 0
},
"p": {
"a": 1,
"k": [
{
"i": {
"x": 0.667,
"y": 1
},
"o": {
"x": 0.333,
"y": 0
},
"n": "0p667_1_0p333_0",
"t": 15,
"s": [
168.6,
128,
0
],
"e": [
168.6,
88,
0
],
"to": [
0,
-6.66666650772095,
0
],
"ti": [
0,
0,
0
]
},
{
"i": {
"x": 0.667,
"y": 1
},
"o": {
"x": 0.333,
"y": 0
},
"n": "0p667_1_0p333_0",
"t": 25,
"s": [
168.6,
88,
0
],
"e": [
168.6,
128,
0
],
"to": [
0,
0,
0
],
"ti": [
0,
-6.66666650772095,
0
]
},
{
"t": 35.0000014255792
}
]
},
"a": {
"a": 0,
"k": [
-70,
-0.5,
0
]
},
"s": {
"a": 0,
"k": [
75,
75,
100
]
}
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"d": 1,
"ty": "el",
"s": {
"a": 0,
"k": [
33.75,
34.5
]
},
"p": {
"a": 0,
"k": [
0,
0
]
},
"nm": "Ellipse Path 1",
"mn": "ADBE Vector Shape - Ellipse"
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [
0.9921569,
0.8470588,
0.2078431,
1
]
},
"o": {
"a": 0,
"k": 100
},
"r": 1,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill"
},
{
"ty": "tr",
"p": {
"a": 0,
"k": [
-70.125,
-0.5
],
"ix": 2
},
"a": {
"a": 0,
"k": [
0,
0
],
"ix": 1
},
"s": {
"a": 0,
"k": [
100,
100
],
"ix": 3
},
"r": {
"a": 0,
"k": 0,
"ix": 6
},
"o": {
"a": 0,
"k": 100,
"ix": 7
},
"sk": {
"a": 0,
"k": 0,
"ix": 4
},
"sa": {
"a": 0,
"k": 0,
"ix": 5
},
"nm": "Transform"
}
],
"nm": "Ellipse 1",
"np": 3,
"cix": 2,
"ix": 1,
"mn": "ADBE Vector Group"
}
],
"ip": 0,
"op": 300.00001221925,
"st": 0,
"bm": 0,
"sr": 1
},
{
"ddd": 0,
"ind": 3,
"ty": 4,
"nm": "Shape Layer 1",
"ks": {
"o": {
"a": 0,
"k": 100
},
"r": {
"a": 0,
"k": 0
},
"p": {
"a": 1,
"k": [
{
"i": {
"x": 0.667,
"y": 1
},
"o": {
"x": 0.333,
"y": 0
},
"n": "0p667_1_0p333_0",
"t": 10,
"s": [
128.594,
127.969,
0
],
"e": [
128.594,
88,
0
],
"to": [
0,
-6.66145849227905,
0
],
"ti": [
0,
-0.00520833348855,
0
]
},
{
"i": {
"x": 0.667,
"y": 1
},
"o": {
"x": 0.333,
"y": 0
},
"n": "0p667_1_0p333_0",
"t": 20,
"s": [
128.594,
88,
0
],
"e": [
128.594,
128,
0
],
"to": [
0,
0.00520833348855,
0
],
"ti": [
0,
-6.66666650772095,
0
]
},
{
"t": 30.0000012219251
}
]
},
"a": {
"a": 0,
"k": [
-70,
-0.5,
0
]
},
"s": {
"a": 0,
"k": [
75,
75,
100
]
}
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"d": 1,
"ty": "el",
"s": {
"a": 0,
"k": [
33.75,
34.5
]
},
"p": {
"a": 0,
"k": [
0,
0
]
},
"nm": "Ellipse Path 1",
"mn": "ADBE Vector Shape - Ellipse"
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [
0.2627451,
0.627451,
0.2784314,
1
]
},
"o": {
"a": 0,
"k": 100
},
"r": 1,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill"
},
{
"ty": "tr",
"p": {
"a": 0,
"k": [
-70.125,
-0.5
],
"ix": 2
},
"a": {
"a": 0,
"k": [
0,
0
],
"ix": 1
},
"s": {
"a": 0,
"k": [
100,
100
],
"ix": 3
},
"r": {
"a": 0,
"k": 0,
"ix": 6
},
"o": {
"a": 0,
"k": 100,
"ix": 7
},
"sk": {
"a": 0,
"k": 0,
"ix": 4
},
"sa": {
"a": 0,
"k": 0,
"ix": 5
},
"nm": "Transform"
}
],
"nm": "Ellipse 1",
"np": 3,
"cix": 2,
"ix": 1,
"mn": "ADBE Vector Group"
}
],
"ip": 0,
"op": 300.00001221925,
"st": 0,
"bm": 0,
"sr": 1
},
{
"ddd": 0,
"ind": 4,
"ty": 4,
"nm": "Shape Layer 4",
"ks": {
"o": {
"a": 0,
"k": 100
},
"r": {
"a": 0,
"k": 0
},
"p": {
"a": 1,
"k": [
{
"i": {
"x": 0.667,
"y": 1
},
"o": {
"x": 0.333,
"y": 0
},
"n": "0p667_1_0p333_0",
"t": 5,
"s": [
88.6,
127.969,
0
],
"e": [
88.6,
88,
0
],
"to": [
0,
-6.66145849227905,
0
],
"ti": [
0,
-0.00520833348855,
0
]
},
{
"i": {
"x": 0.667,
"y": 1
},
"o": {
"x": 0.333,
"y": 0
},
"n": "0p667_1_0p333_0",
"t": 15,
"s": [
88.6,
88,
0
],
"e": [
88.6,
128,
0
],
"to": [
0,
0.00520833348855,
0
],
"ti": [
0,
-6.66666650772095,
0
]
},
{
"t": 25.0000010182709
}
]
},
"a": {
"a": 0,
"k": [
-70,
-0.5,
0
]
},
"s": {
"a": 0,
"k": [
75,
75,
100
]
}
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"d": 1,
"ty": "el",
"s": {
"a": 0,
"k": [
33.75,
34.5
]
},
"p": {
"a": 0,
"k": [
0,
0
]
},
"nm": "Ellipse Path 1",
"mn": "ADBE Vector Shape - Ellipse"
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [
0.1176471,
0.5333334,
0.8980392,
1
]
},
"o": {
"a": 0,
"k": 100
},
"r": 1,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill"
},
{
"ty": "tr",
"p": {
"a": 0,
"k": [
-70.125,
-0.5
],
"ix": 2
},
"a": {
"a": 0,
"k": [
0,
0
],
"ix": 1
},
"s": {
"a": 0,
"k": [
100,
100
],
"ix": 3
},
"r": {
"a": 0,
"k": 0,
"ix": 6
},
"o": {
"a": 0,
"k": 100,
"ix": 7
},
"sk": {
"a": 0,
"k": 0,
"ix": 4
},
"sa": {
"a": 0,
"k": 0,
"ix": 5
},
"nm": "Transform"
}
],
"nm": "Ellipse 1",
"np": 3,
"cix": 2,
"ix": 1,
"mn": "ADBE Vector Group"
}
],
"ip": 0,
"op": 300.00001221925,
"st": 0,
"bm": 0,
"sr": 1
},
{
"ddd": 0,
"ind": 5,
"ty": 4,
"nm": "Shape Layer 5",
"ks": {
"o": {
"a": 0,
"k": 100
},
"r": {
"a": 0,
"k": 0
},
"p": {
"a": 1,
"k": [
{
"i": {
"x": 0.667,
"y": 1
},
"o": {
"x": 0.333,
"y": 0
},
"n": "0p667_1_0p333_0",
"t": 0,
"s": [
48.6,
127.969,
0
],
"e": [
48.6,
88,
0
],
"to": [
0,
-6.66145849227905,
0
],
"ti": [
0,
-0.00520833348855,
0
]
},
{
"i": {
"x": 0.667,
"y": 1
},
"o": {
"x": 0.333,
"y": 0
},
"n": "0p667_1_0p333_0",
"t": 10,
"s": [
48.6,
88,
0
],
"e": [
48.6,
128,
0
],
"to": [
0,
0.00520833348855,
0
],
"ti": [
0,
-6.66666650772095,
0
]
},
{
"t": 20.0000008146167
}
]
},
"a": {
"a": 0,
"k": [
-70,
-0.5,
0
]
},
"s": {
"a": 0,
"k": [
75,
75,
100
]
}
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"d": 1,
"ty": "el",
"s": {
"a": 0,
"k": [
33.75,
34.5
]
},
"p": {
"a": 0,
"k": [
0,
0
]
},
"nm": "Ellipse Path 1",
"mn": "ADBE Vector Shape - Ellipse"
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [
0.8980392,
0.2235294,
0.2078431,
1
]
},
"o": {
"a": 0,
"k": 100
},
"r": 1,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill"
},
{
"ty": "tr",
"p": {
"a": 0,
"k": [
-70.125,
-0.5
],
"ix": 2
},
"a": {
"a": 0,
"k": [
0,
0
],
"ix": 1
},
"s": {
"a": 0,
"k": [
100,
100
],
"ix": 3
},
"r": {
"a": 0,
"k": 0,
"ix": 6
},
"o": {
"a": 0,
"k": 100,
"ix": 7
},
"sk": {
"a": 0,
"k": 0,
"ix": 4
},
"sa": {
"a": 0,
"k": 0,
"ix": 5
},
"nm": "Transform"
}
],
"nm": "Ellipse 1",
"np": 3,
"cix": 2,
"ix": 1,
"mn": "ADBE Vector Group"
}
],
"ip": 0,
"op": 300.00001221925,
"st": 0,
"bm": 0,
"sr": 1
}
]
}
然后准备布局:
<?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>
接下来则来撸码呗,同样的套路来继承咱们封装的抽象类来实现:
package org.devio.hi.ui.refresh;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import com.airbnb.lottie.LottieAnimationView;
import org.devio.hi.ui.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.hi_refresh_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() {
// mText.setText("下拉刷新");
}
@Override
public void onOver() {
// mText.setText("松开刷新");
}
@Override
public void onRefresh() {
pullAnimationView.setSpeed(2);
pullAnimationView.playAnimation();
}
@Override
public void onFinish() {
pullAnimationView.setProgress(0f);
pullAnimationView.cancelAnimation();
}
}
看到没,有了通用封装,替换头图真的是相当的轻松。
整体测试:
好,接下来咱们来测试一下看封装的有木有问题,先来APP上增加一个测试刷新的入口:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv_tab_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:onClick="onClick"
android:text="HiTabBottom"
android:textColor="#0077cc"
android:textSize="20dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_tap_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:onClick="onClick"
android:text="HiTapTop"
android:textColor="#0077cc"
android:textSize="20dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_tab_bottom" />
<TextView
android:id="@+id/tv_hi_refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:onClick="onClick"
android:text="HiRefresh"
android:textColor="#0077cc"
android:textSize="20dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_tap_top" />
</androidx.constraintlayout.widget.ConstraintLayout>
接下来则来在DEMO中调用一下咱们封装的HiRefresh:
然后准备布局,看好了怎么用?
<?xml version="1.0" encoding="utf-8"?>
<org.devio.hi.ui.refresh.HiRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
tools:ignore="MissingDefaultResource">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center"
android:text="fdsfs" />
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center"
android:text="fdsfs" />
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center"
android:text="fdsfs" />
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center"
android:text="fdsfs" />
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center"
android:text="fdsfs" />
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center"
android:text="fdsfs" />
<TextView
android:layout_width="match_parent"
android:layout_height="200dp"
android:gravity="center"
android:text="fdsfs" />
</LinearLayout>
</ScrollView>
</org.devio.hi.ui.refresh.HiRefreshLayout>
也就是我们在布局中声明的是HiRefreshLayout,然后在它里面则可以定义我们想要的内容,这里咱们用能滑动的列表,最典型的用法,接下来则来看一下代码:
package org.devio.hi.ui.app.demo.refresh
import android.os.Bundle
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import org.devio.hi.ui.app.R
import org.devio.hi.ui.refresh.HiLottieOverView
import org.devio.hi.ui.refresh.HiRefresh
import org.devio.hi.ui.refresh.HiRefreshLayout
import org.devio.hi.ui.refresh.HiTextOverView
class HiRefreshDemoActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_hi_refresh)
val refreshLayout = findViewById<HiRefreshLayout>(R.id.refresh_layout)
val xOverView = HiTextOverView(this)
val lottieOverView = HiLottieOverView(this)
refreshLayout.setRefreshOverView(xOverView)
refreshLayout.setRefreshListener(object :
HiRefresh.HiRefreshListener {
override fun onRefresh() {
Handler().postDelayed({ refreshLayout.refreshFinished() }, 1000)
}
override fun enableRefresh(): Boolean {
return true
}
})
refreshLayout.setDisableRefreshScroll(false)
}
}
运行看一下:
呃发现咱们的头部咋跑到了最底部了呢?此时则需要重写onLayout()来对子视图进行重新的摆放了,如下:
再运行:,发现报错了。。这是啥原因呢?原来是咱们的HiLog木有进行配置:
所以咱们在Application中配置一下:
package org.devio.hi.ui.app
import android.app.Application
import com.alibaba.fastjson.JSONObject
import org.devio.hi.library.log.HiConsolePrinter
import org.devio.hi.library.log.HiFilePrinter
import org.devio.hi.library.log.HiLogConfig
import org.devio.hi.library.log.HiLogConfig.JsonParser
import org.devio.hi.library.log.HiLogManager
class MApplication : Application() {
override fun onCreate() {
super.onCreate()
HiLogManager.init(
object : HiLogConfig() {
override fun injectJsonParser(): JsonParser? {
return JsonParser { src -> JSONObject.toJSONString(src) }
}
override fun getGlobalTag(): String {
return "MApplication"
}
override fun enable(): Boolean {
return true
}
override fun includeThread(): Boolean {
return true
}
override fun stackTraceDepth(): Int {
return 5
}
},
HiConsolePrinter(),
HiFilePrinter.getInstance(applicationContext.cacheDir.absolutePath, 0)
)
}
}
此时再来运行,一切完美,看一下:
那换一个头部效果呢?相当的简单:
再运行:
最后再来将我们的内容替换成RecycleView:
<?xml version="1.0" encoding="utf-8"?>
<org.devio.hi.ui.refresh.HiRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycleview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- <ScrollView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content">-->
<!-- <LinearLayout-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:orientation="vertical">-->
<!-- <TextView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="200dp"-->
<!-- android:text="fdsfs" />-->
<!-- <TextView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="200dp"-->
<!-- android:text="fdsfs" />-->
<!-- <TextView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="200dp"-->
<!-- android:text="fdsfs" />-->
<!-- <TextView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="200dp"-->
<!-- android:text="fdsfs" />-->
<!-- <TextView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="200dp"-->
<!-- android:text="fdsfs" />-->
<!-- <TextView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="200dp"-->
<!-- android:text="fdsfs" />-->
<!-- <TextView-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="200dp"-->
<!-- android:text="fdsfs" />-->
<!-- </LinearLayout>-->
<!-- </ScrollView>-->
</org.devio.hi.ui.refresh.HiRefreshLayout>
package org.devio.hi.ui.app.demo.refresh
import android.os.Bundle
import android.os.Handler
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import org.devio.hi.library.log.HiLog
import org.devio.hi.ui.app.R
import org.devio.hi.ui.refresh.HiLottieOverView
import org.devio.hi.ui.refresh.HiRefresh
import org.devio.hi.ui.refresh.HiRefreshLayout
import org.devio.hi.ui.refresh.HiTextOverView
class HiRefreshDemoActivity : AppCompatActivity() {
private var recyclerView: RecyclerView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_hi_refresh)
val refreshLayout = findViewById<HiRefreshLayout>(R.id.refresh_layout)
val xOverView = HiTextOverView(this)
val lottieOverView =
HiLottieOverView(this)
refreshLayout.setRefreshOverView(lottieOverView)
refreshLayout.setRefreshListener(object :
HiRefresh.HiRefreshListener {
override fun onRefresh() {
Handler().postDelayed({ refreshLayout.refreshFinished() }, 1000)
}
override fun enableRefresh(): Boolean {
return true
}
})
refreshLayout.setDisableRefreshScroll(false)
initRecycleView()
}
var myDataset =
arrayOf(
"HiRefresh",
"HiRefresh",
"HiRefresh",
"HiRefresh",
"HiRefresh",
"HiRefresh",
"HiRefresh"
)
private fun initRecycleView() {
recyclerView = findViewById<View>(R.id.recycleview) as RecyclerView
recyclerView!!.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(this)
recyclerView!!.setLayoutManager(layoutManager)
val mAdapter =
MyAdapter(
myDataset
)
recyclerView!!.setAdapter(mAdapter)
}
class MyAdapter
(private val mDataset: Array<String>) :
RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
class MyViewHolder(v: View) : ViewHolder(v) {
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 { HiLog.d("position:$position") }
}
override fun getItemCount(): Int {
return mDataset.size
}
}
}
其中item为:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_marginBottom="10dp"
android:background="#098"
android:gravity="center"
tools:ignore="MissingDefaultResource">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="20dp">
</TextView>
</LinearLayout>
此时运行的效果就如开篇所示了,至此整个HiRefresh就封装完毕了~~