Skip to content

Instantly share code, notes, and snippets.

@pythoncat1024
Last active August 27, 2019 05:55
Show Gist options
  • Save pythoncat1024/0d122e952f8922269669a224efa0b63d to your computer and use it in GitHub Desktop.
Save pythoncat1024/0d122e952f8922269669a224efa0b63d to your computer and use it in GitHub Desktop.
给RecyclerView的下拉刷新,上拉加载控件 https://juejin.im/post/5d6224bff265da03ce39e34d
<com.python.cat.mvvm.widgets.NestedRefreshLayout
android:id="@+id/nested_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/light_gray3"
android:orientation="vertical"
app:footTextColor="@color/colorPrimary"
app:headTextColor="@color/colorAccent"
app:holdupHeadDuration="500"
app:loadDoneText="@string/do_load_done"
app:loadInitText="@string/refresh_footer"
app:loadStartText="@string/drop_to_load"
app:loadingText="@string/do_load_now"
app:refreshDoneText="@string/do_refresh_done"
app:refreshDrawable="@drawable/ic_replay_black_24dp"
app:refreshInitText="@string/refresh_header"
app:refreshStartText="@string/drop_to_refresh"
app:refreshingText="@string/do_refresh_now"
app:springBackDuration="500"
app:loadMoreDrawable="@drawable/ic_replay_black_24dp"
app:visibleGone="@{!isLoading}">
<LinearLayout
android:id="@+id/refresh_header"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginTop="-100dp"
android:contentDescription="@string/refresh_header"
android:orientation="vertical">
<ImageView
android:id="@+id/header_refresh_img"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_gravity="center_horizontal"
android:contentDescription="@string/refresh_header"
android:gravity="center"
android:src="@drawable/ic_replay_black_24dp"
android:textColor="@color/white" />
<TextView
android:id="@+id/header_refresh_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/refresh_header"
android:textColor="@color/normal_color" />
</LinearLayout>
<!--
这里不能将其设置成 match_parent , 导致 footer 不能被测量到
可以直接写成 0dp, 反正真正的测量是在 parent#onMeasure 完成的
-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/articles_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<LinearLayout
android:id="@+id/refresh_footer"
android:layout_width="match_parent"
android:layout_height="100dp"
android:contentDescription="@string/refresh_header"
android:orientation="vertical">
<TextView
android:id="@+id/footer_refresh_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:gravity="center"
android:text="@string/refresh_footer"
android:textColor="@color/normal_color" />
<ImageView
android:id="@+id/footer_refresh_img"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:contentDescription="@string/refresh_header"
android:gravity="center"
android:src="@drawable/refresh_progress" />
</LinearLayout>
</com.python.cat.mvvm.widgets.NestedRefreshLayout>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="NestedRefreshLayout">
<!-- 加载中显示的图片 -->
<attr name="refreshDrawable" format="reference" />
<!--下拉刷新-->
<attr name="refreshInitText" format="reference" />
<!-- 释放刷新 -->
<attr name="refreshStartText" format="reference" />
<!-- 刷新中 -->
<attr name="refreshingText" format="reference" />
<!-- 刷新完成 -->
<attr name="refreshDoneText" format="reference" />
<!-- 隐藏 刷新头图片的时间 -->
<attr name="holdupHeadDuration" format="integer" />
<!-- 刷新完成,隐藏刷新头的时间 -->
<attr name="springBackDuration" format="integer" />
<attr name="headTextColor" format="color" />
<attr name="footTextColor" format="color" />
<attr name="loadInitText" format="reference" />
<attr name="loadStartText" format="reference" />
<attr name="loadingText" format="reference" />
<attr name="loadDoneText" format="reference" />
<attr name="loadMoreDrawable" format="reference" />
</declare-styleable>
</resources>
package com.python.cat.mvvm.widgets;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Build;
import android.util.AttributeSet;
import android.view.HapticFeedbackConstants;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Scroller;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.NestedScrollingParent2;
import androidx.core.view.ViewCompat;
import com.apkfuns.logutils.LogUtils;
import com.python.cat.mvvm.R;
import java.util.Arrays;
/**
* <a href="https://blog.csdn.net/qq_42944793/article/details/88417127">嵌套滑动的惯性滑动</a>
* <a href="https://www.jianshu.com/p/5966d1b2d1ce">Android 嵌套滑动 </a>
* <a href="https://blog.csdn.net/lmj623565791/article/details/52204039">Android 嵌套滑动 hongyang</a>
* <p>
* (子)startNestedScroll → (父)onStartNestedScroll→ (父)onNestedScrollAccepted
* → (子)dispatchNestedPreScroll → (父)onNestedPreScroll
* → (子)dispatchNestedScroll→ (父)onNestedScroll
* → (子)dispatchNestedPreFling → (父)onNestedPreFling
* → (子)dispatchNestedFling → (父)stopNestedScroll
*/
public class NestedRefreshLayout extends LinearLayout implements NestedScrollingParent2 {
public static final int DEF_ANIMATOR_DURATION = 500;
public static final int DEF_TEXT_COLOR = Color.BLACK;
private Scroller mScroller;
private boolean hasRefreshFeedback;
private boolean hasLoadMoreFeedback;
// attrs defined in xml
private int mRefreshDrawable;
private int mRefreshInitText;
private int mRefreshStartText;
private int mRefreshingText;
private int mRefreshDoneText;
private int mRefreshHoldDuration;
private int mRefreshBackDuration;
// field for refresh and load
private boolean mRefreshDone;
private ObjectAnimator mRefreshRotate;
private ObjectAnimator mRefreshHide;
private int mRefreshTextColor;
private int mLoadMoreTextColor;
private int mLoadInitText;
private int mLoadStartText;
private int mLoadingText;
private int mLoadDoneText;
private ObjectAnimator mLoadRotate;
private ObjectAnimator mLoadHide;
private int mLoadMoreDrawable;
private boolean mLoadMoreDone;
/**
* onStopNestedScroll 里面正在执行刷新动画/加载动画/回弹 这些操作 true; 否则 false
*/
private volatile boolean autoScroll;
public NestedRefreshLayout(Context context) {
this(context, null);
}
public NestedRefreshLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedRefreshLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
init(context, attrs, defStyleAttr, 0);
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public NestedRefreshLayout(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr, defStyleRes);
}
private void init(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
mScroller = new Scroller(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.NestedRefreshLayout, defStyleAttr, defStyleRes);
mRefreshDrawable = a.getResourceId(R.styleable.NestedRefreshLayout_refreshDrawable, R.drawable.ic_launcher_background);
mRefreshInitText = a.getResourceId(R.styleable.NestedRefreshLayout_refreshInitText, R.string.app_name);
mRefreshStartText = a.getResourceId(R.styleable.NestedRefreshLayout_refreshStartText, R.string.app_name);
mRefreshingText = a.getResourceId(R.styleable.NestedRefreshLayout_refreshingText, R.string.app_name);
mRefreshDoneText = a.getResourceId(R.styleable.NestedRefreshLayout_refreshDoneText, R.string.app_name);
mRefreshHoldDuration = a.getInt(R.styleable.NestedRefreshLayout_holdupHeadDuration, DEF_ANIMATOR_DURATION);
mRefreshBackDuration = a.getInt(R.styleable.NestedRefreshLayout_springBackDuration, DEF_ANIMATOR_DURATION);
mRefreshTextColor = a.getColor(R.styleable.NestedRefreshLayout_headTextColor, DEF_TEXT_COLOR);
mLoadMoreTextColor = a.getColor(R.styleable.NestedRefreshLayout_footTextColor, DEF_TEXT_COLOR);
mLoadInitText = a.getResourceId(R.styleable.NestedRefreshLayout_loadInitText, R.string.app_name);
mLoadStartText = a.getResourceId(R.styleable.NestedRefreshLayout_loadStartText, R.string.app_name);
mLoadingText = a.getResourceId(R.styleable.NestedRefreshLayout_loadingText, R.string.app_name);
mLoadDoneText = a.getResourceId(R.styleable.NestedRefreshLayout_loadDoneText, R.string.app_name);
mLoadMoreDrawable = a.getResourceId(R.styleable.NestedRefreshLayout_loadMoreDrawable, R.drawable.ic_launcher_background);
a.recycle();
showAttrsInfo();
}
private void showAttrsInfo() {
String string = new StringBuilder()
.append("\nmRefreshDrawable:").append(mRefreshDrawable)
.append("\nmRefreshInitText:").append(getResources().getString(mRefreshInitText))
.append("\nmRefreshStartText:").append(getResources().getString(mRefreshStartText))
.append("\nmRefreshingText:").append(getResources().getString(mRefreshingText))
.append("\nmRefreshDoneText:").append(getResources().getString(mRefreshDoneText))
.append("\nmRefreshDoneText:").append(getResources().getString(mRefreshDoneText))
.append("\nmRefreshHoldDuration:").append(mRefreshHoldDuration)
.append("\nmRefreshBackDuration:").append(mRefreshBackDuration)
.append("\nmRefreshBackDuration:").append(mRefreshBackDuration)
.append("\nmRefreshTextColor:").append(mRefreshTextColor)
.append("\nmLoadMoreTextColor:").append(mLoadMoreTextColor)
.append("\nmLoadInitText:").append(getResources().getString(mLoadInitText))
.append("\nmLoadStartText:").append(getResources().getString(mLoadStartText))
.append("\nmLoadingText:").append(getResources().getString(mLoadingText))
.append("\nmLoadDoneText:").append(getResources().getString(mLoadDoneText))
.append("\nmLoadMoreDrawable:").append(mLoadMoreDrawable)
.toString();
LogUtils.e("attrsInfo:%s", string);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int hs = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY);
int ws = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
View header = getChildAt(0);
View recyclerV = getChildAt(1);
View footer = getChildAt(2);
int selfHeight = getMeasuredHeight() + footer.getMeasuredHeight();
setMeasuredDimension(getMeasuredWidth(),
selfHeight);
LogUtils.e("measure: %s,%s,%s,%s", getMeasuredHeight(),
header.getMeasuredHeight(), recyclerV.getMeasuredHeight(), footer.getMeasuredHeight());
ViewGroup.LayoutParams lp1 = header.getLayoutParams();
ViewGroup.LayoutParams lp2 = recyclerV.getLayoutParams();
ViewGroup.LayoutParams lp3 = footer.getLayoutParams();
// measureChild(recyclerV, ws, hs);
lp2.height = MeasureSpec.getSize(hs);
measureChildWithMargins(recyclerV, ws, 0, MeasureSpec.makeMeasureSpec(selfHeight, MeasureSpec.EXACTLY), 0);
LogUtils.e("measure lp: %s,%s,%s,%s", getLayoutParams().height, lp1.height, lp2.height, lp3.height);
LogUtils.w("measure: %s,%s,%s,%s", getMeasuredHeight(),
header.getMeasuredHeight(), recyclerV.getMeasuredHeight(), footer.getMeasuredHeight());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
LogUtils.e("self size=(%s,%s)", w, h);
initAnimator();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (getScrollY() == 0) {
// 怎么判断已经恢复了? sy==0 就是最好的判断!
autoScroll = false;
}
// 这里不能写成 autoScroll = sy==0; 这会导致 autoScroll 经常改变
if (autoScroll && ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
// 当前正在刷新,加载,回弹
LogUtils.e("当前正在刷新,加载,回弹: sy=%s", getScrollY());
return false;
}
return super.dispatchTouchEvent(ev);
}
private void initAnimator() {
View head = getChildAt(0);
View foot = getChildAt(getChildCount() - 1);
View headerImg = head.findViewById(R.id.header_refresh_img);
// 下拉旋转动画
mRefreshRotate = ObjectAnimator
.ofFloat(headerImg, "rotation", 0, 360);
mRefreshRotate.setDuration(400);
PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1f, 0.01f);
PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1f, 0.01f);
PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1, 0.01f);
// 下拉加载完成的隐藏动画
mRefreshHide = ObjectAnimator.ofPropertyValuesHolder(headerImg, scaleX, scaleY, alpha);
mRefreshHide.setDuration(mRefreshHoldDuration);
// 上拉加载更多的旋转动画
View footerImg = foot.findViewById(R.id.footer_refresh_img);
TextView loadMoreTv = foot.findViewById(R.id.footer_refresh_tv);
mLoadRotate = ObjectAnimator
.ofFloat(footerImg, "rotation", 0, 360);
mLoadRotate.setDuration(400);
mLoadHide = ObjectAnimator.ofPropertyValuesHolder(footerImg, scaleX, scaleY, alpha);
mLoadHide.setDuration(mRefreshHoldDuration);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(0, mScroller.getCurrY());
invalidate();
if (!awakenScrollBars()) {
// Keep on drawing until the animation has finished.
postInvalidateOnAnimation();
}
}
super.computeScroll();
}
@Override
public void scrollTo(int x, int y) {
// 下滑 y 慢慢减小;上滑 y 慢慢变大
// y == 0; 回到起始位置
int rvHeight = getChildAt(1).getHeight();
if (y < -rvHeight) {
// 下拉有个极限
y = -rvHeight;
}
// 让其可以一直下拉
if (y > rvHeight) {
// 自己滑动最多让 footer 完全显示
// 因为 recyclerV 的高度是自己默认高度
y = rvHeight;
}
if (y != getScrollY()) {
super.scrollTo(x, y);
}
// 重写 scrollTo 之后,不让其滑动超出边界
}
/**
* onStartNestedScroll : 对应startNestedScroll,
* 内控件通过调用外控件的这个方法来确定外控件是否接收滑动信息.
*/
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target,
int axes, int type) {
LogUtils.i("onStartNestedScroll:" + child + "," + target + "," + axes + "," + type);
return (axes == ViewCompat.SCROLL_AXIS_VERTICAL);
// && type == ViewCompat.TYPE_TOUCH;
// 不能只处理 TYPE_TOUCH,惯性滑动隐藏头尾也得去处理,否则显示不友好
}
/**
* onNestedScrollAccepted : 当外控件确定接收滑动信息后该方法被回调,
* 可以让外控件针对嵌套滑动做一些前期工作.
*/
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target,
int axes, int type) {
LogUtils.i("onNestedScrollAccepted:"
+ child + "," + target + "," + axes + ", # " + type);
resetHeaderState();
resetFooterState();
clearAllAnimator();
}
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
LogUtils.i("onStopNestedScroll: %s,%s, sy=%s", target, type, getScrollY());
if (type != ViewCompat.TYPE_TOUCH) {
// 每次 fling 操作会走这里
LogUtils.d("这是一次 fling 结束的回调,不管");
return;
}
View head = getChildAt(0);
View foot = getChildAt(getChildCount() - 1);
if (getScrollY() <= -head.getHeight()) { // 头布局完全显示才去刷新
autoScroll = true;
hasRefreshFeedback = false;
mRefreshDone = false;
LogUtils.e("释放刷新。。。。sy=%s", getScrollY());
TextView tvRefresh = head.findViewById(R.id.header_refresh_tv);
// if (1 == 1) {
// smooth2Normal(true);
// return;
// }
// 说明当前是刷新中
mRefreshRotate.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
tvRefresh.setText(mRefreshingText);
if (mOnRefreshListener != null) {
LogUtils.i("触发下拉刷新条件,开始刷新");
mOnRefreshListener.onRefresh();
}
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
LogUtils.e("外部调用setRefreshDone触发这里回调");
mRefreshRotate.removeListener(this);
mRefreshRotate.setRepeatCount(0);
tvRefresh.setText(mRefreshDoneText);
mRefreshHide.start();
}
});
mRefreshRotate.setRepeatCount(ValueAnimator.INFINITE);
// 实际上不知道会刷新多久, 调用者决定
mRefreshRotate.start();
mRefreshHide.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mRefreshHide.removeListener(this);
smooth2Normal();
}
});
} else if (getScrollY() > foot.getHeight()) {
autoScroll = true;
hasLoadMoreFeedback = false;
mLoadMoreDone = false;
LogUtils.e("释放加载。。。。");
TextView loadMoreTv = foot.findViewById(R.id.footer_refresh_tv);
// if (1 == 1) {
// smooth2Normal(true);
// return;
// }
mLoadRotate.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
loadMoreTv.setText(mLoadingText);
if (mOnLoadMoreListener != null) {
LogUtils.i("触发上拉加载条件,开始加载");
mOnLoadMoreListener.onLoadMore();
}
}
@Override
public void onAnimationEnd(Animator animation) {
LogUtils.e("外部调用 setLoadMoreDone 触发这里回调");
mLoadRotate.removeListener(this);
loadMoreTv.setText(mLoadDoneText);
mLoadHide.start();
}
});
mLoadRotate.setRepeatCount(ValueAnimator.INFINITE); // 实际上不知道会加载多久
mLoadRotate.start();
mLoadHide.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mLoadHide.removeListener(this);
smooth2Normal();
}
});
} else if (getScrollY() != 0) {
autoScroll = true;
// 前提是当前不是正常的状态
// 无论是哪种情况,肯定要让其恢复到正常显示状态
smooth2Normal(); // 不改变周期时长
LogUtils.e("我不知道你是什么状态,但是你得正常!sy=%s", getScrollY());
}
}
private void clearAllAnimator() {
clearInnerAnimator(mRefreshRotate);
clearInnerAnimator(mRefreshHide);
clearInnerAnimator(mLoadRotate);
clearInnerAnimator(mLoadHide);
}
private void clearInnerAnimator(ObjectAnimator animator) {
if (animator != null) {
LogUtils.e("before: %s,%s,%s", animator.isStarted(), animator.isRunning(),
animator.isPaused());
animator.removeAllListeners();
animator.setRepeatCount(0);
animator.cancel();
LogUtils.e("after: %s,%s,%s", animator.isStarted(), animator.isRunning(),
animator.isPaused());
}
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
LogUtils.i("onNestedScroll: %s, (%s,%s), %s,%s # %s",
target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
// dy < 0 ,表示手指下滑;>0 上滑
boolean fetchTargetTop = !target.canScrollVertically(-1); //
boolean fetchTargetBottom = !target.canScrollVertically(1);
LogUtils.e("fetchTargetTop=%s ### fetchTargetBottom:%s", fetchTargetTop, fetchTargetBottom);
int firstHeight = getChildAt(0).getHeight();
int lastH = getChildAt(2).getHeight(); // footer height
boolean showTop = fetchTargetTop && dyUnconsumed < 0 && getScrollY() <= 0
&& type == ViewCompat.TYPE_TOUCH;
boolean showBottom = fetchTargetBottom && dyUnconsumed > 0 && getScrollY() >= 0
&& type == ViewCompat.TYPE_TOUCH;
LogUtils.w("dy=%s, sy=%s, fh=%s, fh+lh=%s ,type=%s", dyUnconsumed, getScrollY(), firstHeight, firstHeight + lastH, type);
// 加上一个 =0,包含边界情况
boolean moreHead = showTop && getScrollY() <= -firstHeight;
boolean moreBottom = showBottom && getScrollY() >= lastH;
LogUtils.w("showTop=%s,showB=%s", showTop, showBottom);
LogUtils.w("more bottom %s ; more head %s", moreBottom, moreHead);
TextView tvRefresh = getChildAt(0).findViewById(R.id.header_refresh_tv);
if (getScrollY() <= -firstHeight) {
// 头完全显示了
tvRefresh.setText(mRefreshStartText);
} else {
// 头不完全显示,或者直接看不见
tvRefresh.setText(mRefreshInitText);
}
View lastV = getChildAt(getChildCount() - 1);
TextView loadMreTv = lastV.findViewById(R.id.footer_refresh_tv);
if (getScrollY() >= lastH) {
loadMreTv.setText(mLoadStartText);
} else {
loadMreTv.setText(mLoadInitText);
}
if (moreHead) {
if (!hasRefreshFeedback) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
hasRefreshFeedback = true;
}
scrollBy(0, dyUnconsumed);
} else if (showTop) {
// dy < 0 ,表示手指下滑;>0 上滑
LogUtils.e("scrollBy zz : %s", dyUnconsumed);
scrollBy(0, dyUnconsumed);
} else if (moreBottom) {
if (!hasLoadMoreFeedback) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS,
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
hasLoadMoreFeedback = true;
}
LogUtils.e("more bottom %s", dyUnconsumed);
scrollBy(0, dyUnconsumed);
} else if (showBottom) {
LogUtils.e("scrollBy zz : %s", dyUnconsumed);
// dy < 0 ,表示手指下滑;>0 上滑
LogUtils.e("scrollBy zz : %s", dyUnconsumed);
scrollBy(0, dyUnconsumed);
}
}
/**
* onNestedPreScroll : 关键方法, 接收内控件处理滑动前的滑动距离信息,
* 在这里外控件可以优先响应滑动操作, 消耗部分或者全部滑动距离.
*/
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy,
@NonNull int[] consumed, int type) {
// 这里没有区分,表示惯性滑动与非惯性滑动,都是一样处理嵌套滑动逻辑
LogUtils.i("onNestedPreScroll: %s, (%s,%s), %s # %s",
target, dx, dy, Arrays.toString(consumed), type);
// dy < 0 ,表示手指下滑;>0 上滑
boolean fetchTargetTop = !target.canScrollVertically(-1); //
boolean fetchTargetBottom = !target.canScrollVertically(1);
LogUtils.e("fetchTargetTop=%s ### fetchTargetBottom:%s", fetchTargetTop, fetchTargetBottom);
int firstHeight = getChildAt(0).getHeight();
int lastH = getChildAt(2).getHeight(); // footer height
boolean hideTop = dy > 0 && getScrollY() < 0;
LogUtils.w("dy=%s, sy=%s, fh=%s, fh+lh=%s ,type=%s", dy, getScrollY(), firstHeight, firstHeight + lastH, type);
boolean hideBottom = dy < 0 && getScrollY() > 0;
LogUtils.w(",hideTop=%s,,hideB=%s", hideTop, hideBottom);
TextView tvRefresh = getChildAt(0).findViewById(R.id.header_refresh_tv);
if (getScrollY() <= -firstHeight) {
// 头完全显示了
tvRefresh.setText(mRefreshStartText);
} else {
// 头不完全显示,或者直接看不见
tvRefresh.setText(mRefreshInitText);
}
View lastV = getChildAt(getChildCount() - 1);
TextView loadMreTv = lastV.findViewById(R.id.footer_refresh_tv);
if (getScrollY() >= lastH) {
loadMreTv.setText(mLoadStartText);
} else {
loadMreTv.setText(mLoadInitText);
}
if (hideTop) {
// 要调整一下
// dy < 0 ,表示手指下滑;>0 上滑
if (getScrollY() + dy > 0) {
// 这时候默认会走到02 ,也就是隐藏头部了,然后又继续滑动自己导致尾部被显示了一部分
dy = 0 - getScrollY();
}
LogUtils.e("scrollBy zz : %s", dy);
scrollBy(0, dy);
consumed[1] = dy;
} else if (hideBottom) {
LogUtils.w("scrollBy zz : %s", dy);
// 要调整一下
// dy < 0 ,表示手指下滑;>0 上滑
if (getScrollY() + dy < 0) {
// 这时候会 达到 02 的条件,也就是隐藏自己,并且多滑动自己了一点,导致头被显示了一部分
dy = 0 - getScrollY(); // 不让其多滑动
}
LogUtils.e("scrollBy zz : %s", dy);
scrollBy(0, dy);
consumed[1] = dy;
}
}
private void resetFooterState() {
View child = getChildAt(getChildCount() - 1);
TextView tvLoadMore = child.findViewById(R.id.footer_refresh_tv);
tvLoadMore.setTextColor(mLoadMoreTextColor);
View footerImg = child.findViewById(R.id.footer_refresh_img);
footerImg.clearAnimation();
ImageView iv = (ImageView) footerImg;
iv.setImageResource(mLoadMoreDrawable);
footerImg.setAlpha(1);
footerImg.setScaleX(1);
footerImg.setScaleY(1);
footerImg.setRotation(0);
}
private void resetHeaderState() {
View child = getChildAt(0);
TextView tvRefresh = child.findViewById(R.id.header_refresh_tv);
tvRefresh.setTextColor(mRefreshTextColor);
View headerImg = child.findViewById(R.id.header_refresh_img);
headerImg.clearAnimation();
ImageView iv = (ImageView) headerImg;
iv.setImageResource(mRefreshDrawable);
headerImg.setAlpha(1);
headerImg.setScaleX(1);
headerImg.setScaleY(1);
headerImg.setRotation(0);
}
private void smooth2Normal() {
// if (!mScroller.isFinished()) {
// mScroller.forceFinished(true);
// }
if (getScrollY() == 0) {
autoScroll = false;
LogUtils.e("我不该被调用的,因为没得滚动!");
// 特别注意:如果真的执行了 startScroll(0,0,0,0,dur); 会导致下次滑动卡顿
return;
}
int duration = mRefreshBackDuration;
// 因为头一开始就不显示的,所以不是去到 head.height - sy
mScroller.startScroll(getScrollX(), getScrollY(),
getScrollX(), -getScrollY(), duration);
invalidate(); // must
}
public interface OnRefreshListener {
/**
* Called when a swipe gesture triggers a refresh.
*/
void onRefresh();
}
public interface OnLoadMoreListener {
/**
* Called when a swipe gesture triggers a load-more.
*/
void onLoadMore();
}
private OnRefreshListener mOnRefreshListener;
private OnLoadMoreListener mOnLoadMoreListener;
public void setOnRefreshListener(OnRefreshListener listener) {
this.mOnRefreshListener = listener;
}
public void setOnLoadMoreListener(OnLoadMoreListener listener) {
this.mOnLoadMoreListener = listener;
}
/**
* 结束刷新
*/
public void setRefreshDone() {
if (mRefreshDone) {
// 已经结束了,不重复触发
LogUtils.i("已经结束了,不重复触发 refresh done");
return;
}
mRefreshDone = true;
LogUtils.e("#### 结束刷新");
if (mRefreshRotate != null && mRefreshRotate.isStarted()) {
mRefreshRotate.setRepeatCount(0); // 去掉无穷
mRefreshRotate.cancel();
LogUtils.e("结束刷新啊!!!!!!");
}
}
/**
* 结束加载更多
*/
public void setLoadMoreDone() {
if (mLoadMoreDone) {
// 已经结束了,不重复触发
LogUtils.i("已经结束了,不重复触发 load-more done");
return;
}
mLoadMoreDone = true;
LogUtils.e("#### 结束加载");
if (mLoadRotate != null && mLoadRotate.isStarted()) {
mLoadRotate.setRepeatCount(0); // 去掉无穷
mLoadRotate.cancel();
LogUtils.e("结束加载啊!!!!!!");
}
}
}
/**
* <pre>
*
* Note:
* 1. 为什么加载更多的动画总是只执行一次就回弹了?
* - 去掉 clearAnimators 之后可以正常转多次了
* 2. 为什么重写滑动 rv 到顶部或底部,进行上拉或者下拉,这时候总是会拉不动;非要滑动第二次才可以?
* </pre>
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment