如圖,頂部有輪播圖,tab須要吸頂,不一樣tab對應的條目不一樣,各tab下的條目存在不一樣類型,須要支持下拉刷新與上拉加載。天資愚笨,花了一週時間終於實現,特此記錄。java
項目中刷新加載控件採用SmartRefreshLayout,此次仍然打算採用它,不知道是否衝突。android
【tab切換】毫無疑問採用TabLayout+ViewPager實現,難點是【吸頂】,由於本身沒有實現過。git
google關鍵詞:【android 吸頂+切換】,淘到了這篇簡文,算是有一點眉目,感激做者的分享。程序員
run了Demo並整合了SmartRefreshLayout,謝天謝地,沒有衝突。github
Demo中的一個乾貨是OuterRecyclerView和InnerRecyclerView兩個類。主要解決了RecyclerView嵌套後,縱向滑動的衝突。前者負責是否進行事件攔截,後者負責是否消費事件及將結果通知給前者。這兩個類的部分命名錶意性不強,註釋不足,且引入tab後點擊事件不靈敏了,我進行了改進,代碼見文末。bash
Demo中的另外一個乾貨是讓我知道了阿里vLayout的存在,認可平時太懶了,做爲程序員不關注技術時事及大廠動態的我有點失敗。還好,【吸頂】採用vLayout實現了。但也走了一些彎路,參見了示例的我最後發現要在實例化StickyLayoutHelper以後爲其設置顏色helper.setBgColor(0xffffffff)
才符合產品效果圖。 app
因而就根據不一樣的數據源(因爲Tab不一樣)中item的數量及Item的佈局高度計算出ViewPager的高度,並在合適的時候(ViewPager的pageChange監聽中)改變ViewPager的Height,這須要維護的東西太多了,太low了,並且ViewPager的高度最終是計算出來的最大值。ide
百度關鍵字:【ViewPager Fragment 高度】會發現,各類讓自定義ViewPager並重寫onMeasure方法。有遍歷child找到其中最大高度的、有使用getChildView(0)使用其高度的、有使用getCurrentItem()高度的,看的眼花繚亂。佈局
OuterRecyclerView的Item中layout_height="wrap_content"的ViewPager顯示根本不出來(空白),layout_height="match_parent"的ViewPager也只是顯示一部分(即InnerRecyclerView的列表沒法滑動)ui
但最終找到了老外 寫的 東西解決了自定義ViewPager的問題。同時,對ViewPager的自定義作了擴展:ViewPager支持最小高度,不然不滿一屏特別醜,代碼在下面。
彩蛋:
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;
}
}
複製代碼