學習了別人自定義的文章,然後整了這個滿滿都是bug的控件,自己湊合能用,姑且記錄一下
最開始悶着頭沒想明白就開始寫,結果各種不好使,於是畫了個流程圖梳理了一下思路,終於能用了
思路如上圖,具體在代碼的註釋中
package com.example.myaccount.widget;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.RelativeLayout;
import android.widget.Scroller;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.view.NestedScrollingParent;
import androidx.core.view.NestedScrollingParentHelper;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.example.myaccount.R;
public class PullableLayout extends ViewGroup implements NestedScrollingParent {
private View mTarget; // the target of the gesture,可滑動目標
private View mHeader,mFooter;
private Scroller mLayoutScroller;
private static final int SCROLL_RATIO = 2;
// 普通狀態
private static final int NORMAL = 0;
// 意圖刷新
private static final int TRY_REFRESH = 1;
// 刷新狀態
private static final int REFRESH = 2;
// 意圖加載
private static final int TRY_LOAD_MORE = 3;
// 加載狀態
private static final int LOAD_MORE = 4;
private int status = NORMAL;
private final NestedScrollingParentHelper mNestedScrollingParentHelper;
public PullableLayout(Context context) {
super(context);
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
}
// 從xml中加載view時調用,view都加載完成後會調用onFinishInflate()
public PullableLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mHeader = LayoutInflater.from(context).inflate(R.layout.view_head,null);
mFooter = LayoutInflater.from(context).inflate(R.layout.view_foot,null);
mLayoutScroller = new Scroller(context);
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
}
public PullableLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
}
public PullableLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
}
private TextView tvHead,tvFoot;
private RoundView rvHead,rvFoot;
// 當我們的XML佈局被加載完後,就會回調onFinshInfalte這個方法,在這個方法中我們可以初始化控件和數據。
@Override
protected void onFinishInflate() {
super.onFinishInflate();
tvHead = (TextView) mHeader.findViewById(R.id.tv_head);
rvHead = (RoundView) mHeader.findViewById(R.id.rv_head);
tvFoot = (TextView) mFooter.findViewById(R.id.tv_foot);
rvFoot = (RoundView) mFooter.findViewById(R.id.rv_foot);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT
, LayoutParams.MATCH_PARENT);
mHeader.setLayoutParams(params);
mFooter.setLayoutParams(params);
addView(mHeader);
addView(mFooter);
}
private void ensureTarget(){
for(int i = 0;i<getChildCount();i++){
View child = getChildAt(i);
if(child != mHeader && child != mFooter){
mTarget = child;
break;
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
for(int i = 0; i<getChildCount();i++){
View child = getChildAt(i);
child.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() == 0) {
return;
}
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
// 置位
for (int i = 0; i < getChildCount(); i++){
View child = getChildAt(i);
if (child == mHeader) { // 頭視圖隱藏在頂端
child.layout(0, 0 - child.getMeasuredHeight(), child.getMeasuredWidth(), 0);
} else if (child == mFooter) { // 尾視圖隱藏在layout所有內容視圖之後
child.layout(0, mTarget.getMeasuredHeight(), child.getMeasuredWidth(), mTarget.getMeasuredHeight() + child.getMeasuredHeight());
} else {
child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
}
}
}
// 是否接收嵌套滾動
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes) {
// 縱向返回true
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes) {
}
@Override
public void onStopNestedScroll(@NonNull View target) {
Log.d("zyyu","nested stop");
pullFinish();
mNestedScrollingParentHelper.onStopNestedScroll(target);
}
// 父view是否先消耗滾動參數
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed) {
Log.d("zyyd","onNestedPreScroll dy:"+dy);
Log.d("zyyd","onNestedPreScroll getScrollY():"+getScrollY());
//如果是上滑且頂部控件未完全隱藏,則消耗掉dy,即consumed[1]=dy;
//以mTop的bottom=0時爲原點,只要mTop露頭了,scrollY就是負值
boolean hiddenTop = (dy > 0 && getScrollY() < 0) || status == REFRESH;//下拉過程中的上拉
//-1檢查view向上=手指向下滑動,1向下
// 如果是下滑且內部View已經無法繼續下拉,則消耗掉dy,即consumed[1]=dy,
boolean showTop = dy < 0 && !mTarget.canScrollVertically(-1);
boolean hiddenBottom = (dy < 0 && getScrollY() > 0)|| status == LOAD_MORE;
boolean showBottom = dy > 0 && !mTarget.canScrollVertically(1);
//
// 消耗掉的意思,就是自己去執行scrollBy,實際上就是我們的StickNavLayout滑動
if(hiddenTop || showTop || hiddenBottom || showBottom){
Log.d("zyyd","onNestedPreScroll consumed");
if(hiddenTop || showBottom){
pullUp(dy);
}
if (showTop || hiddenBottom){
pullDown(dy);
}
consumed[1] = dy;
}
}
//子View主動將消費的距離與未消費的距離通知父View
@Override
public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed) {
if(dyUnconsumed > 0){
pullUp(dyUnconsumed);
}else if(dyUnconsumed < 0){
pullDown(dyUnconsumed);
}
}
// 是否消耗fling事件
@Override
public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
// 當頂部控件顯示時,fling可以讓頂部控件隱藏或者顯示。
return false;
}
// 處理慣性事件
@Override
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
return false;
}
@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}
// // 限制滑動範圍
// @Override
// public void scrollTo(int x, int y) {
//
// if(y>0){
// y=0;
// }
//
// int dy = y-getScrollY();
// //down
// if(dy <= 0){
// if (Math.abs(y) >= mHeader.getHeight()){
// y = -mHeader.getHeight();
// }
// }else {
//
// }
//
// super.scrollTo(x,y);
//
// }
private int mLastMoveY;
private int effectiveScrollY = 150;
// important!
@Override
public void computeScroll() {
super.computeScroll();
if (mLayoutScroller.computeScrollOffset()) {
scrollTo(0, mLayoutScroller.getCurrY());
}
postInvalidate();
}
// important 優化AbsListView滑到頂(底)部後不能直接繼續下(上)拉操作
// 原因是AbsListView開始處理滑動事件時,會禁止父view攔截視圖
//// Time to start stealing events! Once we've stolen them, don't let anyone
//// steal from us
// final ViewParent parent = getParent();
// if (parent != null) {
// parent.requestDisallowInterceptTouchEvent(true);
// }
// 如下是Swiperefreshlayout中的解決辦法
@Override
public void requestDisallowInterceptTouchEvent(boolean b) {
// if this is a List < L or another view that doesn't support nested
// scrolling, ignore this request so that the vertical scroll event
// isn't stolen
if ((Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)
|| (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) {
// Nope.
} else {
super.requestDisallowInterceptTouchEvent(b);
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercept = false;
//當前位置
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mLastMoveY = y;
intercept = false;
break;
case MotionEvent.ACTION_MOVE:
Log.d("zyyd"," intercept move");
if(status == REFRESH || status == LOAD_MORE){
//如果不用NestedScrollingParent則不能直接攔截,因爲list滾到頭後應該滾list本身,所以應該是父view滾一段距離把head隱藏後交給list滾
return false;
}
//下拉
if(y > mLastMoveY){
Log.d("zyyd"," intercept move down");
if(mTarget instanceof AbsListView){ //ListView、GridView
intercept = true;
AbsListView adapterChild = (AbsListView) mTarget;
//getFirstVisiblePosition():當前顯示的第一個item的position
//getTop():該item的Top位置
if(adapterChild.getFirstVisiblePosition() != 0
||adapterChild.getChildAt(0).getTop() != 0){
intercept = false;
}
}else {
// intercept = false;
// RecyclerView recyclerView = (RecyclerView)mTarget;
//
// //offset是當前屏幕劃過的距離
// if(recyclerView.computeVerticalScrollOffset()<= 0){
// intercept = true;
// }
intercept = !mTarget.canScrollVertically(-1);
}
}else if(y < mLastMoveY){ //上拉
Log.d("zyyd"," intercept move up");
if(mTarget instanceof AbsListView){
intercept = true;
AbsListView adapterChild = (AbsListView) mTarget;
if(adapterChild.getLastVisiblePosition() != adapterChild.getCount() -1
||adapterChild.getChildAt(adapterChild.getChildCount() - 1).getBottom() != getMeasuredHeight()){
intercept = false;
}
}else {
// intercept = false;
// RecyclerView recyclerView = (RecyclerView) mTarget;
// //extent是當前屏幕顯示的距離;range是整個view可滑動的高度
// if (recyclerView.computeVerticalScrollExtent() + recyclerView.computeVerticalScrollOffset()
// >= recyclerView.computeVerticalScrollRange()){
// intercept = true;
// }
//false表示已滾動底部,攔截
intercept = !mTarget.canScrollVertically(1);
}
}
Log.d("zyyd"," intercept up"+intercept);
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
}
mLastMoveY = y;
return intercept;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//當前位置
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//保存按下的位置
mLastMoveY = y;
break;
case MotionEvent.ACTION_MOVE:
Log.d("zyyd","touch move");
//獲取滑動距離
int dy = mLastMoveY - y;
//下拉
if (dy < 0){
Log.d("zyyd","touch move dy<0");
//1.上拉一段距離後,此時dy<0,然後向下滾動還原getScrollY()>0,此時有可能也超過有效距離,因此需要處理
// 2.|| 滑動不超過頭部1/2
pullDown(dy);
// if(getScrollY()>0||Math.abs(getScrollY()) <= mHeader.getMeasuredHeight()/2){
//滾動
// scrollBy(0, dy);
// if(status != LOAD_MORE && status != TRY_LOAD_MORE) {
// //滑動超過有效距離
// if (Math.abs(getScrollY()) >= effectiveScrollY) {
// tvHead.setText("鬆開刷新");
// status = REFRESH;
// } else {
// status = TRY_REFRESH;
// }
// } else {
// status = LOAD_MORE;
// }
// }
} else if(dy > 0){ //上拉
Log.d("zyyd","touch move dy>0");
pullUp(dy);
}
Log.d("zyyd","touch status:"+status);
break;
case MotionEvent.ACTION_UP:
Log.d("zyyu","touch up status:"+status);
pullFinish();
break;
}
mLastMoveY = y;
// postInvalidate();
return true;
}
private int REFRESH_EFFECTIVE = 300;
private int LOAD_EFFECTIVE = 300;
public void pullDown(int dy){
Log.d("zyyd","pullDown getScrollY:"+getScrollY());
if(getScrollY() >= 0){
scrollBy(0,dy);
if(getScrollY() == 0){
updateStatus(NORMAL);
}else if(Math.abs(getScrollY()) < LOAD_EFFECTIVE){
updateStatus(TRY_LOAD_MORE);
}else {
updateStatus(LOAD_MORE);
}
} else if(Math.abs(getScrollY()) < REFRESH_EFFECTIVE){
scrollBy(0,dy/SCROLL_RATIO);
updateStatus(TRY_REFRESH);
}else {
scrollBy(0,dy/(SCROLL_RATIO*2));
updateStatus(REFRESH);
}
}
public void pullUp(int dy){
Log.d("zyyd","pullUp getScrollY:"+getScrollY());
if(getScrollY() <= 0){
scrollBy(0,dy);
if(getScrollY() == 0){
updateStatus(NORMAL);
}else if(Math.abs(getScrollY()) < REFRESH_EFFECTIVE){
updateStatus(TRY_REFRESH);
}else {
updateStatus(REFRESH);
}
} else if(Math.abs(getScrollY()) <= LOAD_EFFECTIVE){
scrollBy(0,dy/SCROLL_RATIO);
updateStatus(TRY_LOAD_MORE);
}else {
scrollBy(0,dy/(SCROLL_RATIO*2));
updateStatus(LOAD_MORE);
}
}
public void pullFinish(){
//滑動超過有效距離
if(status == REFRESH){
//滑動到可以顯示頭部的位置
//getScroll,當前點爲0,getScroll是以當前點爲原點滑動的距離,下正,上負
mLayoutScroller.startScroll(0, getScrollY(), 0, -(getScrollY() + effectiveScrollY));
tvHead.setVisibility(GONE);
rvHead.setVisibility(VISIBLE);
if(onRefreshListener != null)
onRefreshListener.onRefresh();
} else if( status == LOAD_MORE){
//
mLayoutScroller.startScroll(0,getScrollY(),0,-(getScrollY()-effectiveScrollY));
tvFoot.setVisibility(GONE);
rvFoot.setVisibility(VISIBLE);
if(onRefreshListener != null)
onRefreshListener.onLoad();
}else{
//滑動距離過短,恢復
mLayoutScroller.startScroll(0, getScrollY(), 0, -getScrollY());
}
}
public void updateStatus(int status){
switch (status){
case NORMAL:
break;
case REFRESH:
tvHead.setVisibility(VISIBLE);
tvHead.setText("鬆開刷新");
rvHead.setVisibility(GONE);
break;
case TRY_REFRESH:
tvHead.setVisibility(VISIBLE);
tvHead.setText("下拉刷新");
rvHead.setVisibility(GONE);
break;
case LOAD_MORE:
tvFoot.setVisibility(VISIBLE);
rvFoot.setVisibility(GONE);
tvFoot.setText("鬆開加載更多");
break;
case TRY_LOAD_MORE:
tvFoot.setVisibility(VISIBLE);
rvFoot.setVisibility(GONE);
tvFoot.setText("上拉加載更多");
break;
}
this.status = status;
}
public void refreshDone(){
mLayoutScroller.startScroll(0, getScrollY(), 0, -getScrollY());
rvHead.setVisibility(View.GONE);
tvHead.setText("下拉刷新");
tvHead.setVisibility(View.VISIBLE);
status = NORMAL;
}
public void loadDone(){
mLayoutScroller.startScroll(0, getScrollY(), 0, -getScrollY());
rvFoot.setVisibility(View.GONE);
tvFoot.setText("上拉加載");
tvFoot.setVisibility(View.VISIBLE);
status = NORMAL;
}
private onRefreshListener onRefreshListener;
public void setRefreshListener(PullableLayout.onRefreshListener onRefreshListener) {
this.onRefreshListener = onRefreshListener;
}
public interface onRefreshListener{
void onRefresh();
void onLoad();
}
}