Android -- NestedScrolling滑動機制


1,如今NestedScrolling運用到很多地方了,要想好看一點的滑動變換,基本上就是使用這個來完成的,讓我們來簡單的了解一下。

2,NestedScrolling機制能夠讓父View和子View在滾動式進行配合,其基本流程如下:

  • 當子view開始滾動之前,可以通知父View,讓其先於自己進行滾動;
  • 子View自己進行滾動;
  • 子view滾動之后,還可以通知父view繼續滾動。
  而要實現這樣的交互機制,首先父view要實現NestedScrollingParent接口,而子View需要實現N恩斯特大S從rollingChild接口,在這套機制中子View是發起者,父view是接受回調並做出響應的。
   一下是幾個關鍵的類和接口
//主要接口
NestedScrollingChild
NestedScrollingParent
//幫助類
NestedScrollingChildHelper
NestedScrollingParentHelper

  

  一些新的系統View已經幫我們實現了以上兩個接口,也就是說他們是支持NestedScrolling,例如:
  NestedScrollView已經實現了NestedScrollingChild和NestedScrollingParent兩個接口
  RecycleView已經實現了NestedScrollingChild
  CoordinatorLayout實現了NestedScrollingParent
  ....等等。
 
  NestedScrollingChild接口
      
//開始、停止嵌套滾動
public boolean startNestedScroll(int axes); public void stopNestedScroll();
//觸摸滾動相關
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
//慣性滾動相關 public boolean dispatchNestedPreFling(float velocityX, float velocityY);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
 
  public boolean startNestedScroll(int axes);
 
  開啟嵌套滾動流程(實際上是進行了一些嵌套滾動前准備工作)。
  當找到了能夠配合當前子view進行嵌套滾動的父view時,返回值為true(Returns:true if a cooperative parent was found and nested scrolling has been enabled for the current gesture)。
 
  public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
 
  在子view自己進行滾動之前調用此方法,詢問父view是否要在子view之前進行滾動。
  此方法的前兩個參數用於告訴父View此次要滾動的距離;而第三第四個參數用於子view獲取父view消費掉的距離和父view位置的偏移量。
  第一第二個參數為輸入參數,即常規的函數參數,調用函數的時候我們需要為其傳遞確切的值。而第三第四個參數為輸出參數,調用函數時我們只需要傳遞容器(在這里就是兩個數組),在調用結束后,我們就可以從容器中獲取函數輸出的值。
  如果parent消費了一部分或全部距離,則此方法返回true。
 
  public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
 
  在子view自己進行滾動之后調用此方法,詢問父view是否還要進行余下(unconsumed)的滾動。
  前四個參數為輸入參數,用於告訴父view已經消費和尚未消費的距離,最后一個參數為輸出參數,用於子view獲取父view位置的偏移量。
  返回值:(翻譯出來可能有歧義,直接放原文)true if the event was dispatched, false if it could not be dispatched.
 
  public void stopNestedScroll();
  最后,stopNestedScroll()方法與startNestedScroll(int axes)對應,用於結束嵌套滾動流程;而慣性滾動相關的兩個方法與觸摸滾動相關的兩個方法類似,這里不再贅述。
 
  NestedScrollingParent
  接口概述
  
//當開啟、停止嵌套滾動時被調用
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
//當觸摸嵌套滾動時被調用
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
//當慣性嵌套滾動時被調用
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

  

  從命名可以看出,這幾個都是回調方法。當調用NestedScrollingChild中的方法時,NestedScrollingParent中與之相對應的方法就會被回調。方法之間的具體對應關系如下:
   
  從上面的接口還有方法我們可以得出一些簡單的流程
  • 調用child的startNestedScroll()來發起嵌套滑動流程(實質上是尋找能夠配合child進行嵌套滾動的parent)。parent的onStartNestedScroll()會被調用,若此方法返回true,則OnNestScrollAccepted()也會被調用。
  • chuld每次滾動前,可以先詢問parent是否要滾動,即調用dispatchNestedScroll(),這時可以回調到parent的OnNestedPreScroll(),parent可以在這個回調中先於child滾動。
  • dispatchNestedPreScroll()之后,child可以進行自己的滾動操作。

3,自定義NestedScrolling控件

  先看一下效果

  

  先看一下布局文件

  activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
    >

    <com.qianmo.mynestedscrolling.view.MyNestedScrollParent
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:orientation="vertical">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#f0f"
            android:text="上面的圖片會被隱藏,而這個文字不會被隱藏"/>

        <com.qianmo.mynestedscrolling.view.MyNestedScrollChild
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="123\n456\n789\n111\n222\n333\n444\n555\n666\n777\n888\n999\n14\n12\n13\n44\n55\n66\n77\n88\n99\n11\n22\n33\n44\n55\n66\n77\n88\n99\n77\n88\n88\n8\n88\n88\n"
                android:textColor="#f0f"
                android:textSize="20sp"/>
        </com.qianmo.mynestedscrolling.view.MyNestedScrollChild>
    </com.qianmo.mynestedscrolling.view.MyNestedScrollParent>
</RelativeLayout>

  布局文件只是簡單的嵌套,MyNestedScrollParent繼承Linearlayout,並實現NestedScrollingParent接口,MyNestedScrollChild同理,先來看看MyNestedScrollChild這個類吧。

  MyNestedScrollChild.java

package com.qianmo.mynestedscrolling.view;

import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;


/**
 * Created by Administrator on 2017/2/14 0014.
 * E-Mil:543441727@qq.com
 */

