需求分析:

设计一个下拉刷新组件,能够提供通用的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 {@Overridepublic boolean onDown(MotionEvent e) {return false;}@Overridepublic void onShowPress(MotionEvent e) {}@Overridepublic boolean onSingleTapUp(MotionEvent e) {return false;}@Overridepublic boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {return false;}@Overridepublic void onLongPress(MotionEvent e) {}@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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) {//TODOreturn 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"><LinearLayoutandroid:id="@+id/refresh_area"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_gravity="bottom"android:gravity="center"android:orientation="vertical"><ImageViewandroid: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" /><TextViewandroid: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);}@Overridepublic 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);}@Overrideprotected void onScroll(int scrollY, int pullRefreshHeight) {}@Overridepublic void onVisible() {mText.setText("下拉刷新");}@Overridepublic void onOver() {mText.setText("松开刷新");}@Overridepublic void onRefresh() {mText.setText("正在刷新...");Animation operatingAnim = AnimationUtils.loadAnimation(getContext(), R.anim.rotate_anim);LinearInterpolator lin = new LinearInterpolator();operatingAnim.setInterpolator(lin);mRotateView.startAnimation(operatingAnim);}@Overridepublic void onFinish() {mRotateView.clearAnimation();}}

由于咱们对其行为进行了抽象,所以具体类的实现则根据自己的业务需求来相应的实现既可,这就是通用封装带来的好处,上面还用到了一个动画资源:

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"><rotateandroid: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"><LinearLayoutandroid: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.LottieAnimationViewandroid: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);}@Overridepublic 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");}@Overrideprotected void onScroll(int scrollY, int pullRefreshHeight) {}@Overridepublic void onVisible() {//        mText.setText("下拉刷新");}@Overridepublic void onOver() {//        mText.setText("松开刷新");}@Overridepublic void onRefresh() {pullAnimationView.setSpeed(2);pullAnimationView.playAnimation();}@Overridepublic 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"><TextViewandroid: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" /><TextViewandroid: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" /><TextViewandroid: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"><ScrollViewandroid:layout_width="match_parent"android:layout_height="wrap_content"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:layout_width="match_parent"android:layout_height="200dp"android:gravity="center"android:text="fdsfs" /><TextViewandroid:layout_width="match_parent"android:layout_height="200dp"android:gravity="center"android:text="fdsfs" /><TextViewandroid:layout_width="match_parent"android:layout_height="200dp"android:gravity="center"android:text="fdsfs" /><TextViewandroid:layout_width="match_parent"android:layout_height="200dp"android:gravity="center"android:text="fdsfs" /><TextViewandroid:layout_width="match_parent"android:layout_height="200dp"android:gravity="center"android:text="fdsfs" /><TextViewandroid:layout_width="match_parent"android:layout_height="200dp"android:gravity="center"android:text="fdsfs" /><TextViewandroid: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.refreshimport 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.HiTextOverViewclass 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.appimport 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.HiLogManagerclass 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.RecyclerViewandroid: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.refreshimport 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.HiTextOverViewclass HiRefreshDemoActivity : AppCompatActivity() {private var recyclerView: RecyclerView? = nulloverride 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 RecyclerViewrecyclerView!!.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: TextViewinit {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"><TextViewandroid: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就封装完毕了~~

