Last active
August 27, 2019 05:55
-
-
Save pythoncat1024/0d122e952f8922269669a224efa0b63d to your computer and use it in GitHub Desktop.
给RecyclerView的下拉刷新,上拉加载控件 https://juejin.im/post/5d6224bff265da03ce39e34d
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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