本文已在公衆號鴻洋原創發佈。未經許可,不得以任何形式轉載!java
以前的《淺析NestedScrolling嵌套滑動機制之基礎篇》帶你們瞭解NestedScrolling的原理和使用還有它的改進等等,這篇文章手把手基於NestedScrolling嵌套滑動機制現實餓了麼v8.27.6商家詳情頁。github地址:github.com/pengguanmin…android
須要仿寫的有兩部分,第一個是商家頁的嵌套滑動,第二個是商家點餐頁的嵌套滑動。 git
apk下載地址: github 百度雲 密碼:jm39topBarHeight;//topBar高度
contentTransY;//滑動內容初始化TransY
upAlphaScaleY;//上滑時logo,收藏icon縮放、搜索icon、分享icon透明度臨界值
upAlphaGradientY;//上滑時搜索框、topBar背景,返回icon、拼團icon顏色漸變臨界值
downFlingCutOffY;//從摺疊狀態下滑產生fling時回彈到初始狀態的最大值
downCollapsedAlphaY;//下滑時摺疊內容透明度臨界值
downShopBarTransY;//下滑時購物內容位移臨界值
downContentAlphaY;//下滑時收起按鈕和滑動內容透明度臨界值
ivCloseHeight;//收起按鈕的高度
content部分的上滑範圍=[topBarHeight,contentTransY]
content部分的下滑範圍=[contentTransY,滿屏高度-ivCloseHeight]
複製代碼
舉個根據Content部分的TransitionY計算百分比例子(請注意:根據Android的座標系contentTransY>upAlphaScaleY):github
/** *計算上滑時logo,收藏icon縮放、搜索icon、分享icon透明度的百分比其範圍控制在[0.0f,1.0f], 而Content部分的TranslationY值控制在[upAlphaScaleY,contentTransY],v4包的MathUtils類的clamp方法能夠幫助控制值的範圍,超過範圍時百分下限爲0,上限爲1。 */
upAlphaScalePro= (contentTransY - MathUtils.clamp(Content部分的TranslationY, upAlphaScaleY,
contentTransY)) / (contentTransY-upAlphaScaleY);
//根據upAlphaScalePro設置icon的透明度
float alpha = 1 - upAlphaScalePro;
setAlpha(mIvShare, alpha);
複製代碼
商家頁的嵌套滑的自定義View繼承FrameLayout,命名爲ElemeNestedScrollLayout,在Content部分可以滑動的View只有ViewPager裏Fragment的RecyclerView和NestedScrollView,它們都實現了NestedScrollingChild2接口,故自定義View要實現NestedScrollingParent2接口根據具體邏輯來消費NestedScrollingChild2的滑動值。app
下面是佈局要點,側重於控件的尺寸和位置,完整佈局請參考:ElemeNestedScrollLayout佈局ide
<ElemeNestedScrollLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!--Header部分-->
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/header_height"
... >
<!--Header部分-->
<!--Collasp Content部分-->
<<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
... >
<!--Collasp Content部分-->
</LinearLayout>
<!--Top Bar部分-->//自定義View,其做用是在摺疊狀態時適配沉浸式狀態欄效果。
<TopBarLayout
android:layout_width="match_parent"
android:layout_height="@dimen/top_bar_height"
...>
<!--Top Bar部分-->
<!--Content部分-->
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:translationY="@dimen/content_trans_y">
<com.flyco.tablayout.SlidingTabLayout
android:layout_width="match_parent"
android:layout_height="@dimen/stl_height"/>
<android.support.v4.view.ViewPager
android:layout_width="match_parent"
android:layout_height="0dp"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="@dimen/iv_close_height"/>
</android.support.constraint.ConstraintLayout>
<!--Content部分-->
<!--Shop Bar部分-->
<android.support.constraint.ConstraintLayout
android:layout_gravity="bottom"
android:layout_width="match_parent"
android:layout_height="@dimen/shop_bar_height"
... >
<!--Shop Bar部分-->
</ElemeNestedScrollLayout>
複製代碼
從上面圖片可以分析出:摺疊狀態時,Content部分高度=滿屏高度-TopBar部分的高度佈局
public class ElemeNestedScrollLayout extends FrameLayout implements NestedScrollingParent2 {
//Header部分
private View mIvLogo;
private View mVCollect;
//Collaps Content部分
private View mClCollapsedHeader;
private View mCollapsedContent;
private View mRvCollapsed;
//TopBar部分
private View mIvSearch;
private View mIvShare;
private View mTvSearch;
private View mTopBar;
private ImageView mIvBack;
private ImageView mIvAssemble;
//Content部分
private View mLlContent;
private View mIvClose;
private View mViewPager;
private View mStl;
//ShopBar部分
private View mShopBar;
private float topBarHeight;//topBar高度
private float shopBarHeight;//shopBar高度
private float contentTransY;//滑動內容初始化TransY
private float upAlphaScaleY;//上滑時logo,收藏icon縮放、搜索icon、分享icon透明度臨界值
private float upAlphaGradientY;//上滑時搜索框、topBar背景,返回icon、拼團icon顏色漸變臨界值
private float downFlingCutOffY;//從摺疊狀態下滑產生fling時回彈到初始狀態的臨界值
private float downCollapsedAlphaY;//下滑時摺疊內容透明度臨界值
private float downShopBarTransY;//下滑時購物內容位移臨界值
private float downContentAlphaY;//下滑時收起按鈕和滑動內容透明度臨界值
private float downEndY;//下滑時終點值
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mLlContent = findViewById(R.id.cl_content);
mCollapsedContent = findViewById(R.id.cl_collapsed_content);
mIvSearch = findViewById(R.id.iv_search);
mIvShare = findViewById(R.id.iv_share);
mIvBack = findViewById(R.id.iv_back);
mIvAssemble = findViewById(R.id.iv_assemble);
mIvLogo = findViewById(R.id.iv_logo);
mVCollect = findViewById(R.id.iv_collect);
mTvSearch = findViewById(R.id.tv_search);
mTopBar = findViewById(R.id.cl_top_bar);
mClCollapsedHeader = findViewById(R.id.cl_collapsed_header);
mRvCollapsed = findViewById(R.id.rv_collasped);
mIvClose = findViewById(R.id.iv_close);
mViewPager = findViewById(R.id.vp);
mStl = findViewById(R.id.stl);
mShopBar = findViewById(R.id.cl_shop_bar);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//設置Content部分高度
topBarHeight= mTopBar.getMeasuredHeight();
ViewGroup.LayoutParams params = mLlContent.getLayoutParams();
params.height = (int) (getMeasuredHeight() - topBarHeight);
//從新測量
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
shopBarHeight = getResources().getDimension(R.dimen.shop_bar_height);
contentTransY = getResources().getDimension(R.dimen.content_trans_y);
downShopBarTransY = contentTransY+ shopBarHeight;
upAlphaScaleY = contentTransY - dp2px(32);
upAlphaGradientY = contentTransY - dp2px(64);
downFlingCutOffY = contentTransY + dp2px(28);
downCollapsedAlphaY = contentTransY + dp2px(32);
downContentAlphaY = getResources().getDimension(R.dimen.donw_content_alpha_y);
downEndY = getHeight() - getResources().getDimension(R.dimen.iv_close_height);
}
}
複製代碼
ElemeNestedScrollLayout只處理Content部分裏可滑動View的垂直方向的滑動。post
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
return child.getId() == mLlContent.getId()&&
axes==ViewCompat.SCROLL_AXIS_VERTICAL;
}
複製代碼
接下來就是處理滑動,上面效果分析提過:Content部分的上滑範圍=[topBarHeight,contentTransY]、 下滑範圍=[contentTransY,downEndY(即滿屏高度-ivCloseHeight)]即滑動範圍爲[topBarHeight,downEndY];ElemeNestedScrollLayout要控制Content部分的TransitionY值要在範圍內,具體處理以下。字體
Content部分裏可滑動View往上滑動時:動畫
一、若是Content部分當前TransitionY+View滑動的dy > topBarHegiht,ElemeNestedScrollLayout設置Content部分的TransitionY爲Content部分當前TransitionY+View滑動的dy達到移動的效果來消費View的dy。
二、若是Content部分當前TransitionY+View滑動的dy = topBarHegiht,ElemeNestedScrollLayout同上操做。
三、若是Content部分當前TransitionY+View滑動的dy < topBarHegiht,ElemeNestedScrollLayout只消費部分dy(即Content部分當前TransitionY到topBarHeight差值),剩餘的dy讓View滑動消費。
Content部分裏可滑動View往下滑動而且View已經不能往下滑動 (好比RecyclerView已經到頂部還往下滑)時:
一、若是Content部分當前TransitionY+View滑動的dy >= topBarHeight 而且 Content部分當前TransitionY+View滑動的dy <= downEndY,ElemeNestedScrollLayout設置Content部分的TransitionY爲Content部分當前TransitionY+View滑動的dy達到移動的效果來消費View的dy。
二、Content部分當前TransitionY+View滑動的dy > downEndY,ElemeNestedScrollLayout只消費部分dy(即Content部分當前TransitionY到downEndY差值)。
下面是代碼實現:
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
float contentTransY = mLlContent.getTranslationY() - dy;
//處理上滑
if (dy > 0) {
if (contentTransY >= topBarHeight) {
translationByConsume(mLlContent, contentTransY, consumed, dy);
} else {
translationByConsume(mLlContent, topBarHeight, consumed, (mLlContent.getTranslationY() - topBarHeight));
}
}
if (dy < 0 && !target.canScrollVertically(-1)) {
//處理下滑
if (contentTransY >= topBarHeight && contentTransY <= downEndY) {
translationByConsume(mLlContent, contentTransY, consumed, dy);
} else {
translationByConsume(mLlContent, downEndY, consumed, downEndY - mLlContent.getTranslationY());
}
}
}
private void translationByConsume(View view, float translationY, int[] consumed, float consumedDy) {
consumed[1] = (int) consumedDy;
view.setTranslationY(translationY);
}
複製代碼
就這樣處理好了Content部分的滑動,接下來就處理Content部分的滑動過程當中各類View的效果並對外暴露百分比監聽接口,好比上滑摺疊過程改變狀態欄字體就須要用到。下面的範圍值是針對Content部分的TransitionY,根據以前效果分析具體以下:
一、設置logo、收藏icon縮放,搜索icon、分享icon透明度範圍[upAlphaScaleY,contentTransY]
二、設置搜索框、topBar背景,返回icon、拼團icon顏色漸範圍[upAlphaGradientY,contentTransY]
三、設置Collapse Content部分透明度範圍[contentTransY,downCollapsedAlphaY]
四、設置Shop Bar位移範圍[contentTransY,downShopBarTransY]
五、設置收起按鈕和滑動內容透明度[downContentAlphaY,downEndY]
下面是代碼實現:
...
private ArgbEvaluator iconArgbEvaluator;//返回icon、拼團icon顏色漸變的Evaluator
private ArgbEvaluator topBarArgbEvaluator;//topbar顏色漸變的Evaluator
public ElemeNestedScrollLayout(@NonNull Context context) {
this(context, null);
}
public ElemeNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, -1);
}
public ElemeNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mParentHelper = new NestedScrollingParentHelper(this);
iconArgbEvaluator = new ArgbEvaluator();
topBarArgbEvaluator = new ArgbEvaluator();
...
}
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
...
//根據upAlphaScalePro,設置logo、收藏icon縮放,搜索icon、分享icon透明度
float upAlphaScalePro = getUpAlphaScalePro();
alphaScaleByPro(upAlphaScalePro);
//根據upAlphaGradientPro,設置topBar背景、返回icon、拼團icon顏色漸變值,搜索框透明度
float upAlphaGradientPro = getUpAlphaGradientPro();
alphaGradientByPro(upAlphaGradientPro);
//根據downCollapsedAlphaPro,設置摺疊內容透明度
float downCollapsedAlphaPro = getDownCollapsedAlphaPro();
alphaCollapsedContentByPro(downCollapsedAlphaPro);
//根據downContentAlphaPro,設置滑動內容、收起按鈕的透明度
float downContentAlphaPro = getDownContentAlphaPro();
alphaContentByPro(downContentAlphaPro);
//根據downShopBarTransPro,設置購物內容內容位移
float downShopBarTransPro = getDownShopBarTransPro();
transShopBarByPro(downShopBarTransPro);
//根據upCollapsedContentTransPro,設置摺疊內容位移
float upCollapsedContentTransPro = getUpCollapsedContentTransPro();
transCollapsedContentByPro(upCollapsedContentTransPro);
if (mProgressUpdateListener!=null){
mProgressUpdateListener.onUpAlphaScaleProUpdate(upAlphaScalePro);
mProgressUpdateListener.onUpAlphaGradientProUpdate(upAlphaGradientPro);
mProgressUpdateListener.onDownCollapsedAlphaProUpdate(downCollapsedAlphaPro);
mProgressUpdateListener.onDownContentAlphaProUpdate(downContentAlphaPro);
mProgressUpdateListener.onDownShopBarTransProUpdate(downShopBarTransPro);
mProgressUpdateListener.onUpCollapsedContentTransProUpdate(upCollapsedContentTransPro);
}
}
/** * 根據upCollapsedContentTransPro,設置摺疊內容位移 */
private void transCollapsedContentByPro(float upCollapsedContentTransPro) {
float collapsedContentTranY = - (upCollapsedContentTransPro * (contentTransY - topBarHeight));
translation(mCollapsedContent,collapsedContentTranY);
}
/** * 根據downShopBarTransPro,設置購物內容內容位移 */
private void transShopBarByPro(float downShopBarTransPro) {
float shopBarTransY = (1-downShopBarTransPro) * shopBarHeight;
translation(mShopBar,shopBarTransY);
}
/** * 根據downContentAlphaPro,設置滑動內容、收起按鈕的透明度 */
private void alphaContentByPro(float downContentAlphaPro) {
setAlpha(mViewPager,downContentAlphaPro);
setAlpha(mStl,downContentAlphaPro);
setAlpha(mIvClose,1-downContentAlphaPro);
if (mIvClose.getAlpha()==0){
mIvClose.setVisibility(GONE);
}else {
mIvClose.setVisibility(VISIBLE);
}
}
/** * 根據downCollapsedAlphaPro,設置摺疊內容透明度 */
private void alphaCollapsedContentByPro(float downCollapsedAlphaPro) {
setAlpha(mClCollapsedHeader,downCollapsedAlphaPro);
setAlpha(mRvCollapsed,1 - downCollapsedAlphaPro);
}
/** * 根據upAlphaGradientPro,設置topBar背景、返回icon、拼團icon顏色漸變值,搜索框透明度 */
private void alphaGradientByPro(float upAlphaGradientPro) {
setAlpha(mTvSearch, upAlphaGradientPro);
int iconColor = (int) iconArgbEvaluator.evaluate(
upAlphaGradientPro,
getContext().getResources().getColor(R.color.white),
getContext().getResources().getColor(R.color.black)
);
int topBarColor = (int) topBarArgbEvaluator.evaluate(
upAlphaGradientPro,
getContext().getResources().getColor(R.color.trans_white),
getContext().getResources().getColor(R.color.white)
);
mTopBar.setBackgroundColor(topBarColor);
mIvBack.getDrawable().mutate().setTint(iconColor);
mIvAssemble.getDrawable().mutate().setTint(iconColor);
}
/** * 根據upAlphaScalePro,設置logo、收藏icon縮放,搜索icon、分享icon透明度 */
private void alphaScaleByPro(float upAlphaScalePro) {
float alpha = 1 - upAlphaScalePro;
float scale = 1 - upAlphaScalePro;
setAlpha(mIvSearch, alpha);
setAlpha(mIvShare, alpha);
setScaleAlpha(mIvLogo, scale, scale, alpha);
setScaleAlpha(mVCollect, scale, scale, alpha);
}
private float getDownContentAlphaPro() {
return (downEndY - MathUtils.clamp(mLlContent.getTranslationY(), downContentAlphaY, downEndY)) / (downEndY - downContentAlphaY);
}
private float getDownCollapsedAlphaPro() {
return (downCollapsedAlphaY - MathUtils.clamp(mLlContent.getTranslationY(), contentTransY, downCollapsedAlphaY)) / (downCollapsedAlphaY -contentTransY);
}
private float getDownShopBarTransPro() {
return (downShopBarTransY - MathUtils.clamp(mLlContent.getTranslationY(), contentTransY, downShopBarTransY)) / (downShopBarTransY -contentTransY);
}
private float getUpAlphaGradientPro() {
return (upAlphaScaleY - MathUtils.clamp(mLlContent.getTranslationY(), upAlphaGradientY, upAlphaScaleY)) / (upAlphaScaleY-upAlphaGradientY);
}
private float getUpAlphaScalePro() {
return (contentTransY - MathUtils.clamp(mLlContent.getTranslationY(), upAlphaScaleY, contentTransY)) / (contentTransY-upAlphaScaleY);
}
private float getUpCollapsedContentTransPro() {
return (contentTransY - MathUtils.clamp(mLlContent.getTranslationY(), topBarHeight, contentTransY)) / (contentTransY-topBarHeight);
}
private void setAlpha(View view, float alpha){
view.setAlpha(alpha);
}
private void setScale(View view ,float scaleY,float scaleX){
view.setScaleY(scaleY);
view.setScaleX(scaleX);
}
private void setScaleAlpha(View view ,float scaleY,float scaleX,float alpha){
setAlpha(view,alpha);
setScale(view,scaleY,scaleX);
}
private void translation(View view, float translationY) {
view.setTranslationY(translationY);
}
public interface ProgressUpdateListener{
void onUpCollapsedContentTransProUpdate(float pro);
void onUpAlphaScaleProUpdate(float pro);
void onUpAlphaGradientProUpdate(float pro);
void onDownCollapsedAlphaProUpdate(float pro);
void onDownContentAlphaProUpdate(float pro);
void onDownShopBarTransProUpdate(float pro);
}
複製代碼
在下滑Content部分從初始狀態轉換到展開狀態的過程當中鬆手就會執行收起或展開的動畫,這邏輯在onStopNestedScroll()實現,但注意若是動畫未執行完畢手指再落下滑動時,應該在onNestedScrollAccepted()取消當前執行中的動畫。
若是Content部分的TransitionY沒有超過downCollapsedAlphaY,執行收起Content部分動畫效果,恢復到初始轉態。 若是Content部分的TransitionY超過了downCollapsedAlphaY,執行展開Content部分動畫效果,轉換到展開轉態。 代碼實現以下:...
public static final long ANIM_DURATION_FRACTION = 200L;
private ValueAnimator restoreOrExpandAnimator;//收起或展開摺疊內容時執行的動畫
public ElemeNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
...
restoreOrExpandAnimator = new ValueAnimator();
restoreOrExpandAnimator.setInterpolator(new AccelerateInterpolator());
restoreOrExpandAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
translation(mLlContent, (float) animation.getAnimatedValue());
//根據downShopBarTransPro,設置購物內容內容位移
float downShopBarTransPro = getDownShopBarTransPro();
transShopBarByPro(downShopBarTransPro);
//根據downCollapsedAlphaPro,設置摺疊內容透明度
float downCollapsedAlphaPro = getDownCollapsedAlphaPro();
alphaCollapsedContentByPro(downCollapsedAlphaPro);
//根據downContentAlphaPro,設置滑動內容、收起按鈕的透明度
float downContentAlphaPro = getDownContentAlphaPro();
alphaContentByPro(downContentAlphaPro);
if (mProgressUpdateListener!=null){
mProgressUpdateListener.onDownCollapsedAlphaProUpdate(downCollapsedAlphaPro);
mProgressUpdateListener.onDownContentAlphaProUpdate(downContentAlphaPro);
mProgressUpdateListener.onDownShopBarTransProUpdate(downShopBarTransPro);
}
}
});
}
@Override
public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int axes, int type) {
mParentHelper.onNestedScrollAccepted(child, target, axes, type);
if (restoreOrExpandAnimator.isStarted()) {
restoreOrExpandAnimator.cancel();
}
}
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
mParentHelper.onStopNestedScroll(target, type);
//若是不是從初始狀態轉換到展開狀態過程觸發返回
if (mLlContent.getTranslationY() <= contentTransY) {
return;
}
//根據百分比計算動畫執行的時長
float downCollapsedAlphaPro = getDownCollapsedAlphaPro();
float downContentAlphaYPro = getDownContentAlphaPro();
if (downCollapsedAlphaPro == 0) {
expand((long) (downContentAlphaYPro * ANIM_DURATION_FRACTION));
} else {
restore((long) (downCollapsedAlphaPro * ANIM_DURATION_FRACTION));
}
}
public void restore(long dur){
if (restoreOrExpandAnimator.isStarted()) {
restoreOrExpandAnimator.cancel();
}
restoreOrExpandAnimator.setFloatValues(mLlContent.getTranslationY(), contentTransY);
restoreOrExpandAnimator.setDuration(dur);
restoreOrExpandAnimator.start();
}
public void expand(long dur){
if (restoreOrExpandAnimator.isStarted()) {
restoreOrExpandAnimator.cancel();
}
restoreOrExpandAnimator.setFloatValues(mLlContent.getTranslationY(), downEndY);
restoreOrExpandAnimator.setDuration(dur);
restoreOrExpandAnimator.start();
}
複製代碼
NestedScrollingParent2處理慣性滑動的方式主要有兩種: 1、在onNestedPreFling()或者onNestedFling()返回值爲true消費掉。 2、在onNestedPreFling()和onNestedFling()返回值都爲false的前提下,在onNestedPreScroll()或者onNestedScroll()消費掉,這種方式能夠和普通的滑動共用邏輯代碼。
場景1:快速往上滑動Content部分的可滑動View產生慣性滑動,這和前面onNestedPreScroll()處理上滑的效果如出一轍,所以能夠複用邏輯,使用第二種方式處理。 場景2:在摺疊狀態並Content部分的可滑動View沒有滑動到頂部盡頭時,快速往下滑動Content部分的可滑動View產生慣性滑動滑到頂部盡頭就中止了,這和前面onNestedPreScroll()處理下滑的效果相似,但多了個慣性滑動滑到頂部盡頭就中止的條件判斷,使用第二種方式處理。 場景3:快速往下滑動Content部分的可滑動View轉化展開狀態產生慣性滑動,這和前面onNestedPreScroll()處理下滑的效果相似,使用第二種方式處理,但注意在慣性滑動沒有徹底消費掉的時候,會不斷觸發onNestedPreScroll()來消費直到慣性滑動徹底消費掉,因此當滑動到展開狀態的時候要中止Content部分的View滑動由於這時已是展開狀態了,不需View繼續滑動觸發onNestedPreScroll(),注意NestedScrollView並無暴露對外中止滑動的方法,只能反射獲取它的OverScroller中止滑動。 下面是代碼實現:@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
float contentTransY = mLlContent.getTranslationY() - dy;
//處理上滑和場景1
if (dy > 0) {
if (contentTransY >= topBarHeight) {
translationByConsume(mLlContent, contentTransY, consumed, dy);
} else {
translationByConsume(mLlContent, topBarHeight, consumed, (mLlContent.getTranslationY() - topBarHeight));
}
}
if (dy < 0 && !target.canScrollVertically(-1)) {
//下滑時處理Fling,徹底摺疊時,下滑Recycler(或NestedScrollView) Fling滾動到列表頂部(或視圖頂部)中止Fling,對應場景2
if (type == ViewCompat.TYPE_NON_TOUCH&&mLlContent.getTranslationY() == topBarHeight) {
stopViewScroll(target);
return;
}
//處理下滑
if (contentTransY >= topBarHeight && contentTransY <= downEndY) {
translationByConsume(mLlContent, contentTransY, consumed, dy);
} else {
translationByConsume(mLlContent, downEndY, consumed, downEndY - mLlContent.getTranslationY());
//對應場景3
if (target instanceof NestedScrollView) {
stopViewScroll(target);
}
}
}
...
}
/** * 中止View的滑動 */
private void stopViewScroll(View target){
if (target instanceof RecyclerView) {
((RecyclerView) target).stopScroll();
}
if (target instanceof NestedScrollView) {
try {
Class<? extends NestedScrollView> clazz = ((NestedScrollView) target).getClass();
Field mScroller = clazz.getDeclaredField("mScroller");
mScroller.setAccessible(true);
OverScroller overScroller = (OverScroller) mScroller.get(target);
overScroller.abortAnimation();
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
複製代碼
場景4:快速往下滑動Content部分的可滑動View,從非摺疊狀態轉化展開狀態產生慣性滑動,由於有回彈效果,因此邏輯處理和onNestedPreScroll()不同,使用第一種方式處理。
...
private ValueAnimator reboundedAnim;//回彈動畫
public ElemeNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
...
reboundedAnim = new ValueAnimator();
reboundedAnim.setInterpolator(new DecelerateInterpolator());
reboundedAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
translation(mLlContent, (float) animation.getAnimatedValue());
//根據upAlphaScalePro,設置logo、收藏icon縮放,搜索icon、分享icon透明度
float upAlphaScalePro = getUpAlphaScalePro();
alphaScaleByPro(upAlphaScalePro);
//根據upAlphaGradientPro,設置topBar背景、返回icon、拼團icon顏色漸變值,搜索框透明度
float upAlphaGradientPro = getUpAlphaGradientPro();
alphaGradientByPro(upAlphaGradientPro);
//根據downCollapsedAlphaPro,設置摺疊內容透明度
float downCollapsedAlphaPro = getDownCollapsedAlphaPro();
alphaCollapsedContentByPro(downCollapsedAlphaPro);
//根據downShopBarTransPro,設置購物內容內容位移
float downShopBarTransPro = getDownShopBarTransPro();
transShopBarByPro(downShopBarTransPro);
//根據upCollapsedContentTransPro,設置摺疊內容位移
float upCollapsedContentTransPro = getUpCollapsedContentTransPro();
transCollapsedContentByPro(upCollapsedContentTransPro);
if (mProgressUpdateListener!=null){
mProgressUpdateListener.onUpAlphaScaleProUpdate(upAlphaScalePro);
mProgressUpdateListener.onUpAlphaGradientProUpdate(upAlphaGradientPro);
mProgressUpdateListener.onDownCollapsedAlphaProUpdate(downCollapsedAlphaPro);
mProgressUpdateListener.onDownShopBarTransProUpdate(downShopBarTransPro);
mProgressUpdateListener.onUpCollapsedContentTransProUpdate(upCollapsedContentTransPro);
}
}
});
reboundedAnim.setDuration(ANIM_DURATION_FRACTION);
}
@Override
public boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed) {
return false;
}
@Override
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
if (velocityY<0){//往下滑動的慣性滑動
float translationY = mLlContent.getTranslationY();
float dy = translationY - velocityY;
if (translationY >topBarHeight && translationY<= downFlingCutOffY) {//非摺疊狀態
//根據dy設置動畫結束值,只有dy>contentTransY纔會有回彈,downFlingCutOffY是回彈的最大值
if (dy<=contentTransY){
reboundedAnim.setFloatValues(translationY,dy);
}else if (dy>contentTransY&&dy<downFlingCutOffY){
reboundedAnim.setFloatValues(translationY,dy,contentTransY);
}else {
reboundedAnim.setFloatValues(translationY,downFlingCutOffY,contentTransY);
}
reboundedAnim.start();
return true;
}
}
return false;
}
複製代碼
在View從Window上移除時候,執行要中止動畫、釋放監聽者的操做。
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (restoreOrExpandAnimator.isStarted()) {
restoreOrExpandAnimator.cancel();
restoreOrExpandAnimator.removeAllUpdateListeners();
restoreOrExpandAnimator = null;
}
if(reboundedAnim.isStarted()){
reboundedAnim.cancel();
reboundedAnim.removeAllUpdateListeners();
reboundedAnim = null;
}
if (mProgressUpdateListener!=null){
mProgressUpdateListener=null;
}
}
複製代碼
contentTransY;//滑動內容初始化TransY
iconTransY;//分享、關閉icon初始化transY
upEndIconTransY;//分享、關閉icon上滑最終transY
downFlingCutOffY;///從展開狀態下滑產生fling時回彈到初始狀態的最大值
複製代碼
商家點餐頁的嵌套滑的自定義View繼承FrameLayout、實現NestedScrollingParent2接口,命名爲ElemeFoodNestedScrollLayout。
下面是佈局要點,側重於控件的尺寸和位置,完整佈局請參考:ElemeFoodNestedScrollLayout佈局
<ElemeFoodNestedScrollLayout android:layout_width="match_parent" android:layout_height="match_parent">
<!--Mask部分-->
<View android:layout_width="match_parent" android:layout_height="match_parent"/>
<!--Mask部分-->
<!--Content部分-->
<android.support.v4.widget.NestedScrollView android:id="@+id/ns" android:translationY="@dimen/food_content_trans_y" android:fillViewport="true" android:background="@android:color/white" android:layout_width="match_parent" android:layout_height="match_parent" ... >
<!--Content部分-->
<!--Icon部分-->
<ImageView android:layout_gravity="right" android:layout_width="28dp" android:layout_height="28dp" android:translationY="@dimen/iv_food_icon_trans_y" android:layout_marginEnd="60dp" />
<ImageView android:layout_gravity="right" android:layout_width="28dp" android:layout_height="28dp" android:translationY="@dimen/iv_food_icon_trans_y" android:layout_marginEnd="16dp" />
<!--Icon部分-->
<!--Expand部分-->
<ImageView android:alpha="0" android:layout_gravity="bottom" android:layout_width="match_parent" android:layout_height="@dimen/iv_food_expand" />
<!--Expand部分-->
<!--Shop Bar部分-->
<android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_gravity="bottom" android:layout_height="@dimen/shop_bar_height" android:translationY="@dimen/shop_bar_height" ... >
<!--Shop Bar部分-->
</ElemeFoodNestedScrollLayout>
複製代碼
從上面圖片可以分析出:關閉狀態時,Content部分的TransY爲滿屏高度
public class ElemeFoodNestedScrollLayout extends FrameLayout implements NestedScrollingParent2 {
...
//shopBar部分
private View mShopBar;
//content部分
private View mNestedScrollView;
private View mTvComm;
private View mTvGoodCommRate;
private View mTvCommDetail;
private View mTvCommCount;
private View mVLine;
private View mTvFoodDetail;
//expand部分
private View mIvExpand;
//icon部分
private View mIvShare;
private View mIvClose;
//mask部分
private View mVMask;
private float shopBarHeight;//shopBar部分高度
private float ivExpandHegiht;//ivExpand部分高度
private float statusBarHeight;//狀態欄高度
private float iconTransY;//分享、關閉icon初始化transY
private float contentTransY;//滑動內容初始化TransY
private float downFlingCutOffY;//下滑時fling上部分回彈臨界值
private float upEndIconTransY;//分享、關閉icon上滑最終transY
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
shopBarHeight = getResources().getDimension(R.dimen.shop_bar_height);
ivExpandHegiht = getResources().getDimension(R.dimen.iv_food_expand);
contentTransY = getResources().getDimension(R.dimen.food_content_trans_y);
iconTransY = getResources().getDimension(R.dimen.iv_food_icon_trans_y);
statusBarHeight = getResources().getDimensionPixelSize(getResources().getIdentifier("status_bar_height", "dimen", "android"));
downFlingCutOffY = contentTransY + dp2px(92);
upEndIconTransY = statusBarHeight + dp2px(8);
//由於開始就是關閉狀態,設置Content部分的TransY爲滿屏高度
mNestedScrollView.setTranslationY(getMeasuredHeight());
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mNestedScrollView = findViewById(R.id.ns);
mShopBar = findViewById(R.id.cl_food_shop_bar);
mTvComm = findViewById(R.id.t_comm);
mTvGoodCommRate = findViewById(R.id.t_good_comm_rate);
mTvCommDetail = findViewById(R.id.t_comm_detail);
mTvFoodDetail = findViewById(R.id.t_food_detail);
mTvCommCount = findViewById(R.id.t_comm_count);
mVLine = findViewById(R.id.view_line);
mIvExpand = findViewById(R.id.iv_food_expand);
mIvShare = findViewById(R.id.iv_small_share);
mIvClose = findViewById(R.id.iv_small_close);
mVMask = findViewById(R.id.v_mask);
}
}
複製代碼
ElemeFoodNestedScrollLayout只處理Content部分裏可滑動View的垂直方向的滑動。
@Override
public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int axes, int type) {
//處理Content部分裏可滑動View的垂直方向的滑動
return axes == ViewCompat.SCROLL_AXIS_VERTICAL && target.getId() == R.id.ns;
}
複製代碼
接下來就是處理滑動,上面效果分析提過:Content部分的上滑範圍=[0,contentTransY]、 下滑範圍=[contentTransY,即滿屏高度]即滑動範圍爲[0,即滿屏高度],ElemeFoodNestedScrollLayout要控制Content部分的TransitionY值要在範圍內,以前的商家頁已經說過了具體思路,這裏再也不贅述。
...
private ProgressUpdateListener mProgressUpdateListener;
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
float contentTransY = target.getTranslationY() - dy;
//處理上滑
if (dy > 0) {
if (contentTransY >= 0) {
translationByConsume(target, contentTransY, consumed, dy);
} else {
translationByConsume(target, 0, consumed, (target.getTranslationY() - 0));
}
}
//處理下滑
if (dy < 0 && !target.canScrollVertically(-1)) {
if (contentTransY >= 0 && contentTransY < getMeasuredHeight()) {
translationByConsume(target, contentTransY, consumed, dy);
} else {
translationByConsume(target, getMeasuredHeight(), consumed, getMeasuredHeight() - target.getTranslationY());
}
}
alphaTransView(contentTransY);
if (mProgressUpdateListener!=null){
mProgressUpdateListener.onDownConetntCloseProUpdate(getDownConetntClosePro());
}
}
private void alphaTransView(float transY) {
float upCollapseTransPro = getUpExpandTransPro();
//位移購物內容
float shopBarTransY = (1 - upCollapseTransPro) * shopBarHeight;
translation(mShopBar, shopBarTransY);
//設置商品信息View的透明度變化
setAlpha(mTvComm, upCollapseTransPro);
setAlpha(mTvGoodCommRate, upCollapseTransPro);
setAlpha(mTvCommDetail, upCollapseTransPro);
setAlpha(mTvFoodDetail, upCollapseTransPro);
setAlpha(mTvCommCount, 1 - upCollapseTransPro);
setAlpha(mVLine, 1 - upCollapseTransPro);
//位移share close兩個Icon,設置展開icon透明度
if (transY <= contentTransY) {
float ivExpandUpTransY = upCollapseTransPro * -contentTransY;
translation(mIvExpand, ivExpandUpTransY);
setAlpha(mIvExpand, 1 - upCollapseTransPro);
float iconTransY = upEndIconTransY + (1 - upCollapseTransPro) * (this.iconTransY - upEndIconTransY);
translation(mIvShare, iconTransY);
translation(mIvClose, iconTransY);
} else if (transY > contentTransY && transY <= getMeasuredHeight()) {
float ivExpandDowndTransY = (1 - getDownIvExpnadPro()) * ivExpandHegiht;
translation(mIvExpand, ivExpandDowndTransY);
float iconTransY = transY + dp2px(16);
translation(mIvShare, iconTransY);
translation(mIvClose, iconTransY);
}
}
private float getDownConetntClosePro() {
return (mNestedScrollView.getTranslationY() - contentTransY) / (getMeasuredHeight() - contentTransY);
}
private float getDownIvExpnadPro() {
return ((contentTransY+ivExpandHegiht)-MathUtils.clamp(mNestedScrollView.getTranslationY(), contentTransY, contentTransY+ivExpandHegiht)) / ivExpandHegiht;
}
private float getUpExpandTransPro() {
return (contentTransY - MathUtils.clamp(mNestedScrollView.getTranslationY(), 0, contentTransY)) / contentTransY;
}
public interface ProgressUpdateListener{
void onDownConetntCloseProUpdate(float pro);
}
複製代碼
在從初始狀態到展開狀態的上滑過程當中鬆手,若上滑百分比小於等於50%則執行恢復到初始狀態的動畫,不然執行轉化到展開狀態的動畫;同理從初始狀態到關閉狀態下滑過程當中鬆手,若下滑百分比小於等於50%則執行恢復到初始狀態的動畫,不然執行轉化到關閉狀態的動畫;
...
private ValueAnimator restoreOrExpandOrCloseAnimator;//收起或展開摺疊內容時執行的動畫
private NestedScrollingParentHelper mParentHelper;
public ElemeFoodNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mParentHelper = new NestedScrollingParentHelper(this);
...
restoreOrExpandOrCloseAnimator = new ValueAnimator();
restoreOrExpandOrCloseAnimator.setInterpolator(new AccelerateInterpolator());
restoreOrExpandOrCloseAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
translation(mNestedScrollView, (float) animation.getAnimatedValue());
alphaTransView(mNestedScrollView.getTranslationY());
if (mProgressUpdateListener!=null){
mProgressUpdateListener.onDownConetntCloseProUpdate(getDownConetntClosePro());
}
}
});
restoreOrExpandOrCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
int alpha=mNestedScrollView.getTranslationY() >= getMeasuredHeight()?0:1;
setAlpha(mIvClose,alpha);
setAlpha(mIvShare,alpha);
}
});
}
@Override
public void onStopNestedScroll(@NonNull View target, int type) {
mParentHelper.onStopNestedScroll(target, type);
float translationY = target.getTranslationY();
if (translationY == contentTransY|| reboundedAnim.isStarted()|| restoreOrExpandOrCloseAnimator.isStarted()) {
return;
}
long dur;
if (translationY < contentTransY) {
if (getUpExpandTransPro() <= 0.5f) {
dur = (long) (getUpExpandTransPro() * ANIM_DURATION_FRACTION);
restore(dur);
} else {
dur = (long) ((1 - getUpExpandTransPro()) * ANIM_DURATION_FRACTION);
expand(dur);
}
} else {
if (getDownConetntClosePro() >= 0.5f) {
dur = (long) (getDownConetntClosePro() * ANIM_DURATION_FRACTION);
close(dur);
} else {
dur = (long) ((1 - getDownConetntClosePro()) * ANIM_DURATION_FRACTION);
restore(dur);
}
}
}
public void restore(long dur) {
mIvClose.setClickable(true);
mVMask.setClickable(true);
mIvExpand.setClickable(true);
if (restoreOrExpandOrCloseAnimator.isStarted()) {
restoreOrExpandOrCloseAnimator.cancel();
}
restoreOrExpandOrCloseAnimator.setFloatValues(mNestedScrollView.getTranslationY(), contentTransY);
restoreOrExpandOrCloseAnimator.setDuration(dur);
restoreOrExpandOrCloseAnimator.start();
}
public void expand(long dur) {
if (restoreOrExpandOrCloseAnimator.isStarted()) {
restoreOrExpandOrCloseAnimator.cancel();
}
restoreOrExpandOrCloseAnimator.setFloatValues(mNestedScrollView.getTranslationY(), 0);
restoreOrExpandOrCloseAnimator.setDuration(dur);
restoreOrExpandOrCloseAnimator.start();
}
public void close(long dur) {
mNestedScrollView.scrollTo(0,0);
mIvClose.setClickable(false);
mVMask.setClickable(false);
mIvExpand.setClickable(false);
if (restoreOrExpandOrCloseAnimator.isStarted()) {
restoreOrExpandOrCloseAnimator.cancel();
}
restoreOrExpandOrCloseAnimator.setFloatValues(mNestedScrollView.getTranslationY(), getMeasuredHeight());
restoreOrExpandOrCloseAnimator.setDuration(dur);
restoreOrExpandOrCloseAnimator.start();
}
複製代碼
這裏有兩個場景須要使用onNestedPreFling()處理慣性滑動: 一、從展開狀態下滑時處理回彈Fling,執行回彈動畫; 二、從初始狀態到關閉狀態下滑百分比超過50%慣性滑動關閉;
private ValueAnimator reboundedAnim;//回彈動畫
...
public ElemeFoodNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
...
reboundedAnim = new ValueAnimator();
reboundedAnim.setInterpolator(new DecelerateInterpolator());
reboundedAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
translation(mNestedScrollView, (float) animation.getAnimatedValue());
alphaTransView(mNestedScrollView.getTranslationY());
if (mProgressUpdateListener!=null){
mProgressUpdateListener.onDownConetntCloseProUpdate(getDownConetntClosePro());
}
}
});
reboundedAnim.setDuration(ANIM_DURATION_FRACTION);
}
@Override
public boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY) {
if (velocityY<0) {
float translationY = target.getTranslationY();
float dy = translationY - velocityY;
//從展開狀態下滑時處理回彈Fling,執行回彈動畫
if (translationY >= 0 && translationY <= downFlingCutOffY){
if (dy<contentTransY){
reboundedAnim.setFloatValues(translationY,dy);
}else if (dy>contentTransY&&dy<downFlingCutOffY){
reboundedAnim.setFloatValues(translationY,dy,contentTransY);
}else {
reboundedAnim.setFloatValues(translationY,downFlingCutOffY,contentTransY);
}
target.scrollTo(0,0);
reboundedAnim.start();
return true;
}
//從初始狀態到關閉狀態下滑百分比超過50%慣性滑動關閉
float dur = (1- getDownConetntClosePro()) * ANIM_DURATION_FRACTION;
if (translationY<=(getMeasuredHeight()/2f)&&translationY>downFlingCutOffY){
restore((long) dur);
return true;
}else {
close((long) dur);
return true;
}
}
return false;
}
複製代碼
在View從Window上移除時候,執行要中止動畫、釋放監聽者的操做。
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (restoreOrExpandOrCloseAnimator.isStarted()) {
restoreOrExpandOrCloseAnimator.cancel();
restoreOrExpandOrCloseAnimator.removeAllUpdateListeners();
restoreOrExpandOrCloseAnimator.removeAllListeners();
restoreOrExpandOrCloseAnimator = null;
}
if (reboundedAnim.isStarted()) {
reboundedAnim.cancel();
reboundedAnim.removeAllUpdateListeners();
restoreOrExpandOrCloseAnimator = null;
}
if (mProgressUpdateListener!=null){
mProgressUpdateListener=null;
}
}
複製代碼
從效果預覽看的出,在點擊商品時ElemeFoodNestedScrollLayout內容彈出,而背後有蒙層和ElemeFoodNestedScrollLayout內容的縮放效果,它們兩都提供滑動百分比監聽者,因此這部分邏輯應該在Activity處理:
mElemeFoodNsLayout.setProgressUpdateListener(new ElemeFoodNestedScrollLayout.ProgressUpdateListener() {
@Override
public void onDownConetntCloseProUpdate(float pro) {
mElemeNSLayout.setScaleX(0.9f+(pro*0.1f));
mElemeNSLayout.setScaleY(0.9f+(pro*0.1f));
mVMask.setAlpha(1-pro);
if (pro==1){
mVMask.setVisibility(View.GONE);
}else {
mVMask.setVisibility(View.VISIBLE);
}
}
});
複製代碼
NestedScrolling機制結合View的Alpha、Scale、Transitio、動畫等等能夠弄出各類神奇而體驗好的交互,當自定義View趕上NestedScrolling機制,給你不同的精彩,只要理清思路、分析問題、一步一步來解決或許就會守得雲開見月明,因爲本人水平有限僅給各位提供參考,但願可以拋磚引玉,若是有什麼能夠討論的問題能夠在評論區留言或聯繫本人,本文餓了麼例子圖片素材來源於h5餓了麼和阿里巴巴矢量素材庫。