Android自定义View--使用NestScrolling机制实现一个上下滑动退出Layout

效果图

NestScrolling的实现

新建ElasticDragDismissFrameLayout继承自FrameLayout,重写onStartNestedScroll,onNestedPreScroll,onNestedScroll,onStopNestedScroll方法,同时onNestedPreScroll,onNestedScroll,onStopNestedScroll去掉调用父类的方法,改由我们自己实现。

package rc.loveq.elasticdragdismiss.widget;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;

/**
 * Author:Rc
 * 0n 2017/10/29 11:17
 */

public class ElasticDragDismissFrameLayout extends FrameLayout {

    public ElasticDragDismissFrameLayout(@NonNull Context context) {
        this(context, null, 0, 0);
    }

    public ElasticDragDismissFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0, 0);
    }

    public ElasticDragDismissFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ElasticDragDismissFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        Log.d("ElasticDragDismissFrame", "onStartNestedScroll");
        return super.onStartNestedScroll(child, target, nestedScrollAxes);
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        Log.d("ElasticDragDismissFrame", "onNestedPreScroll dy:" + dy);
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
   Log.d("ElasticDragDismissFrame", "onNestedScroll  dyConsumed" + dyConsumed
                + "     dyUnconsumed:" + dyUnconsumed);
    }

    @Override
    public void onStopNestedScroll(View child) {
      Log.d("ElasticDragDismissFrame", "onStopNestedScroll");
    }
}

然后在布局中引入

"1.0" encoding="utf-8"?>
.loveq.elasticdragdismiss.widget.ElasticDragDismissFrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="rc.loveq.elasticdragdismiss.MainActivity">

    .support.v7.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

.loveq.elasticdragdismiss.widget.ElasticDragDismissFrameLayout>

RecyclerView设置adapterlayoutManagerMyAdapter的代码这里就不贴了。

public class MainActivity extends AppCompatActivity {
    RecyclerView mRecyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRecyclerView = findViewById(R.id.recycler);
        mRecyclerView.setAdapter(new MyAdapter());
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
    }
}

onStartNestedScroll

现在可以滚动了,但是发现在控制台NestScrolling4个方法中只有onStartNestedScroll输出。

10-29 11:55:36.350 14079-14079/rc.loveq.elasticdragdismiss D/ElasticDragDismissFrame: onStartNestedScroll
10-29 11:55:36.351 14079-14079/rc.loveq.elasticdragdismiss D/ElasticDragDismissFrame: onStartNestedScroll
10-29 11:55:37.573 14079-14079/rc.loveq.elasticdragdismiss D/ElasticDragDismissFrame: onStartNestedScroll
10-29 11:55:37.574 14079-14079/rc.loveq.elasticdragdismiss D/ElasticDragDismissFrame: onStartNestedScroll

到底是为什么呢?在源码中我们看到一段关于onStartNestedScroll方法的描述

......

This method will be called in response to a descendant view invoking{@link View#startNestedScroll(int)}. Each parent up the view hierarchy will be given an opportunity to respond and claim the nested scrolling operation by returningtrue

This method may be overridden by ViewParent implementations to indicate when the view is willing to support a nested scrolling operation that is about to begin. If it returns true, this ViewParent will become the target view's nested scrolling parent for the duration of the scroll operation in progress. When the nested scroll is finished this ViewParent will receive a call to {@link #onStopNestedScroll(View)}

......

也就是说onStartNestedScroll返回true我们才能响应嵌套滚动操作,响应后序的滚动事件。这里我们这在垂直方向返回true

  @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        Log.d("ElasticDragDismissFrame", "onStartNestedScroll");
        return (nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0;
    }

这时候我们滚动RecyclerView,在控制台能看到每个方法的回调都有了。

onNestedPreScroll

接下来,先介绍一下onNestedPreScroll方法各个参数的意思

  @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        }
  @param target View that initiated the nested scroll
      引发嵌套滚动的View(这里是RecyclerView)
  @param dx Horizontal scroll distance in pixels
  @param dy Vertical scroll distance in pixels
  @param consumed Output. The horizontal and vertical scroll distance consumed by this parent
      父亲(这里是ElasticDragDismissFrameLayout)消耗的距离

RecyclerView准备滚动的时候,还没滚动,会调用onNestedPreScroll,告诉ElasticDragDismissFrameLayout,要不要消费RecyclerView我的滚动距离,如果不消费,那么我就开始滚动了。如果消费,那么RecyclerView滚动的距离=准备滚动的距离-被消费的距离。

也就是说如果

 @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        Log.d("ElasticDragDismissFrame", "onStartNestedScroll dy:" + dy);
        consumed[1] = dy;
    }

那么表示ElasticDragDismissFrameLayout完全消费掉RecyclerView滚动的距离,RecyclerView无法滚动(注意:这里fling没有消费),这时ElasticDragDismissFrameLayout是这样的

