在前一篇博文中已經(jīng)實(shí)現(xiàn)過一個仿魅族flyme6應(yīng)用市場應(yīng)用詳情彈出式layout: Android自定義控件:從零開始實(shí)現(xiàn)魅族flyme6應(yīng)用市場應(yīng)用詳情彈出式layout琼娘,主要是通過viewDragHelper來實(shí)現(xiàn)渠欺,大部分效果算是實(shí)現(xiàn)了肥惭,但是在最后還是有一些bug。
趁著這段時間工作比較輕松一點(diǎn)迈螟,這次再通過NestedScrolling來實(shí)現(xiàn)一次這個自定義控件弄息,對比前面的實(shí)現(xiàn)方法,通過NestedScrolling實(shí)現(xiàn)起來會簡單許多。
老規(guī)矩狸涌,先看看最終要實(shí)現(xiàn)的效果圖:
NestedScrolling
NestedScrolling是個啥玩意呢切省?這是Google官方從5.0后引入的滑動嵌套解決方案。
看效果圖看的出來帕胆,這次我們要實(shí)現(xiàn)的效果的難點(diǎn)就在嵌套滑動朝捆,因?yàn)槭种阜诺絪crollview中,然后實(shí)際滾動的是卻外部的ViewGroup懒豹,在ViewGroup滾動到頂部的時候呢芙盘,內(nèi)部的Scrollview又繼續(xù)滾動。按照傳統(tǒng)的View事件攔截和處理方式脸秽,那首先要保證ViewGroup攔截事件儒老,否則事件會被內(nèi)部的scrollview消費(fèi)掉。但是如果攔截了记餐,當(dāng)ViewGroup滾動到頂部的時候又如何讓scrollview又持續(xù)滑動呢驮樊?按照傳統(tǒng)的方式,一次事件攔截就是一次性處理的事情片酝,ViewGroup如果攔截了這次滑動事件囚衔,那么scrollview肯定是沒法繼續(xù)處理這次滑動事件的。
我們上篇博文是通過事件攔截和分發(fā)人為的在ViewGroup中更動態(tài)的修改scrollView的滑動雕沿,從視覺上實(shí)現(xiàn)一次滑動事件ViewGroup和子view嵌套的滾動效果练湿。實(shí)際上從本質(zhì)上來講,還是ViewGroup攔截和消費(fèi)了事件审轮,第一次ViewGroup中的事件并沒有到子view中去處理肥哎。
那么NestedScrolling如何實(shí)現(xiàn)嵌套滑動呢?
NestedScrollingParent內(nèi)部實(shí)現(xiàn)了NestedScrollingChild接口的子View會優(yōu)先獲得事件處理權(quán)疾渣,然后滑動的時候篡诽,會先將dx、dy傳入給NestedScrollingParent榴捡,NestedScrollingParent可以決定是否對其進(jìn)行消耗霞捡,也就是說NestedScrollingParent可以消費(fèi)部分dx、dy薄疚,余下的未消費(fèi)完的dx碧信、dy交還給子view去消費(fèi)。
這樣看實(shí)際上要實(shí)現(xiàn)本次的效果就很簡單了街夭,話不多說砰碴,貼代碼。
先讓我們的自定義ScrollView實(shí)現(xiàn)NestedScrollingChild接口板丽,并且將NestedScrolling相關(guān)的處理全部交給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;
}
/**
* 設(shè)置ScrollView滑動到邊界監(jiān)聽
*
* @param onScrollLimitListener ScrollView滑動到邊界監(jiān)聽
*/
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();
}
/**
* 回調(diào)
*/
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滑動到邊界監(jiān)聽接口
*/
public interface OnScrollLimitListener {
/**
* 滑動到頂部
*/
void onScrollTop();
/**
* 滑動到頂部和底部之間的位置(既不是頂部也不是底部)
*/
void onScrollOther();
/**
* 滑動到底部
*/
void onScrollBottom();
}
}
然后是我們的PopupLayout趁尼,上一篇博文是通過自定義FrameLayout的方式實(shí)現(xiàn)的,這次由于是通過NestedScrolling實(shí)現(xiàn)猖辫,所以一次滑動事件其實(shí)是針對整個ViewGroup的酥泞,所以本次采取自定義LinearLayout的方式去實(shí)現(xiàn)。
在這里我們重點(diǎn)看下面幾個方法啃憎,首先是onMeasure方法芝囤。因?yàn)槌跏紶顟B(tài)下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。如果是下滑且內(nèi)部scrollview已經(jīng)滑動到頂谆甜,則消耗掉dy垃僚,即consumed[1]=dy,消耗掉的意思店印,就是自己去執(zhí)行scrollBy,實(shí)際上就是滑動PopupLayout本身倒慧。
onStopNestedScroll中按摘,我們判斷向下滑動的距離,來確定是dismiss PopupLayout還是回彈到初始位置纫谅。
最后由于需要更新TitleBar的狀態(tài)炫贤,所以重寫了scrollTo方法,在scrollTo方法中更新TitleBar的狀態(tài)付秕。
@Override
public void scrollTo(int x, int y) {
if (y >= mDarkViewHeight - UIUtils.getStatusBarHeight(this)) {
y = mDarkViewHeight - UIUtils.getStatusBarHeight(this);
darkView.setBackgroundColor(Color.WHITE);//拖動到頂部時darkview背景設(shè)置白色
titleBar.setBackImageResource(R.mipmap.back);
} else {
darkView.setBackgroundResource(R.color.dark);//沒有拖動到頂部時darkview背景設(shè)置暗色
titleBar.setBackImageResource(R.mipmap.close);
}
if (y != getScrollY()) {
super.scrollTo(x, y);
}
}
本次的要點(diǎn)基本就這么多兰珍,總的來說相較上一篇博文各種絞盡腦汁想著事件處理,這次通過NestedScrolling就重寫幾個方法询吴,然后根據(jù)自己的實(shí)際需求做一些判斷掠河,實(shí)現(xiàn)起來還是很簡單的。