前段时间公司要求实现类似小米的时钟的闹钟列表可以上滑下滑的效果,我当时的第一反应就是使用 CoordinatorLayout ,但后来使用发现一些问题,CoordinatorLayout 收缩的时候必须是一个 ToolBar ,而小米的时钟收缩上去是一个数字时钟,无奈之下只好自己重写。
经过大量的 google 百度,我了解到 NestedScrollingParent 和 NestedScrollingChild 可以实现类似 CoordinatorLayout 的效果,NestedScrollingParent 和 NestedScrollingChild 都是接口,因此必须自定义 ViewGroup 来实现它们,庆幸的是 RecyclerView 内部已经实现了 NestedScrollingChild ,因此我们只需要自定义一个ViewGroup 来实现 NestedScrollingParent 即可,然后和 Recyclerview 结合起来使用。
问题的关键就是一开始拖动下面的 Recyclerview 向上滑动的时候,Recyclerview 内部是不动的,而是整体先向上移动到一定位置后才开始内部滑动, 然后向下滑动的时候先把 Recyclerview 滑动到内部 item 的最上端,然后再整体向下滑动到原来的位置,整体滑动的逻辑是要自己写的,有很多方法都可以实现,但是却很坑。
最初我使用的方法是利用 layout 方法来控制 Recyclerview 的整体上滑和下滑,但是有一个问题,滑上去之后,只要这个整体的 ViewGroup 有 ui 更新,它就会自动掉下来,至今我也不明白为什么会这样,这样根本无法达到想要的效果。
然后我发现可以更改 Recyclerview 上面的 View 的高度,缩短它的高度,然后 Recyclerview 就会顶上去,视觉效果和移动上去是一样的,而且这样不会掉下来,但是这样又有一个问题,那就是滑动过程中会发生抖动,经过无数次的尝试,我发现抖动是由于滑动的时候判断手势方向的一个参数 dy 在不停的发送正负变化,即使是一直朝着同一个方向滑动,它也会不停的发生正负变化,而且这种变化貌似毫无规律。
然后我就想要不这样吧,先用layout 方法让 Recyclerview 滑上去,当手指松开的时候,再滑回来,同时缩小上面那个 View 的高度,由于过程进行的很快,肉眼根本看不出来,从而可以达到 Recyclerview 画上去了的视觉效果,这个方法应该是可行的,但是这样会造成一个问题,那就是要考虑太多的情况,逻辑异常的复杂。
后来偶然间我发现直接使用 Recyclerview 的 settranslationY 方法就可以达到想要的效果,不会滑上去之后随着 ui 更新而掉下来,也不会产生抖动,这样问题就轻松解决了。
下面贴出代码:
public class DragLayout extends LinearLayout implements NestedScrollingParent {
private NestedScrollingParentHelper nsph;
//分别表示RecyclerView上面那个View(是个ViewGroup),Recyclerview,收缩之后上面显示的View,展开之后上面显示的View
private View topView, bottomView, smallView, bigView;
public DragLayout(Context context) {
this(context, null);
}
public DragLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
//onFinishInflate方法中获得各个子view
@Override
protected void onFinishInflate() {
super.onFinishInflate();
topView = getChildAt(0);
bottomView = getChildAt(1);
smallView = ((ViewGroup) topView).getChildAt(0);
bigView = ((ViewGroup) topView).getChildAt(1);
smallView.setAlpha(0);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//重新设置Recyclerview的高度,保证Recyclerview的高度和收缩之后上面显示的View的高度之和等于整个ViewGroup的高度
LayoutParams params = (LayoutParams) bottomView.getLayoutParams();
int bottomHeight = getHeight() - smallView.getHeight();
if (bottomHeight != 0) {
params.height = bottomHeight;
bottomView.setLayoutParams(params);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
Log.d("TAG", "onStartNestedScroll");
return true;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
nsph.onNestedScrollAccepted(child, target, axes);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
//满足两种情况是可以滑动,第一是向上滑并且滑动的高度小于可滑动的最大值
//第二种是向下滑动,并且滑动的高度大于0
if ((dy > 0 && (bottomView.getTranslationY() > smallView.getHeight() - topView.getHeight())
&& ((RecyclerView) bottomView).getChildCount() > 0) //如果RecyclerView为空,则不能向上滑动
|| (dy < 0 && bottomView.getTranslationY() < 0
//判断RecyclerView内部是否可以向下滑动,如果可以,则Recyclerview无法整体向下滑动
&& !ViewCompat.canScrollVertically(target, - 1))) {
if (count == 0) {
moveDir = dy > 0 ? 1 : 2;
}
consumed[1] = dy;
Log.d("TAG", "dy = " + dy);
bottomView.setTranslationY(bottomView.getTranslationY() - dy);
Log.d("TAG", "translationY = " + bottomView.getTranslationY());
bigView.setAlpha((smallView.getHeight() - topView.getHeight() - 2 * bottomView.getTranslationY())
/ (smallView.getHeight() - topView.getHeight()));
smallView.setAlpha((2 * bottomView.getTranslationY()
- (smallView.getHeight() - topView.getHeight())) / (smallView.getHeight() - topView.getHeight()));
}
}
@Override
public void onStopNestedScroll(View child) {
nsph.onStopNestedScroll(child);
float stopValue = 0;
//手指松开时判断当前的位置,大于一半向上移到最高点,小于一半向下移到原始位置
if (bottomView.getTranslationY() > (smallView.getHeight() - topView.getHeight()) / 2) {
stopValue = 0;
} else {
stopValue = smallView.getHeight() - topView.getHeight();
}
ObjectAnimator bottomAnimator = ObjectAnimator.ofFloat(bottomView, "translationY", bottomView.getTranslationY(), stopValue);
ObjectAnimator smallAnimator = ObjectAnimator.ofFloat(smallView, "alpha",
smallView.getAlpha(), stopValue / (smallView.getHeight() - topView.getHeight()));
ObjectAnimator bigAnimator = ObjectAnimator.ofFloat(bigView, "alpha", bigView.getAlpha(),
(smallView.getHeight() - topView.getHeight() - stopValue) / (smallView.getHeight() - topView.getHeight()));
AnimatorSet set = new AnimatorSet();
set.play(bottomAnimator).with(smallAnimator).with(bigAnimator);
set.start();
}
private void initView() {
nsph = new NestedScrollingParentHelper(this);
moveDir = 0;
count = 0;
}
}