夸克瀏覽器是我非常喜歡的一款瀏覽器,使用起來簡潔流暢,UI做的也很精緻。今天我就來仿寫主頁底部的工具欄。先來看看原本的效果:
效果比較簡單,從外表看就是一個彈框,特別之處就是可以收縮伸展布局,再來看看我實現的效果:
怎麼樣?效果是不是已經非常接近。先整體說下思路吧,底部對話方塊用DialogFragment
來實現,裡面的可伸縮佈局採用自定義ViewGroup
。看了本文你將能學到(鞏固)以下知識點:
DialogFragment
的用法;- 自定義
ViewGroup
的用法,包括onMeasure
和onLayout
方法; ViewDragHelper
的用法,包括處理手勢和事件衝突
聽起來內容挺多的,但只要一步步去解析,其實實現過程也不算複雜。
底部對話方塊
底部對話方塊我採用了DialogFragment
,因為相比傳統的AlertDialog實現起來更簡單,用法也幾乎和普通的Fragment沒有什麼區別。
主要工作就是指定顯示位置:
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);
//顯示消失動畫
window.setWindowAnimations(R.style.animate_dialog);
//設定背景透明
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
//設定點選外部可以取消對話方塊
setCancelable(true);
}
}
}
複製程式碼
點選顯示彈框:
FragmentManager fm = getSupportFragmentManager();
BottomDialogFragment bottomDialogFragment = new BottomDialogFragment();
bottomDialogFragment.show(fm, "fragment_bottom_dialog");
複製程式碼
自定義摺疊佈局
這裡主要用到的就是自定義ViewGroup
的知識了。先大致梳理一下:我們需要包含兩個子view,在上面的topView
,在下面的bottomView
。topView
往下滑的時候要覆蓋bottomView
。但是ViewGroup
的顯示的層次順序和新增順序是反過來的,後面新增的view如果和前面新增的View有重疊的話會覆蓋前面會覆蓋新增的view,而我們預想的佈局檔案應該是這樣的:
<ViewGroup>
<topView/>
<bottom/>
</ViewGroup>
複製程式碼
所以我們需要在程式碼中手動對換兩者順序:
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() != 2) {
throw new RuntimeException("必須是2個子View!");
}
topView = getChildAt(0);
bottomView = getChildAt(1);
bringChildToFront(topView);
}
複製程式碼
這樣之後getChildAt(0)
取到的就是bottomView
了。接下來是onMeasure()
,計算自身的大小:
/**
* 計算所有ChildView的寬度和高度 然後根據ChildView的計算結果,設定自己的寬和高
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/**
* 獲得此ViewGroup上級容器為其推薦的寬和高,以及計算模式
*/
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
// 計算出所有的childView的寬和高
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height = 0;
/**
* 根據childView計算的出的寬和高,以及設定的margin計算容器的寬和高,主要用於容器是warp_content時
*/
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;
//高度為兩個子view的和
height = height + cHeightWithMargin;
//寬度取兩個子view中的最大值
width = cWidthWithMargin > width ? cWidthWithMargin : width;
}
/**
* 如果是wrap_content設定為我們計算的值
* 否則:直接設定為父容器計算的值
*/
setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? sizeWidth
: width, (heightMode == MeasureSpec.EXACTLY) ? sizeHeight
: height);
}
複製程式碼
然後自定義onLayout()
,放置兩個子View的位置:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
/**
* 遍歷所有childView根據其寬和高,以及margin進行佈局
*/
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);
}
}
複製程式碼
這樣之後,就可以顯示佈局了,但還是不能滑動。處理滑動我採用了ViewDragHelper
,這個工具類可謂自定義ViewGroup
神器。有了它,ViewGroup
可以很容易的控制各個子View的滑動。什麼事件分發,滑動衝突都不需要我們操心了。
mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelperCallBack())
建立例項需要3個引數,第一個就是當前的ViewGroup,第二個是sensitivity
(敏感係數,聯想下滑鼠靈敏度就知道了)。第三個引數就是Callback,會在觸控過程中會回撥相關方法,也是我們主要需要實現的方法。
private class ViewDragHelperCallBack extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return topView == child;//限制只有topView可以滑動
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return 0;//橫向可滑動範圍,因為不可以橫向滑動直接返回0就行
}
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}
@Override
public int clampViewPositionVertical(View child, int top, int dy){
//豎向可滑動範圍,top是child即將滑動到的top值,限制top的範圍在topBound和bottomBound之間。
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - child.getHeight() - getPaddingBottom();
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動畫
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
changedView.setElevation(percent * 10);
}
//處理bottomView動畫
bottomView.setScaleX(1 - percent * 0.03f);
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
//手指釋放時,滑動距離大於一半直接滾動到底部,否則返回頂部
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();
}
}
}
複製程式碼
至於處理事件分發,處理滾動全都交給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;
}
複製程式碼
總結
好了實現大致分析完了,還有一些小細節的處理和自定義View常用的回撥、get/set方法就不說了,大家如果有興趣的話就直接去看原始碼吧。個人覺得以上實現通用性還是不足吧,現在只能實現一層摺疊,摺疊方向也是固定的。作為對比,我們來看下Android系統通知欄的流式摺疊佈局。怎麼樣,是不是比上面這個不知道高到哪裡去了!Excited!
最近我也在琢磨如何實現(recyclerView
+自定義layoutManager
???)。有實現方法或原始碼的同學請在下方留言,感激不盡!如果我琢磨出來了也會第一時間分享出來。
最後貼下本慄的Github地址: