新建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
设置adapter
和layoutManager
,MyAdapter
的代码这里就不贴了。
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));
}
}
现在可以滚动了,但是发现在控制台NestScrolling
4个方法中只有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
方法各个参数的意思
@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
方法
@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
滑动到边缘,不能在滑动了,这时候onNestedPreScroll
的dy
等于滑动的距离,因为这时候RecyclerView
不能再滑动,所以onNestedScroll
的dyConsumed
等于0(RecyclerView
没有消费距离),而dxUnconsumed
等于onNestedPreScroll
的dy
。
因为想实现的RecyclerView
滚动到边缘之后,能再拖拽一段距离并伴随这缩放,所以在onNestedScroll
处理dyUnconsumed
(注意这里是dyUnconsumed
)。
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
dragScale(dyUnconsumed);
}
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);
}