在前一篇博文中已经实现过一个仿魅族flyme6应用市场应用详情弹出式layout: Android自定义控件:从零开始实现魅族flyme6应用市场应用详情弹出式layout,主要是通过viewDragHelper来实现,大部分效果算是实现了,但是在最后还是有一些bug。
趁着这段时间工作比较轻松一点,这次再通过NestedScrolling来实现一次这个自定义控件,对比前面的实现方法,通过NestedScrolling实现起来会简单许多。
老规矩,先看看最终要实现的效果图:
NestedScrolling
NestedScrolling是个啥玩意呢?这是Google官方从5.0后引入的滑动嵌套解决方案。
看效果图看的出来,这次我们要实现的效果的难点就在嵌套滑动,因为手指放到scrollview中,然后实际滚动的是却外部的ViewGroup,在ViewGroup滚动到顶部的时候呢,内部的Scrollview又继续滚动。按照传统的View事件拦截和处理方式,那首先要保证ViewGroup拦截事件,否则事件会被内部的scrollview消费掉。但是如果拦截了,当ViewGroup滚动到顶部的时候又如何让scrollview又持续滑动呢?按照传统的方式,一次事件拦截就是一次性处理的事情,ViewGroup如果拦截了这次滑动事件,那么scrollview肯定是没法继续处理这次滑动事件的。
我们上篇博文是通过事件拦截和分发人为的在ViewGroup中更动态的修改scrollView的滑动,从视觉上实现一次滑动事件ViewGroup和子view嵌套的滚动效果。实际上从本质上来讲,还是ViewGroup拦截和消费了事件,第一次ViewGroup中的事件并没有到子view中去处理。
那么NestedScrolling如何实现嵌套滑动呢?
NestedScrollingParent内部实现了NestedScrollingChild接口的子View会优先获得事件处理权,然后滑动的时候,会先将dx、dy传入给NestedScrollingParent,NestedScrollingParent可以决定是否对其进行消耗,也就是说NestedScrollingParent可以消费部分dx、dy,余下的未消费完的dx、dy交还给子view去消费。
这样看实际上要实现本次的效果就很简单了,话不多说,贴代码。
先让我们的自定义ScrollView实现NestedScrollingChild接口,并且将NestedScrolling相关的处理全部交给ScrollingChildHelper处理。
public class MyScrollView extends ScrollView implements NestedScrollingChild{
private boolean isScrollToTop = true;
private boolean isScrollToBottom = false;
private OnScrollLimitListener mOnScrollLimitListener;
private NestedScrollingChildHelper mScrollingChildHelper;
public MyScrollView(Context context) {
this(context, null);
}
public MyScrollView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@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);
}
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
return mScrollingChildHelper;
}
/**
* 设置ScrollView滑动到边界监听
*
* @param onScrollLimitListener ScrollView滑动到边界监听
*/
public void setOnScrollLimitListener(OnScrollLimitListener onScrollLimitListener) {
mOnScrollLimitListener = onScrollLimitListener;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (getScrollY() == 0) {//滑动到顶部
isScrollToTop = true;
isScrollToBottom = false;
isScrollToBottom = false;
} else if (getScrollY() + getHeight() - getPaddingTop() - getPaddingBottom() ==
getChildAt(0).getHeight()) {
// 小心踩坑: 这里不能是 >=
// 小心踩坑:这里最容易忽视的就是ScrollView上下的padding
isScrollToTop = false;
isScrollToBottom = true;
} else {
isScrollToTop = false;
isScrollToBottom = false;
}
notifyScrollChangedListeners();
}
/**
* 回调
*/
private void notifyScrollChangedListeners() {
if (isScrollToTop) {
if (mOnScrollLimitListener != null) {
mOnScrollLimitListener.onScrollTop();
}
} else if (isScrollToBottom) {
if (mOnScrollLimitListener != null) {
mOnScrollLimitListener.onScrollBottom();
}
} else {
if (mOnScrollLimitListener != null) {
mOnScrollLimitListener.onScrollOther();
}
}
}
/**
* scrollview滑动到边界监听接口
*/
public interface OnScrollLimitListener {
/**
* 滑动到顶部
*/
void onScrollTop();
/**
* 滑动到顶部和底部之间的位置(既不是顶部也不是底部)
*/
void onScrollOther();
/**
* 滑动到底部
*/
void onScrollBottom();
}
}
然后是我们的PopupLayout,上一篇博文是通过自定义FrameLayout的方式实现的,这次由于是通过NestedScrolling实现,所以一次滑动事件其实是针对整个ViewGroup的,所以本次采取自定义LinearLayout的方式去实现。
在这里我们重点看下面几个方法,首先是onMeasure方法。因为初始状态下ContentView是在界面之外的,所以要确定ContentView的高度。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.e("tag", "onMeasure");
ViewGroup.LayoutParams params = contentView.getLayoutParams();
params.height = darkView.getMeasuredHeight() - mOrginY;
setMeasuredDimension(getMeasuredWidth(), contentView.getMeasuredHeight() + darkView
.getMeasuredHeight());
}
接下来看看重写的NestedScrollingParent几个方法。
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
Log.e(TAG, "onStartNestedScroll");
return true;
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
Log.e(TAG, "onNestedScrollAccepted");
}
@Override
public void onStopNestedScroll(View target) {
Log.e(TAG, "onStopNestedScroll");
if (mDarkViewHeight - mOrginY - getScrollY() > mDragRange) {//向下拖拽,超出拖拽限定距离
dismiss();
} else if (mDarkViewHeight - mOrginY - getScrollY() > 0) {//向下拖拽,但是没有超出拖拽限定距离
springback();
}
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int
dyUnconsumed) {
Log.e(TAG, "onNestedScroll");
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
boolean patchDown = dy < 0 && mIsScrollInTop;//下滑
boolean patchUp = dy > 0 && getScrollY() < (mDarkViewHeight - UIUtils.getStatusBarHeight
(target));//上滑
if (patchDown || patchUp) {
scrollBy(0, dy);
consumed[1] = dy;
}
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return true;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
//不做拦截 可以传递给子View
return false;
}
@Override
public int getNestedScrollAxes() {
Log.e(TAG, "getNestedScrollAxes");
return 0;
}
onNestedPreScroll中,我们判断,如果是上滑且contentView未滑动到顶部,则消耗掉dy,即consumed[1]=dy。如果是下滑且内部scrollview已经滑动到顶,则消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去执行scrollBy,实际上就是滑动PopupLayout本身。
onStopNestedScroll中,我们判断向下滑动的距离,来确定是dismiss PopupLayout还是回弹到初始位置。
最后由于需要更新TitleBar的状态,所以重写了scrollTo方法,在scrollTo方法中更新TitleBar的状态。
@Override
public void scrollTo(int x, int y) {
if (y >= mDarkViewHeight - UIUtils.getStatusBarHeight(this)) {
y = mDarkViewHeight - UIUtils.getStatusBarHeight(this);
darkView.setBackgroundColor(Color.WHITE);//拖动到顶部时darkview背景设置白色
titleBar.setBackImageResource(R.mipmap.back);
} else {
darkView.setBackgroundResource(R.color.dark);//没有拖动到顶部时darkview背景设置暗色
titleBar.setBackImageResource(R.mipmap.close);
}
if (y != getScrollY()) {
super.scrollTo(x, y);
}
}
本次的要点基本就这么多,总的来说相较上一篇博文各种绞尽脑汁想着事件处理,这次通过NestedScrolling就重写几个方法,然后根据自己的实际需求做一些判断,实现起来还是很简单的。