Kotlin x Java打造 UI 通用组件三-------高级UI组件定制与解耦设计之HiRefresh相关推荐

  1. html触摸电脑ui系统,如何用高级UI制作触摸系统界面

    一.什么是触摸系统 触摸系统实际上是一个令玩家与作品中的角色产生互动的环节.顾名思义,通过模拟触摸这个行为,来令作品中的角色产生不同的反应.说出不同的台词.设计巧妙的触摸系统,一方面可以增添作品的可玩 ...

  2. 【Flutter】监听滚动动作 控制组件 透明度渐变 ( 移除顶部状态栏空白 | 帧布局组件 | 透明度组件 | 监听滚动组件 )

    文章目录 前言 一.移除顶部状态栏空白 二.帧布局组件 三.透明度组件 四.监听滚动事件 五.完整代码示例 六.相关资源 前言 在上一篇博客 [Flutter]Banner 轮播组件 ( flutte ...

  3. react 子传参父_React 子组件给父组件传值、整个组件、方法

    一.准备工作 1.定义一个父组件,名字为Parent /src/component/Parent.js import React, {Component} from 'react' export de ...

  4. Android学习——UI高级组件三

    Android学习--UI高级组件三 PopupWindow(弹出式窗口) Android的对话框有两种:PopupWindow和AlertDialog.它们的不同点在于:AlertDialog位置固 ...

  5. 用vulkan写个引擎 (三)ui组件

    ui组件也许是最令人着迷的部分.这部分最复杂的要数文字排版系统,比较重要的部分分别是属性集部件,信号槽部件,布局系统.相对复杂的部分是图片顶点数据拼装,和系统事件处理. 仓库:https://bitb ...

  6. java jtextfield 密码_Java Swing实战(三)文本组件JTextField和密码组件JPasswordField

    接下来添加文本组件JTextField和密码组件JPasswordField. /** * @author: lishuai * @date: 2018/11/26 13:51 */ public c ...

  7. 打造工业级推荐系统(三):推荐系统的工程实现与架构优化

    打造工业级推荐系统(三):推荐系统的工程实现与架构优化 gongyouliu 阅读数:4148 2019 年 4 月 26 日 导读:个性化推荐系统,简单来说就是根据每个人的偏好推荐他喜欢的物品.互联 ...

  8. Android八大模块进阶学习笔记(性能优化、百大框架、高级UI、Flutter、Kotlin...)

    今年来,Android开发行业的就业形势愈加严峻,无论刚刚入门Android学习没有头绪的.还是开发多年想要突破薪资范畴的,都需要跳出编码和业务的局限,学会选型.扩展, 提升编程思维,建立良好的职业规 ...

  9. 全新的 React 组件设计理念 Headless UI

    其实,最早接触 Headless UI 是在去年,碰巧看到了一个非常前沿且优秀的组件库 ---- Chakra UI,这个组件库本身就是 Headless UI 的实践者,同时也是 CSS-IN-JS ...

最新文章

  1. 企业网络推广中关键词“出镜率”高会影响企业网络推广吗?
  2. bsc是指什么_掌握BSC,实现企业数字化管理
  3. 徐坤用话剧震了我们一道
  4. Python 位操作运算符
  5. 视频显示边缘空白的真相
  6. android 个推打开页面,个推android客户端点击跳到指定activity
  7. https及核心SSL
  8. 虚拟机VM10装Mac OS X 10.9.3
  9. c++遍历文件夹下的文件_算法面试|开发者必备|使用递归函数进行无限分类及文件夹遍历...
  10. 华为又遭重击:谷歌暂停提供 Android 支持,新出售手机不能使用 Google 服务
  11. [MapReduce_8] MapReduce 中的自定义分区实现
  12. FFmpegFFplay常用命令汇总
  13. PyQt5 clicked和clicked[bool]信号区别/setCheckable()的应用
  14. 经方的魅力第二版》读书摘录
  15. python opencv 将白色底变成透明底
  16. for循环判定质数合数
  17. Win11下载速度慢怎么办?Win11下载速度慢的解决方法
  18. 大二上学期总结与感想
  19. 微信早安,利用uniCloud阿里云的云函数实现定时推送
  20. php开源记账,php记账

热门文章

  1. 【侯捷于华科演讲】对侯老师演讲的在思考
  2. 君子博学而日参省乎己
  3. JS常用正则表达式及其语法
  4. Java酒店订房管理系统
  5. 计算机中除法的函数英文,EXECL中哪一个英文是算除法的?-excle 除法的英文
  6. ROS激光雷达数据过滤
  7. IT网络赚钱-网赚项目-创业项目-0成本月赚几千实操攻略
  8. python3ddos攻击_DDOS攻击
  9. sting stingbuffer 区别 总结
  10. 也就是说,Haskell中的monad还是遵守范畴 论的定义,只是应用到计算 机当中罢了?...