夸克瀏覽器是我很是喜歡的一款瀏覽器,使用起來簡潔流暢,UI作的也很精緻。今天我就來仿寫主頁底部的工具欄。先來看看本來的效果:java
效果比較簡單,從外表看就是一個彈框,特別之處就是能夠收縮伸展布局,再來看看我實現的效果:git
怎麼樣?效果是否是已經很是接近。先總體說下思路吧,底部對話框用DialogFragment
來實現,裏面的可伸縮佈局採用自定義
ViewGroup
。看了本文你將能學到(鞏固)如下知識點:
DialogFragment
的用法;ViewGroup
的用法,包括onMeasure
和onLayout
方法;ViewDragHelper
的用法,包括處理手勢和事件衝突聽起來內容挺多的,但只要一步步去解析,其實實現過程也不算複雜。github
底部對話框我採用了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);
}
}
}
複製代碼
點擊顯示彈框:bash
FragmentManager fm = getSupportFragmentManager();
BottomDialogFragment bottomDialogFragment = new BottomDialogFragment();
bottomDialogFragment.show(fm, "fragment_bottom_dialog");
複製代碼
這裏主要用到的就是自定義ViewGroup
的知識了。先大體梳理一下:咱們須要包含兩個子view,在上面的topView
,在下面的bottomView
。topView
往下滑的時候要覆蓋bottomView
。可是ViewGroup
的顯示的層次順序和添加順序是反過來的,後面添加的view若是和前面添加的View有重疊的話會覆蓋前面會覆蓋添加的view,而咱們預想的佈局文件應該是這樣的:ide
<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的位置:post
@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地址: