仿夸克瀏覽器底部工具欄

夸克瀏覽器是我很是喜歡的一款瀏覽器,使用起來簡潔流暢,UI作的也很精緻。今天我就來仿寫主頁底部的工具欄。先來看看本來的效果:java

效果比較簡單,從外表看就是一個彈框,特別之處就是能夠收縮伸展布局,再來看看我實現的效果:git

怎麼樣?效果是否是已經很是接近。先總體說下思路吧,底部對話框用 DialogFragment來實現,裏面的可伸縮佈局採用自定義 ViewGroup。看了本文你將能學到(鞏固)如下知識點:

  • DialogFragment的用法;
  • 自定義ViewGroup的用法,包括onMeasureonLayout方法;
  • 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,在下面的bottomViewtopView往下滑的時候要覆蓋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地址:

Github地址

12月12日更新:以上的效果我已經實現啦,請關注後續博客:

RecyclerView進階之層疊列表(上)

相關文章
相關標籤/搜索