public class MyNestedScrollChild extends LinearLayout implements NestedScrollingChild {
    private NestedScrollingChildHelper mNestedScrollingChildHelper;
    private final int[] offset = new int[2]; //偏移量
    private final int[] consumed = new int[2]; //消費
    private int lastY;
    private int showHeight;


    public MyNestedScrollChild(Context context) {
        super(context);
    }

    public MyNestedScrollChild(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //第一次測量,因為布局文件中高度是wrap_content,因此測量模式為atmost,即高度不超過父控件的剩余空間
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        showHeight = getMeasuredHeight();

        //第二次測量,對稿哦度沒有任何限制,那么測量出來的就是完全展示內容所需要的高度
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            //按下
            case MotionEvent.ACTION_DOWN:
                lastY = (int) event.getRawY();
                break;
            //移動
            case MotionEvent.ACTION_MOVE:
                int y = (int) (event.getRawY());
                int dy = y - lastY;
                lastY = y;
                if (startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL)
                        && dispatchNestedPreScroll(0, dy, consumed, offset)) //如果找到了支持嵌套滑動的父類,父類進行了一系列的滑動
                {
                    //獲取滑動距離
                    int remain = dy - consumed[1];
                    if (remain != 0) {
                        scrollBy(0, -remain);
                    }

                } else {
                    scrollBy(0, -dy);
                }
                break;
        }

        return true;
    }

    //限制滾動范圍
    @Override
    public void scrollTo(int x, int y) {
        int maxY = getMeasuredHeight() - showHeight;
        if (y > maxY) {
            y = maxY;
        }
        if (y < 0) {
            y = 0;
        }
        super.scrollTo(x, y);
    }

    //初始化helper對象
    private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mNestedScrollingChildHelper == null) {
            mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
            mNestedScrollingChildHelper.setNestedScrollingEnabled(true);
        }
        return mNestedScrollingChildHelper;
    }

    //實現一下接口
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        getScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return getScrollingChildHelper().isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return getScrollingChildHelper().startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        getScrollingChildHelper().stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return getScrollingChildHelper().hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
    }
}

 主要是在OnTouchEvent中先后調用了startNestedScroll()dispatchNestedPreScroll()方法,在借助helper來完成NestedScrollingParent接口方法

  MyNestedScrollParent.java

package com.qianmo.mynestedscrolling.view;

import android.content.Context;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

/**
 * Created by wangjitao on 2017/2/14 0014.
 * E-Mail:543441727@qq.com
 * 嵌套滑動機制父View
 */

public class MyNestedScrollParent extends LinearLayout implements NestedScrollingParent {
    private ImageView img;
    private TextView tv;
    private MyNestedScrollChild myNestedScrollChild;
    private NestedScrollingParentHelper mNestedScrollingParentHelper;
    private int imgHeight;
    private int tvHeight;

    public MyNestedScrollParent(Context context) {
        super(context);
    }

    public MyNestedScrollParent(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
    }

    //獲取子view
    @Override
    protected void onFinishInflate() {
        img = (ImageView) getChildAt(0);
        tv = (TextView) getChildAt(1);
        myNestedScrollChild = (MyNestedScrollChild) getChildAt(2);
        img.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (imgHeight <= 0) {
                    imgHeight = img.getMeasuredHeight();
                }
            }
        });
        tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if (tvHeight <= 0) {
                    tvHeight = tv.getMeasuredHeight();
                }
            }
        });
    }

    //在此可以判斷參數target是哪一個子view以及滾動的方向,然后決定是否要配合其進行嵌套滾動
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        if (target instanceof MyNestedScrollChild) {
            return true;
        }
        return false;
    }


    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
    }

    @Override
    public void onStopNestedScroll(View target) {
        mNestedScrollingParentHelper.onStopNestedScroll(target);
    }

    //先於child滾動
    //前3個為輸入參數,最后一個是輸出參數
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        if (showImg(dy) || hideImg(dy)) {//如果需要顯示或隱藏圖片,即需要自己(parent)滾動
            scrollBy(0, -dy);//滾動
            consumed[1] = dy;//告訴child我消費了多少
        }
    }

    //后於child滾動
    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {

    }

    //返回值:是否消費了fling
    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }

    //返回值:是否消費了fling
    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }

    @Override
    public int getNestedScrollAxes() {
        return mNestedScrollingParentHelper.getNestedScrollAxes();
    }

    //下拉的時候是否要向下滾動以顯示圖片
    public boolean showImg(int dy) {
        if (dy > 0) {
            if (getScrollY() > 0 && myNestedScrollChild.getScrollY() == 0) {
                return true;
            }
        }

        return false;
    }

    //上拉的時候,是否要向上滾動,隱藏圖片
    public boolean hideImg(int dy) {
        if (dy < 0) {
            if (getScrollY() < imgHeight) {
                return true;
            }
        }
        return false;
    }

    //scrollBy內部會調用scrollTo
    //限制滾動范圍
    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        }
        if (y > imgHeight) {
            y = imgHeight;
        }

        super.scrollTo(x, y);
    }
}

  MyNestedScrollParent主要是實現一下功能

  ①、在onStartNestedScroll()中判斷參數target是哪一個子view以及滾動的方向,然后決定是否要配合其進行嵌套滾動

  ②、在onNestedPreScroll()中獲取需要滾動的距離,根據情況決定自己是否要進行滾動,最后還要將自己滾動消費掉的距離存儲在consumed數組中回傳給child

  就這樣基本實現了,很簡單有沒有,再看看我們接下來要實現的效果,如圖:

  

  也很簡單,就不在廢話了,直接和上面的項目一起,直接上源碼  https://github.com/543441727/MyNestedScrolling

  See You Next Time ! 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM