专栏名称: 唤之
目录
相关文章推荐
OSC开源社区  ·  2024: 大模型背景下知识图谱的理性回归 ·  昨天  
程序猿  ·  “未来 3 年内,Python 在 AI ... ·  2 天前  
程序员小灰  ·  DeepSeek做AI代写,彻底爆了! ·  2 天前  
程序猿  ·  问问DeepSeek,你和ChatGPT谁厉 ... ·  3 天前  
程序员小灰  ·  DeepSeek创始人梁文峰牛逼的个人经历 ·  3 天前  
51好读  ›  专栏  ›  唤之

仿夸克浏览器底部工具栏

唤之  · 掘金  · 程序员  · 2017-12-08 09:25

正文

夸克浏览器是我非常喜欢的一款浏览器,使用起来简洁流畅,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值,限制最大值。
            final int topBound = getPaddingTop();
            final int bottomBound = getHeight() - child.getHeight() - topBound;
            return Math.min(Math.max(top, topBound), bottomBound);
        }

        @Override






请到「今天看啥」查看全文