package rc.loveq.elasticdragdismiss.widget;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;

/**
 * Author:Rc
 * 0n 2017/10/29 11:17
 */

public class ElasticDragDismissFrameLayout extends FrameLayout {

    public ElasticDragDismissFrameLayout(@NonNull Context context) {
        this(context, null, 0, 0);
    }

    public ElasticDragDismissFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0, 0);
    }

    public ElasticDragDismissFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ElasticDragDismissFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        Log.d("ElasticDragDismissFrame", "onStartNestedScroll");
        return (nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0;
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        Log.d("ElasticDragDismissFrame", "onStartNestedScroll dy:" + dy);
        consumed[1] = dy;
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        Log.d("ElasticDragDismissFrame", "onNestedScroll  dyConsumed" + dyConsumed
                + "     dyUnconsumed:" + dyUnconsumed);
    }

    @Override
    public void onStopNestedScroll(View child) {
        Log.d("ElasticDragDismissFrame", "onStopNestedScroll");
    }
}

这时尝试去滚动RecyclerView,会发现是滚动不了的,因为ElasticDragDismissFrameLayout完全消费掉RecyclerView滚动的距离。

onNestedScroll

接着onNestedScroll方法

  @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        Log.d("ElasticDragDismissFrame", "onNestedScroll  dyConsumed" + dyConsumed
                + "     dyUnconsumed:" + dyUnconsumed);
    }
      @param target The descendent view controlling the nested scroll
      触发NestScroll的View(这里是RecyclerView)
      @param dxConsumed Horizontal scroll distance in pixels already consumed by target

      @param dyConsumed Vertical scroll distance in pixels already consumed by target
      y方向RecyclerView消费的像素
      @param dxUnconsumed Horizontal scroll distance in pixels not consumed by target
      @param dyUnconsumed Vertical scroll distance in pixels not consumed by target
      y方向RecyclerView没有消费的像素

ElasticDragDismissFrameLayout没有在onNestedPreScroll方法中消费移动的距离的情况,onNestedPreScroll方法的dy等于onNestedScroll方法的dyConsumed
RecyclerView滑动到边缘,不能在滑动了,这时候onNestedPreScrolldy等于滑动的距离,因为这时候RecyclerView不能再滑动,所以onNestedScrolldyConsumed等于0(RecyclerView没有消费距离),而dxUnconsumed等于onNestedPreScrolldy

拖拽缩放

因为想实现的RecyclerView滚动到边缘之后,能再拖拽一段距离并伴随这缩放,所以在onNestedScroll处理dyUnconsumed(注意这里是dyUnconsumed)。

   @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
                               int dxUnconsumed, int dyUnconsumed) {
        dragScale(dyUnconsumed);
    }

dragScale

 private void dragScale(int scroll) {
        if (scroll == 0) return;

        totalDrag += scroll;


        //跟踪滑动的方向并设置缩放的中心
        //
        // don't double track i.e. if start dragging down and then reverse, keep tracking as
        // dragging down until they reach the 'natural' position
        if (scroll < 0 && !draggingUp && !draggingDown) {
            draggingDown = true;
            if (shouldScale) setPivotY(getHeight());
        } else if (scroll > 0 && !draggingDown && !draggingUp) {
            draggingUp = true;
            if (shouldScale) setPivotY(0f);
        }
        // how far have we dragged relative to the distance to perform a dismiss
        // (0–1 where 1 = dismiss distance). Decreasing logarithmically as we approach the limit
        float dragFraction = (float) Math.log10(1 + (Math.abs(totalDrag) / dragDismissDistance));

        // calculate the desired translation given the drag fraction
        float dragTo = dragFraction * dragDismissDistance * dragElacticity;

        if (draggingUp) {
            // as we use the absolute magnitude when calculating the drag fraction, need to
            // re-apply the drag direction
            dragTo *= -1;
        }
        setTranslationY(dragTo);

        if (shouldScale) {
            final float scale = 1 - ((1 - dragDismissScale) * dragFraction);
            setScaleX(scale);
            setScaleY(scale);
        }

        // if we've reversed direction and gone past the settle point then clear the flags to
        // allow the list to get the scroll events & reset any transforms
        if ((draggingDown && totalDrag >= 0)
                || (draggingUp && totalDrag <= 0)) {
            totalDrag = dragTo = dragFraction = 0;
            draggingDown = draggingUp = false;
            setTranslationY(0f);
            setScaleX(1f);
            setScaleY(1f);
        }
        dispatchDragCallback(dragFraction, dragTo,
                Math.min(1f, Math.abs(totalDrag) / dragDismissDistance), totalDrag);
    }

Demo下载

你可能感兴趣的:(自定义view)