一、需求簡介
app首頁中經常要實現首頁頭卡共享,tab吸頂,內容區通過ViewPager切換的需求,以前往往是利用事件處理來完成,還有Google官方也提供了相關的庫,但是這些也有一定的弊端,適配起來還是比較複雜。這裏我們利用NestedScrolling機制來實現,之前的博客中也有一篇類似的博客《Android 利用NestedScrolling聯動機制爲RecyclerView添加Header》。這個版本實際上在之前的版本上進行了一些擴展,使其能支持ViewPager。
當然也有很多開源項目,發現存在的問題很多面,主要問題如下:
- 頭部和內容區域不聯動
- 沒有中斷RecyclerView 的fling效果,導致RecyclerView搶佔ViewPager事件
- 侵入式設計太多(當然,本篇方案解決RecyclerView中斷fling時用了侵入式設計)
二、效果展示
三、代碼實現
要點:
頭部不聯動問題:
我們需要處理在dispatchTouchEvent或者利用onInteceptTouchEvent + onTouchEvent處理,主要處理VelocityTracker + fling事件。
RecyclerView中斷fling效果問題:
RecyclerView沒有在stopNestedScroll()方法中中斷滑動,因此需要通過侵入方式,調用stopScroll()去完成
主要代碼
public class NestedPagerRecyclerViewLayout extends FrameLayout implements NestedScrollingParent2 {
private final int mFlingVelocity;
private int mHeadExpandedOffset;
private float startEventX = 0;
private float startEventY = 0;
private float mSlopTouchScale = 0;
private boolean isTouchMoving = false;
private View mHeaderView = null;
private View mBodyView = null;
private View mVerticalScrollView = null;
private VelocityTracker mVelocityTracker;
private NestedScrollingParentHelper parentHelper = new NestedScrollingParentHelper(this);
public NestedPagerRecyclerViewLayout(@NonNull Context context) {
this(context, null);
}
public NestedPagerRecyclerViewLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedPagerRecyclerViewLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (attrs != null) {
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NestedPagerRecyclerViewLayout);
mHeadExpandedOffset = a.getInt(R.styleable.NestedPagerRecyclerViewLayout_headExpandedOffset, 0);
a.recycle();
}
mSlopTouchScale = ViewConfiguration.get(context).getScaledTouchSlop();
mFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
setClickable(true);
}
/**
* 頭部餘留偏移
* @param headExpandedOffset
*/
public void setHeadExpandOffset(int headExpandedOffset) {
this.mHeadExpandedOffset = headExpandedOffset;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
int height = MeasureSpec.getSize(heightMeasureSpec);
int overScrollExtent = overScrollExtent();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+ 0, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+ 0, height-overScrollExtent);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
public boolean canScrollVertically(int direction) {
final int offset = computeVerticalScrollOffset();
final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range;
}
}
@Override
protected int computeVerticalScrollRange() {
int childCount = getChildCount();
if (childCount == 0) return super.computeVerticalScrollRange();
int range = getPaddingBottom() + getPaddingTop();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
range += child.getHeight() + lp.bottomMargin + lp.topMargin;
}
if (range < getHeight()) {
return super.computeVerticalScrollRange();
}
return range;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mHeaderView = getChildView(LayoutParams.TYPE_HEAD);
mBodyView = getChildView(LayoutParams.TYPE_BODY);
int childLeft = getPaddingLeft();
int childTop = getPaddingTop();
if (mHeaderView != null) {
LayoutParams lp = (LayoutParams) mHeaderView.getLayoutParams();
mHeaderView.layout(childLeft + lp.leftMargin, childTop + lp.topMargin, childLeft + lp.leftMargin + mHeaderView.getMeasuredWidth(), childTop + lp.topMargin + mHeaderView.getMeasuredHeight());
childTop += mHeaderView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
if (mBodyView != null) {
LayoutParams lp = (LayoutParams) mBodyView.getLayoutParams();
mBodyView.layout(childLeft + lp.leftMargin, childTop + lp.topMargin, childLeft + lp.leftMargin + mBodyView.getMeasuredWidth(), childTop + lp.topMargin + mBodyView.getMeasuredHeight());
}
}
protected int overScrollExtent() {
return Math.max(mHeadExpandedOffset,0);
}
private View getHeaderView() {
return mHeaderView;
}
private View getBodyView() {
return mBodyView;
}
private View findTouchView(float currentX, float currentY) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
float childX = (child.getX() - getScrollX());
float childY = (child.getY() - getScrollY());
if (currentX < childX || currentX > (childX + child.getWidth())) {
continue;
}
if (currentY < childY || currentY > (childY + child.getHeight())) {
continue;
}
return child;
}
return null;
}
private boolean hasHeader() {
int count = getChildCount();
for (int i = 0; i < count; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_HEAD) {
return true;
}
}
return false;
}
public View getChildView(int layoutType) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.childLayoutType == layoutType) {
return getChildAt(i);
}
}
return null;
}
private boolean hasBody() {
int count = getChildCount();
for (int i = 0; i < count; i++) {
LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
if (lp.childLayoutType == LayoutParams.TYPE_BODY) {
return true;
}
}
return false;
}
@Override
public void addView(View child) {
assertLayoutType(child);
super.addView(child);
}
private void assertLayoutType(View child) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
assertLayoutParams(lp);
}
private void assertLayoutParams(ViewGroup.LayoutParams lp) {
if (hasHeader() && hasBody()) {
throw new IllegalStateException("header and body has already existed");
}
if (hasHeader()) {
if (!(lp instanceof LayoutParams)) {
throw new IllegalStateException("header should keep only one");
}
if (((LayoutParams) lp).childLayoutType == LayoutParams.TYPE_HEAD) {
throw new IllegalStateException("header should keep only one");
}
}
if (hasBody()) {
if ((lp instanceof LayoutParams) && ((LayoutParams) lp).childLayoutType == LayoutParams.TYPE_BODY) {
throw new IllegalStateException("header should keep only one");
}
}
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
assertLayoutParams(params);
super.addView(child, index, params);
}
@Override
public void addView(View child, int index) {
assertLayoutType(child);
super.addView(child, index);
}
@Override
public void addView(View child, int width, int height) {
assertLayoutParams(new LinearLayout.LayoutParams(width, height));
super.addView(child, width, height);
}
@Override
public void onViewAdded(View child) {
super.onViewAdded(child);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected FrameLayout.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
@Override
public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
return new LayoutParams(lp);
}
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
if (axes == SCROLL_AXIS_VERTICAL) {
//只關注垂直方向的移動
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int offset = computeVerticalScrollOffset();
if (offset <= maxOffset) {
mVerticalScrollView = target;
return true;
}
}else {
mVerticalScrollView = null;
}
return false;
}
@Override
protected int computeVerticalScrollExtent() {
int computeVerticalScrollExtent = super.computeVerticalScrollExtent();
return computeVerticalScrollExtent ;
}
@Override
public int getNestedScrollAxes() {
return parentHelper.getNestedScrollAxes();
}
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
parentHelper.onNestedScrollAccepted(child,target,axes,type);
}
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
if (mVerticalScrollView == target) {
Log.d("onNestedScroll", "::::onStopNestedScroll vertical");
parentHelper.onStopNestedScroll(target, type);
}
}
@Override
public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
Log.e("onNestedScroll", "::::onNestedScroll 11111");
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @Nullable int[] consumed, int type) {
int scrollRange = computeVerticalScrollRange();
if (scrollRange <= getHeight()) {
return;
}
if (target == null) return;
if (mVerticalScrollView != target) {
return;
}
Log.e("onNestedScroll", "::::onNestedPreScroll 00000");
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
handleVerticalNestedScroll(dx, dy, consumed, maxOffset, scrollOffset);
}
private void handleVerticalNestedScroll(int dx, int dy, @Nullable int[] consumed, int maxOffset, int scrollOffset) {
if (dy == 0) {
return;
}
if (!canScrollOffsetTop(mVerticalScrollView)) {
return;
}
int dyOffset = dy;
int targetOffset = scrollOffset + dy;
if (targetOffset >= maxOffset) {
dyOffset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
dyOffset = 0 - scrollOffset;
}
if (!canScrollVertically(dyOffset)) {
return;
}
consumed[1] = dyOffset;
Log.d("onNestedScroll", "::::" + dyOffset + "+" + scrollOffset + "=" + (scrollOffset + dyOffset));
scrollBy(0, dyOffset);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int scrollRange = computeVerticalScrollRange();
if (scrollRange <= getHeight()) {
return super.dispatchTouchEvent(event);
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mVelocityTracker.addMovement(event);
startEventX = event.getX();
startEventY = event.getY();
isTouchMoving = false;
if(mVerticalScrollView instanceof RecyclerView){
((RecyclerView) mVerticalScrollView).stopScroll();
/**
*RecyclerView 雖然繼承了NestedScrollingChild,但是沒有在stopNestedScroll中停止
*調用stopScroll,導致滑動狀態事件自動捕獲,造成ViewPager切換問題,這裏使用stopScroll()侵入式調用
*/
}else if(mVerticalScrollView instanceof NestedScrollingChild){
mVerticalScrollView.stopNestedScroll();
}
break;
case MotionEvent.ACTION_MOVE:
float currentX = event.getX();
float currentY = event.getY();
float dx = currentX - startEventX;
float dy = currentY - startEventY;
if (!isTouchMoving && Math.abs(dy) < Math.abs(dx)) {
startEventX = currentX;
startEventY = currentY;
break;
}
View touchView = null;
int offset = (int) -dy;
if (Math.abs(dy) >= mSlopTouchScale) {
touchView = findTouchView(currentX, currentY);
isTouchMoving = touchView != null && touchView == getHeaderView();
}
if (offset != 0 && !canScrollVertically(offset)) {
isTouchMoving = false;
}
startEventX = currentX;
startEventY = currentY;
if (!isTouchMoving) {
break;
}
mVelocityTracker.addMovement(event);
int maxOffset = computeVerticalScrollRange() - computeVerticalScrollExtent();
int scrollOffset = computeVerticalScrollOffset();
int targetOffset = scrollOffset + offset;
if (targetOffset >= maxOffset) {
offset = maxOffset - scrollOffset;
}
if (targetOffset <= 0) {
offset = 0 - scrollOffset;
}
if (offset == 0) {
break;
}
scrollBy(0, offset);
Log.d("onNestedScroll", ">:>:>" + offset + "+" + scrollOffset + "=" + (scrollOffset + offset));
super.dispatchTouchEvent(event);
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
mVelocityTracker.addMovement(event);
if (isTouchMoving) {
isTouchMoving = false;
mVelocityTracker.computeCurrentVelocity(1000, mFlingVelocity);
startFling(mVelocityTracker, (int) event.getX(), (int) event.getY());
mVelocityTracker.recycle();
mVelocityTracker = null;
stopNestedScroll();
}
break;
}
return super.dispatchTouchEvent(event);
}
private void startFling(VelocityTracker velocityTracker, int x, int y) {
int xVolecity = (int) velocityTracker.getXVelocity();
int yVolecity = (int) velocityTracker.getYVelocity();
if (mVerticalScrollView instanceof NestedScrollingChild) {
Log.d("onNestedScroll", "onNestedScrollfling xVolecity=" + xVolecity + ", yVolecity=" + yVolecity);
((RecyclerView) mVerticalScrollView).fling(xVolecity, -yVolecity);
}
}
private boolean canScrollOffsetTop(View view) {
if (view instanceof RecyclerView) {
//顯示區域最上面一條信息的position
RecyclerView.LayoutManager manager = ((RecyclerView) view).getLayoutManager();
if (manager == null) {
return true;
}
if (manager.getChildCount() == 0) {
return true;
}
int scrollOffset = ((RecyclerView) view).computeVerticalScrollOffset();
return scrollOffset <= 0;
}
if (view instanceof NestedScrollingChild) {
return view.canScrollVertically(-1);
}
if (!(view instanceof ViewGroup) && (view instanceof View) ) {
return true;
}
throw new IllegalArgumentException("不支持非NestedScrollingChild子類ViewGroup");
}
public static class LayoutParams extends FrameLayout.LayoutParams {
public final static int TYPE_HEAD = 0;
public final static int TYPE_BODY = 1;
private int childLayoutType = TYPE_HEAD;
public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
super(c, attrs);
if (attrs == null) return;
final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.NestedPagerRecyclerViewLayout);
childLayoutType = a.getInt(R.styleable.NestedPagerRecyclerViewLayout_layoutScrollNestedType, 0);
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(@NonNull MarginLayoutParams source) {
super(source);
}
}
}
屬性定義:
<declare-styleable name="NestedPagerRecyclerViewLayout">
<attr name="layoutScrollNestedType" format="flags">
<flag name="Head" value="0"/>
<flag name="Body" value="1"/>
</attr>
<attr name="headExpandedOffset" format="dimension|reference" />
</declare-styleable>
佈局文件
<?xml version="1.0" encoding="utf-8"?>
<com.smartian.widget.NestedPagerRecyclerViewLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/NestedScrollChildLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
android:focusableInTouchMode="true"
app:headExpandedOffset="45dp">
<LinearLayout
android:id="@+id/head"
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical"
app:layoutScrollNestedType="Head">
<TextView
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:background="@color/colorAccent"
android:gravity="center"
android:text="top Head" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="45dp">
<TextView
android:id="@+id/tab1"
android:layout_width="0dip"
android:layout_height="45dp"
android:layout_weight="1"
android:background="@android:color/white"
android:gravity="center"
android:text="我是tab1" />
<View
android:layout_width="1dip"
android:layout_height="match_parent"
android:background="@color/colorAccent" />
<TextView
android:id="@+id/tab2"
android:layout_width="0dip"
android:layout_height="45dp"
android:layout_weight="1"
android:background="@android:color/white"
android:gravity="center"
android:text="我是tab2" />
</LinearLayout>
</LinearLayout>
<android.support.v4.view.ViewPager
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
app:layoutScrollNestedType="Body" />
</com.smartian.widget.NestedPagerRecyclerViewLayout>
至此,我們的方案基本實現了,使用方式如下
public class MyNestedScrollViewActivity extends Activity implements View.OnClickListener {
private ViewPager viewPager;
private NestedPagerRecyclerViewLayout scrollChildLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_nested_scrolling_child_layout);
scrollChildLayout = findViewById(R.id.NestedScrollChildLayout);
scrollChildLayout.setHeadExpandOffset((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,45,getResources().getDisplayMetrics()));
viewPager = findViewById(R.id.body);
findViewById(R.id.tab1).setOnClickListener(this);
findViewById(R.id.tab2).setOnClickListener(this);
viewPager.setAdapter(new PagerAdapter() {
@Override
public int getCount() {
return 2;
}
@Override
public boolean isViewFromObject(@NonNull View view, Object object) {
return view==object;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.addView((View) object);
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
View layoutView = LayoutInflater.from(container.getContext()).inflate(R.layout.fragment_recycler_view, container, false);
RecyclerView recyclerView = layoutView.findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(container.getContext()));
SimpleRecyclerAdapter adapter = new SimpleRecyclerAdapter(container.getContext(), position%2==0?getData():getData2());
recyclerView.setAdapter(adapter);
container.addView(layoutView);
return layoutView;
}
});
}
private List<String> getData() {
List<String> data = new ArrayList<>();
data.add("#ff9999");
data.add("#ffaa77");
data.add("#ff9966");
data.add("#ffcc55");
data.add("#ff99bb");
data.add("#ff77dd");
data.add("#ff33bb");
data.add("#ff9999");
data.add("#ffaa77");
data.add("#ff9966");
data.add("#ffcc55");
return data;
}
private List<String> getData2() {
List<String> data = new ArrayList<>();
data.add("#9999ff");
data.add("#aa77ff");
data.add("#9966ff");
data.add("#cc55ff");
data.add("#99bbff");
data.add("#77ddff");
data.add("#33bbff");
data.add("#9999ff");
data.add("#aa77ff");
data.add("#9966ff");
data.add("#cc55ff");
return data;
}
@Override
public void onClick(View v) {
int id = v.getId();
if(id==R.id.tab1){
viewPager.setCurrentItem(0,true);
}else if(id==R.id.tab2){
viewPager.setCurrentItem(1,true);
}
}
}