相信大家平時(shí)使用最多的應(yīng)用應(yīng)該就是支付寶和微信了吧秩霍,但是個(gè)人感覺(jué)從設(shè)計(jì)上看 支付寶的交互設(shè)計(jì)明顯更勝一籌,要說(shuō)到底是哪里強(qiáng)吧,我感覺(jué)就是支付寶的里面各種嵌套滑動(dòng)給了應(yīng)用更高的可玩性或链,不會(huì)顯得那么單調(diào).
于是,咱們就擼個(gè)支付寶首頁(yè)玩吧理澎! 哈哈哈
先擺上項(xiàng)目地址https://github.com/nokiafen/viewpro/tree/master/alihomepage
獻(xiàn)上動(dòng)圖
是不是覺(jué)得我山寨了一個(gè)支付寶逞力??糠爬? 冤枉寇荧!我只是偷了懶只實(shí)現(xiàn)了交互效果而已 其它區(qū)域都是截圖啊 ,大兄弟执隧!
怎么實(shí)現(xiàn)呢揩抡?其實(shí)只實(shí)現(xiàn)交互效果并不算太復(fù)雜,改bug倒是花了不少功夫 就一個(gè)類300來(lái)行就可以搞定了 不信看我截圖
要達(dá)到這個(gè)效果最簡(jiǎn)單的就是自定義CoordinatorLayout.Behavior了 大概談?wù)勊墓ぷ髟戆啥屏穑珻oordinatorLayout就是一個(gè)加強(qiáng)版的FrameLayout,它通過(guò)Behavior來(lái)與它的直接子控件進(jìn)行通信峦嗤,CoordinatorLayout會(huì)通過(guò)behavior來(lái)控制子控件的位置,以及統(tǒng)一分配滑動(dòng)事件.
1.首先咋們定義一個(gè)behavior 給咱們布局里面最下面 NestSrollerView用(里面有一大堆可以滑動(dòng)的東西)并且在布局文件里面NestScrollerView標(biāo)簽里面聲明一下
這里的scroll_behavior 就是你自定義behavior的全包名
- 然后其它邏輯都在behavior了
2.1 構(gòu)造函數(shù)得重寫(xiě)
public ScrollBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
scroller = new Scroller(context);
scrollerRefresh = new Scroller(context);
handler = new Handler();
}
2.2 指定一個(gè)依賴屋摔,其實(shí)就是你想監(jiān)聽(tīng)那個(gè)view的位置變化你就指定誰(shuí)
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull NestedScrollView child,
@NonNull View dependency) {
if (dependency.getId() == R.id.function_area) {
dependy = new WeakReference<View>(dependency);
return true;
}
return super.layoutDependsOn(parent, child, dependency);
}
2.3 處理依賴位置變化 烁设,如果你想要依據(jù)依賴位置的變化做一些邏輯操作
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent,
@NonNull NestedScrollView child, @NonNull View dependency) {
child.setTranslationY(dependency.getTranslationY()*2);
float perchent = dependency.getTranslationY() / maxFunctionCollaped * 0.3f;
// parent.findViewById(R.id.title_bar).setAlpha(1-perchent);
if (dependency.getTranslationY() > -maxFunctionCollaped * 0.3f) {
parent.findViewById(R.id.title_bar).setBackgroundResource(R.mipmap.top_search_back);
} else {
parent.findViewById(R.id.title_bar).setBackgroundResource(R.mipmap.search_bar_collapsing);
}
return super.onDependentViewChanged(parent, child, dependency);
}
2.4 指定界面初始化時(shí)的布局位置,因?yàn)镃oordinateLayout相當(dāng)于幀布局 钓试,你要在這里指定一些位置關(guān)系(通常是view的上下關(guān)系装黑,幀布局里面寫(xiě)比較費(fèi)力,在代碼里面直接碼比較直接).
@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull NestedScrollView child,
int layoutDirection) {
View function_area = parent.findViewById(R.id.function_area);
View tigle_bar = parent.findViewById(R.id.title_bar);
View bottomLayout = parent.findViewById(R.id.bottom_layout);
child.layout(0, function_area.getBottom(), parent.getMeasuredWidth(),
function_area.getMeasuredHeight() + parent.getMeasuredHeight()+bottomLayout.getMeasuredHeight());
maxFunctionCollaped = (int) (function_area.getMeasuredHeight()*0.5f);
anim_root = child.findViewById(R.id.anim_root);
scroll_content = child.findViewById(R.id.scroll_content);
return true;
}
2.5 嵌套滑動(dòng)第一步 亚侠,判斷是否進(jìn)行嵌套滑動(dòng) 曹体,每次嵌套滑動(dòng)就是從這個(gè)方法開(kāi)始的
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull NestedScrollView child, @NonNull View directTargetChild, @NonNull View target,
int axes, int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
2.6 嵌套滑動(dòng)第二步 如果第一步返回true 就會(huì)到這里來(lái)了
@Override
public void onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull NestedScrollView child, @NonNull View directTargetChild, @NonNull View target,
int axes, int type) {
scroller.abortAnimation();
scrollerRefresh.forceFinished(true);
isNestScrolling=false;
super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, axes, type);
}
2.7 嵌套滑動(dòng)第三步 開(kāi)始分配滑動(dòng)距離
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull NestedScrollView child, @NonNull View target, int dx, int dy,
@NonNull int[] consumed, int type) {
View dependView = dependy.get();
if (dy > 0 && dependView.getTranslationY() <= 0 && (anim_root.getTranslationY() == 0||anim_root.getTranslationY()==anim_root.getMeasuredHeight())) { //刷新布局未偏移 , 向上滑動(dòng) 且 上部分折疊區(qū)未折疊或正在折疊
dy=(int)(Math.abs(dy));
int currentTranslation = (int) dependView.getTranslationY();
int targetDy = currentTranslation - dy;
int concorrect = targetDy < -maxFunctionCollaped ? -maxFunctionCollaped : targetDy;
dependView.setTranslationY(concorrect);
consumed[1] = currentTranslation - concorrect;
} else if (dy < 0 && dependView.getTranslationY() >= 0) {// 向下滑動(dòng) 且上部分折疊區(qū)未折疊
int currentTranslationY = (int) anim_root.getTranslationY();
dy=-(int)(Math.abs(dy)*0.6f);
float calculate = currentTranslationY - dy > anim_root.getMeasuredHeight() ? anim_root.getMeasuredHeight() : currentTranslationY - dy ;
//calculate+=(calculate/anim_root.getMeasuredHeight())*dy; //阻尼效果
anim_root.setTranslationY(calculate);
scroll_content.setTranslationY(calculate);
consumed[1] = (int) (calculate - currentTranslationY);
} else if (dy > 0 && anim_root.getTranslationY() > 0) { //向上滑動(dòng) 硝烂,刷新布局已經(jīng)劃出來(lái)
int currentTranslationY = (int) anim_root.getTranslationY();
int calculate = currentTranslationY - dy >= 0 ? currentTranslationY - dy : 0;
anim_root.setTranslationY(calculate);
scroll_content.setTranslationY(calculate);
consumed[1] = dy;
}
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
這里dy表示y方向 感應(yīng)到的滑動(dòng)量 dy>0表示向上滑動(dòng) dy<0表示向下滑動(dòng),然后你可以根據(jù)dy以及當(dāng)前控件的位置來(lái)決定用哪一個(gè)控件來(lái)消耗這個(gè)dy ,然后把你的消耗值傳給consumed[1] 铜幽,如果你把dy消耗完了滞谢,那里面嵌套的可滑動(dòng)控件NestScrollerview就不會(huì)滑動(dòng)內(nèi)部?jī)?nèi)容了,如果沒(méi)用完除抛,那么NestScrollerview會(huì)緊接著onNestedPreScroll你自己定義的滑動(dòng)繼續(xù)滑動(dòng)NestScrollerview里面的內(nèi)容
2.8 嵌套滑動(dòng)第四步:可滑動(dòng)控件NestScrollerview 滑動(dòng)完了(前提是它有得滑)緊接著就會(huì)到 onNestedScroll方法
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull NestedScrollView child, @NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int type) {
if (dyUnconsumed < 0 && dependy.get().getTranslationY() < 0) {
View dependView = dependy.get();
int currentTranslation = (int) dependView.getTranslationY();
int targetDy = currentTranslation - dyUnconsumed;
int concorrect = targetDy > 0 ? 0 : targetDy;
dependView.setTranslationY(concorrect);
dyConsumed = currentTranslation - concorrect;
isNestScrolling=true;
}
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed,
dyUnconsumed, type);
}
如果可滑動(dòng)控件NestScrollerview滑動(dòng)(它是在滑動(dòng)它里面的內(nèi)容)完了 還有未消耗完的滑動(dòng)量 你可以在這里拿到滑動(dòng)量dyUnconsumed,繼續(xù)定義緊接著NestScrollerview滑動(dòng)完的其它滑動(dòng)事件
2.9 慣性滑動(dòng)處理 ---滑動(dòng)結(jié)束的一種情況(不一定會(huì)調(diào)用)
public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull NestedScrollView child, @NonNull View target, float velocityX, float velocityY) {
onRefreshEnd();
Log.d("onNestedStroll","fling");
return onDragEnd(velocityY);
}
一般就是快速滑動(dòng)馬上抬手就會(huì)出現(xiàn)狮杨,這里會(huì)返回滑動(dòng)速度給你,參考這個(gè)數(shù)值來(lái)處理滑動(dòng)view的最終停留狀態(tài)。
2.10 滑動(dòng)結(jié)束的回調(diào)到忽,不同于上個(gè)方法這個(gè) 一定會(huì)觸發(fā)的
@Override
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
@NonNull NestedScrollView child, @NonNull View target, int type) {
Log.d("onNestedStroll","onStopNestedScroll");
onRefreshEnd();
onDragEnd(600);
isNestScrolling=false;
}
滑動(dòng)結(jié)束后 必須確定View的最終位置(到底是展開(kāi)還是縮起來(lái))橄教,擺在中間不太好看吧,這里需要借助scroll開(kāi)展平滑動(dòng)畫(huà)來(lái)將你的view從中間態(tài)移動(dòng)到你最終希望它到達(dá)的位置喘漏,達(dá)到一種彈性效果护蝶。
onNestedPreFling onStopNestedScroll兩個(gè)方法處理滑動(dòng)結(jié)束的邏輯比較統(tǒng)一也相對(duì)簡(jiǎn)單,而且邏輯基本上都是一樣的翩迈。麻煩點(diǎn)是動(dòng)畫(huà)播放時(shí)進(jìn)行嵌套移動(dòng)以及如何分配滑動(dòng)量給子view
博文里只能簡(jiǎn)述基本流程持灰,詳情請(qǐng)移步倉(cāng)庫(kù)
https://github.com/nokiafen/viewpro/tree/master/alihomepage