如圖,頂部有輪播圖,tab需要吸頂,不同tab對應的條目不同,各tab下的條目存在不同型別,需要支援下拉重新整理與上拉載入。天資愚笨,花了一週時間終於實現,特此記錄。
專案中重新整理載入控制元件採用SmartRefreshLayout,這次仍然打算採用它,不知道是否衝突。
【tab切換】毫無疑問採用TabLayout+ViewPager實現,難點是【吸頂】,因為自己沒有實現過。
google關鍵詞:【android 吸頂+切換】,淘到了這篇簡文,算是有一點眉目,感激作者的分享。
run了Demo並整合了SmartRefreshLayout,謝天謝地,沒有衝突。
Demo中的一個乾貨是OuterRecyclerView和InnerRecyclerView兩個類。主要解決了RecyclerView巢狀後,縱向滑動的衝突。前者負責是否進行事件攔截,後者負責是否消費事件及將結果通知給前者。這兩個類的部分命名錶意性不強,註釋不足,且引入tab後點選事件不靈敏了,我進行了改進,程式碼見文末。
Demo中的另一個乾貨是讓我知道了阿里vLayout的存在,承認平時太懶了,作為程式設計師不關注技術時事及大廠動態的我有點失敗。還好,【吸頂】採用vLayout實現了。但也走了一些彎路,參見了示例的我最後發現要在例項化StickyLayoutHelper之後為其設定顏色helper.setBgColor(0xffffffff)
才符合產品效果圖。
於是就根據不同的資料來源(由於Tab不同)中item的數量及Item的佈局高度計算出ViewPager的高度,並在合適的時候(ViewPager的pageChange監聽中)改變ViewPager的Height,這需要維護的東西太多了,太low了,而且ViewPager的高度最終是計算出來的最大值。
百度關鍵字:【ViewPager Fragment 高度】會發現,各種讓自定義ViewPager並重寫onMeasure方法。有遍歷child找到其中最大高度的、有使用getChildView(0)使用其高度的、有使用getCurrentItem()高度的,看的眼花繚亂。
OuterRecyclerView的Item中layout_height="wrap_content"的ViewPager顯示根本不出來(空白),layout_height="match_parent"的ViewPager也只是顯示一部分(即InnerRecyclerView的列表無法滑動)
但最終找到了老外 寫的 東西解決了自定義ViewPager的問題。同時,對ViewPager的自定義做了擴充套件:ViewPager支援最小高度,否則不滿一屏特別醜,程式碼在下面。
彩蛋:
- 由於ViewPager中呼叫了requestLayout方法,因此tab切換的時候就無法保留之前的狀態(每次都會顯示InnerRecyclerView的頂部)
- requestLayout和requestDisallowInterceptTouchEvent兩個方法很重要
- ViewPager2出來了
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
/**
* 該類由 <b>鍵筆刀</b> 於 2019年2月21日 星期四 9時10分13秒 建立;<br>
* 作用是:<b>存在RecyclerView巢狀時,外層Recyclerview</b>;<br>
* 用於【吸頂】+【切換】功能的實現
* 該RecyclerView對外提供了方法用於控制【是否進行事件攔截】
* <p>
* 參見:https://github.com/FrizzleLiu/NestDemo
*/
public class OuterNestingRecyclerView extends RecyclerView {
/**
* 標記是否需要進行事件攔截,預設攔截
*/
private boolean isNeedIntercept = true;
private float downX; //按下時 的X座標
private float downY; //按下時 的Y座標
/**
* 內層巢狀的ViewPager
*/
private ViewPager vp;
public OuterNestingRecyclerView(Context context) {
super(context);
}
public OuterNestingRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public OuterNestingRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
//將按下時的座標儲存
downX = x;
downY = y;
break;
case MotionEvent.ACTION_MOVE:
//獲取到距離差
float dx = x - downX;
float dy = y - downY;
//通過距離差判斷方向
int orientation = getOrientation(dx, dy);
switch (orientation) {
//右滑動交給ViewPager處理(只有當可以右滑的時候)
case 'r':
//如果呼叫方沒有設定Viewpager,則不攔截手指向右移動事件
if (vp == null) {
setNeedIntercept(false);
} else {
//如果設定了ViewPager,則只有當左邊有ViewPager的條目時,才不攔截手指向右移動事件
if (vp.getCurrentItem() > 0) {
setNeedIntercept(false);
}
}
break;
//左滑動交給ViewPager處理(只有當可以左滑的時候)
case 'l':
//如果呼叫方沒有設定Viewpager,則不攔截手指向左移動事件
if (vp == null) {
setNeedIntercept(false);
} else {
//如果設定了Viewpager,則之後當右邊有ViewPager的條目時,才不攔截手指向左移動事件
if (vp.getCurrentItem() < vp.getAdapter().getCount() - 1) {
setNeedIntercept(false);
}
}
break;
// 點選事件,則不攔截,如果不做此判斷,則tablayout的點選事件就不靈敏了
case 'c':
return false;
}
return isNeedIntercept;
}
return super.onInterceptTouchEvent(e);
}
public void setNeedIntercept(boolean needIntercept) {
isNeedIntercept = needIntercept;
}
private int getOrientation(float dx, float dy) {
if (Math.abs(dx) < 3 && Math.abs(dy) < 3) {
return 'c';//click的意思
}
if (Math.abs(dx) > Math.abs(dy)) {
//X軸移動
return dx > 0 ? 'r' : 'l';//右,左
} else {
//Y軸移動
return dy > 0 ? 'b' : 't';//下//上
}
}
public void setViewPager(ViewPager vp) {
this.vp = vp;
}
}
複製程式碼
import android.content.Context;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
/**
* 該類由 <b>鍵筆刀</b> 於 2019年2月21日 星期四 9時16分09秒 建立;<br>
* 作用是:<b>當存在RecyclerView之間的巢狀時,內層Recyclerview</b>;<br>
* 用於【吸頂】+【切換】功能的實現
* 該RecyclerView主要負責何時進行事件消費,何時禁止父容器攔截事件
* <p>
* 參見:https://github.com/FrizzleLiu/NestDemo
*/
public class InnerNestingRecyclerView extends RecyclerView {
private float downX; //按下時 的X座標
private float downY; //按下時 的Y座標
/**
* 吸頂時,內層RecyclerView左上角的y軸座標
*/
private int stickY;
//初始化個預設值,使用的時候就無需判null了
private InnerConsumeEventListener innerConsumeEventListener = innerConsumeEventOrNot -> {
//no-op
};
public InnerNestingRecyclerView(Context context) {
super(context);
}
public InnerNestingRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public InnerNestingRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean onTouchEvent(MotionEvent e) {
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
//將按下時的座標儲存
downX = x;
downY = y;
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
//獲取到距離差
float dx = x - downX;
float dy = y - downY;
//通過距離差判斷方向
int orientation = getOrientation(dx, dy);
int[] location = {0, 0};
getLocationOnScreen(location);
switch (orientation) {
case 'b':
// 手指從上往下滑動時,即資料向bottom方向滑動,canScrollVertically()用於判
// 斷RecyclerView是否可以縱向滑動,檢查向上滾動為負,檢查向下滾動為正。
// 內層RecyclerView下拉到最頂部時候不再處理事件
if (!canScrollVertically(-1)) {
getParent().requestDisallowInterceptTouchEvent(false);
innerConsumeEventListener.notice(false);
} else {
//內層RecyclerView可以向上滾動的話,父容器禁止攔截時間,內部RecyclerView消費事件
getParent().requestDisallowInterceptTouchEvent(true);
innerConsumeEventListener.notice(true);
}
break;
case 't':
// 當手指從下往上滑動時,即資料向top方向滑動,location[1]代表內層RecyclerView的左
// 上角與螢幕左上點的y軸方向上的距離,
// 如果內層RecyclerView的左上角(亦即頂部)沒有向上滑動到指定位置,即沒有吸
// 頂,則事件由父容器處理
if (location[1] > stickY) {
getParent().requestDisallowInterceptTouchEvent(false);
innerConsumeEventListener.notice(false);
return true;
} else {
//如果已經吸頂了,手指往上滑動時,內層RecyclerView進行事件消費,
//父容器禁止攔截事件
getParent().requestDisallowInterceptTouchEvent(true);
innerConsumeEventListener.notice(true);
}
break;
//左右滑動交給ViewPager處理,不禁止父類進行攔截,即允許父類進行事件攔截
case 'r':
case 'l':
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
break;
}
return super.onTouchEvent(e);
}
private int getOrientation(float dx, float dy) {
if (Math.abs(dx) > Math.abs(dy)) {
//X軸移動
return dx > 0 ? 'r' : 'l';//右,左
} else {
//Y軸移動
return dy > 0 ? 'b' : 't';//下//上
}
}
public void setStickY(int stickY) {
this.stickY = stickY;
}
/**
* 內層RecyclerView是否需要消費事件的監聽
*/
public interface InnerConsumeEventListener {
/**
* 用於通知呼叫方,內層RecyclerView是否消費了事件
*
* @param innerConsumeEventOrNot true:內層消費了事件 false:內層 無需/沒有 消費事件
*/
void notice(boolean innerConsumeEventOrNot);
}
/**
* 設定監聽器,監聽內層RecyclerView是否消費了時間
*/
public void setInnerConsumeEventListener(InnerConsumeEventListener innerConsumeEventListener) {
this.innerConsumeEventListener = innerConsumeEventListener;
}
}
複製程式碼
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.View;
/**
* https://mobikul.com/viewpager/
* https://medium.com/winkl-insights/how-to-have-a-height-wrapping-viewpager-when-images-have-variable-heights-on-android-60b18e55e72e
* https://stackoverflow.com/questions/8394681/android-i-am-unable-to-have-viewpager-wrap-content
*/
public class WrapContentHeightViewPager extends ViewPager {
public WrapContentHeightViewPager(Context context) {
super(context);
initPageChangeListener();
}
public WrapContentHeightViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
initPageChangeListener();
}
private void initPageChangeListener() {
addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
requestLayout();
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
View child = getChildAt(getCurrentItem());
if (child != null) {
child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
int h = child.getMeasuredHeight();
if (minHeight > h) {
h = minHeight;
}
heightMeasureSpec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private int minHeight;
public void setMinHeight(int minHeight) {
this.minHeight = minHeight;
}
}
複製程式碼