夸克瀏覽器是我非常喜歡的一款瀏覽器尤蛮,整體非常簡(jiǎn)潔媳友,UI做的也很精致。今天我就來(lái)仿寫(xiě)主頁(yè)底部的工具欄产捞。先來(lái)看看原本的效果:
從外表看就是一個(gè)彈框醇锚,特別之處就是可以收縮伸展布局,再來(lái)看看我實(shí)現(xiàn)的效果:
怎么樣轧葛?效果是不是已經(jīng)非常接近搂抒。先整體說(shuō)下思路吧,底部對(duì)話(huà)框用DialogFragment
來(lái)實(shí)現(xiàn)尿扯,里面的可伸縮布局采用自定義ViewGroup
求晶。看了本文你將能學(xué)到(鞏固)以下知識(shí)點(diǎn):
-
DialogFragment
的用法衷笋; - 自定義
ViewGroup
的用法芳杏,包括onMeasure
和onLayout
方法矩屁; -
ViewDragHelper
的用法,包括處理手勢(shì)和事件沖突
聽(tīng)起來(lái)內(nèi)容挺多的爵赵,但只要一步步去解析吝秕,其實(shí)實(shí)現(xiàn)過(guò)程也不算復(fù)雜。
底部對(duì)話(huà)框
底部對(duì)話(huà)框我采用了DialogFragment
,因?yàn)橄啾葌鹘y(tǒng)的AlertDialog實(shí)現(xiàn)起來(lái)更簡(jiǎn)單空幻,用法也幾乎和普通的Fragment沒(méi)有什么區(qū)別烁峭。
主要工作就是指定顯示位置:
public class BottomDialogFragment extends DialogFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_bottom, null);
}
public void onStart() {
super.onStart();
Dialog dialog = getDialog();
if (dialog != null && dialog.getWindow() != null) {
Window window = dialog.getWindow();
//指定顯示位置
dialog.getWindow().setGravity(Gravity.BOTTOM);
//指定顯示大小
dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
//顯示消失動(dòng)畫(huà)
window.setWindowAnimations(R.style.animate_dialog);
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
//設(shè)置點(diǎn)擊外部可以取消對(duì)話(huà)框
setCancelable(true);
}
}
}
點(diǎn)擊顯示彈框:
FragmentManager fm = getSupportFragmentManager();
BottomDialogFragment bottomDialogFragment = new BottomDialogFragment();
bottomDialogFragment.show(fm, "fragment_bottom_dialog");
自定義折疊布局
這里主要用到的就是自定義ViewGroup
的知識(shí)了。先大致梳理一下:我們需要包含兩個(gè)子view秕铛,在上面的topView
约郁,在下面的bottomView
。topView
往下滑的時(shí)候要覆蓋bottomView
但两。但是ViewGroup
的層次順序默認(rèn)是和添加順序是反過(guò)來(lái)的鬓梅,后面添加的view會(huì)覆蓋前面的view,而我們預(yù)想的布局文件應(yīng)該是這樣的:
<ViewGroup>
<topView/>
<bottom/>
</ViewGroup>
所以我們需要在代碼中手動(dòng)對(duì)換兩者順序:
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() != 2) {
throw new RuntimeException("必須是2個(gè)子View谨湘!");
}
topView = getChildAt(0);
bottomView = getChildAt(1);
bringChildToFront(topView);
}
這樣之后getChildAt(0)
取到的就是bottomView
了绽快。接下來(lái)是onMeasure()
,計(jì)算自身的大薪衾:
/**
* 計(jì)算所有ChildView的寬度和高度 然后根據(jù)ChildView的計(jì)算結(jié)果坊罢,設(shè)置自己的寬和高
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/**
* 獲得此ViewGroup上級(jí)容器為其推薦的寬和高,以及計(jì)算模式
*/
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
// 計(jì)算出所有的childView的寬和高
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height = 0;
/**
* 根據(jù)childView計(jì)算的出的寬和高寓辱,以及設(shè)置的margin計(jì)算容器的寬和高艘绍,主要用于容器是warp_content時(shí)
*/
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
MarginLayoutParams cParams = (MarginLayoutParams) childView.getLayoutParams();
int cWidthWithMargin = childView.getMeasuredWidth() + cParams.leftMargin + cParams.rightMargin;
int cHeightWithMargin = childView.getMeasuredHeight() + cParams.topMargin + cParams.bottomMargin;
//高度為兩個(gè)子view的和
height = height + cHeightWithMargin;
//寬度取兩個(gè)子view中的最大值
width = cWidthWithMargin > width ? cWidthWithMargin : width;
}
/**
* 如果是wrap_content設(shè)置為我們計(jì)算的值
* 否則:直接設(shè)置為父容器計(jì)算的值
*/
setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? sizeWidth
: width, (heightMode == MeasureSpec.EXACTLY) ? sizeHeight
: height);
}
然后自定義onLayout()
,放置兩個(gè)子View的位置:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
/**
* 遍歷所有childView根據(jù)其寬和高秫筏,以及margin進(jìn)行布局
*/
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
int cWidth = childView.getMeasuredWidth();
int cHeight = childView.getMeasuredHeight();
MarginLayoutParams cParams = (MarginLayoutParams) childView.getLayoutParams();
int cl = 0, ct = 0, cr = 0, cb = 0;
switch (i) {
case 0://bottomView放下面
cl = cParams.leftMargin;
ct = getHeight() - cHeight - cParams.bottomMargin;
cb = cHeight + ct ;
childView.setPadding(0, extendHeight, 0, 0);
cr = cl + cWidth;
break;
case 1://topView放上面
cl = cParams.leftMargin;
ct = cParams.topMargin;
cb = cHeight + ct;
cr = cl + cWidth;
break;
}
childView.layout(cl, ct, cr, cb);
}
}
這樣之后,就可以顯示布局了挎挖,但還是不能滑動(dòng)这敬。處理滑動(dòng)我采用了ViewDragHelper
,這個(gè)工具類(lèi)可謂自定義ViewGroup
神器蕉朵。有了它崔涂,ViewGroup
可以很容易的控制各個(gè)子View的滑動(dòng)。什么事件分發(fā)始衅,滑動(dòng)沖突都不需要我們操心了冷蚂。
mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelperCallBack())
創(chuàng)建實(shí)例需要3個(gè)參數(shù),第一個(gè)就是當(dāng)前的ViewGroup汛闸,第二個(gè)是sensitivity
(敏感系數(shù)蝙茶,聯(lián)想下鼠標(biāo)靈敏度就知道了)。第三個(gè)參數(shù)就是Callback诸老,會(huì)在觸摸過(guò)程中會(huì)回調(diào)相關(guān)方法隆夯,也是我們主要需要實(shí)現(xiàn)的方法。
private class ViewDragHelperCallBack extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return topView == child;//限制只有topView可以滑動(dòng)
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;//橫向可滑動(dòng)范圍,因?yàn)椴豢梢詸M向滑動(dòng)直接返回0就行
}
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}
@Override
public int clampViewPositionVertical(View child, int top, int dy){
//豎向可滑動(dòng)范圍蹄衷,top是child即將滑動(dòng)到的top值忧额,限制最大值。
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - child.getHeight() - topBound;
return Math.min(Math.max(top, topBound), bottomBound);
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
float percent = (float) top / (getHeight() - changedView.getHeight());
//處理topView動(dòng)畫(huà)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
changedView.setElevation(percent * 10);
}
//處理bottomView動(dòng)畫(huà)
bottomView.setScaleX(1 - percent * 0.03f);
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//手指釋放時(shí),滑動(dòng)距離大于一半直接滾動(dòng)到底部愧口,否則返回頂部
if (releasedChild == topView) {
float movePercentage = (float) (releasedChild.getTop()) / (getHeight() - releasedChild.getHeight() - elevationHeight);
int finalTop = (movePercentage >= .5f) ? getHeight() - releasedChild.getHeight() - elevationHeight : 0;
mDragger.settleCapturedViewAt(releasedChild.getLeft(), finalTop);
invalidate();
}
}
}
至于處理事件分發(fā)睦番,處理滾動(dòng)全都交給ViewDragHelper
做就行了:
@Override
public void computeScroll() {
if (mDragger.continueSettling(true)) {
invalidate();
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return mDragger.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mDragger.processTouchEvent(event);
return true;
}
總結(jié)
好了實(shí)現(xiàn)大致分析完了,還有一些小細(xì)節(jié)的處理和自定義View常用的回調(diào)耍属、get/set方法就不說(shuō)了抡砂,大家如果有興趣的話(huà)就直接去看源碼吧。個(gè)人覺(jué)得以上實(shí)現(xiàn)通用性還是不足吧恬涧,現(xiàn)在只能實(shí)現(xiàn)一層折疊注益,折疊方向也是固定的。作為對(duì)比溯捆,我們來(lái)看下Android系統(tǒng)通知欄的流式折疊布局丑搔。怎么樣,是不是比上面這個(gè)不知道高到哪里去了提揍!Excited啤月!
最近我也在琢磨如何實(shí)現(xiàn)(recyclerView
+自定義layoutManager
?劳跃?谎仲?)。有實(shí)現(xiàn)方法或源碼的同學(xué)請(qǐng)?jiān)谙路搅粞耘俾兀屑げ槐M郑诺!如果我琢磨出來(lái)了也會(huì)第一時(shí)間分享出來(lái)。
最后貼下本栗的Github地址: