仿夸克瀏覽器底部工具欄

夸克瀏覽器是我非常喜歡的一款瀏覽器尤蛮,整體非常簡(jiǎn)潔媳友,UI做的也很精致。今天我就來(lái)仿寫(xiě)主頁(yè)底部的工具欄产捞。先來(lái)看看原本的效果:

gfx.gif

從外表看就是一個(gè)彈框醇锚,特別之處就是可以收縮伸展布局,再來(lái)看看我實(shí)現(xiàn)的效果:

gif.gif

怎么樣轧葛?效果是不是已經(jīng)非常接近搂抒。先整體說(shuō)下思路吧,底部對(duì)話(huà)框用DialogFragment來(lái)實(shí)現(xiàn)尿扯,里面的可伸縮布局采用自定義ViewGroup求晶。看了本文你將能學(xué)到(鞏固)以下知識(shí)點(diǎn):

  • DialogFragment的用法衷笋;
  • 自定義ViewGroup的用法芳杏,包括onMeasureonLayout方法矩屁;
  • 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");
device-2017-12-08-160140.png

自定義折疊布局

這里主要用到的就是自定義ViewGroup的知識(shí)了。先大致梳理一下:我們需要包含兩個(gè)子view秕铛,在上面的topView约郁,在下面的bottomViewtopView往下滑的時(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啤月!

gxxx.gif

最近我也在琢磨如何實(shí)現(xiàn)(recyclerView+自定義layoutManager?劳跃?谎仲?)。有實(shí)現(xiàn)方法或源碼的同學(xué)請(qǐng)?jiān)谙路搅粞耘俾兀屑げ槐M郑诺!如果我琢磨出來(lái)了也會(huì)第一時(shí)間分享出來(lái)。
最后貼下本栗的Github地址:

Github地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末杉武,一起剝皮案震驚了整個(gè)濱河市辙诞,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌轻抱,老刑警劉巖飞涂,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異祈搜,居然都是意外死亡较店,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)容燕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)梁呈,“玉大人,你說(shuō)我怎么就攤上這事缰趋∨跎迹” “怎么了陕见?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)味抖。 經(jīng)常有香客問(wèn)我评甜,道長(zhǎng),這世上最難降的妖魔是什么仔涩? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任忍坷,我火速辦了婚禮,結(jié)果婚禮上熔脂,老公的妹妹穿的比我還像新娘佩研。我一直安慰自己,他們只是感情好霞揉,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布旬薯。 她就那樣靜靜地躺著,像睡著了一般适秩。 火紅的嫁衣襯著肌膚如雪绊序。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,246評(píng)論 1 308
  • 那天秽荞,我揣著相機(jī)與錄音骤公,去河邊找鬼。 笑死扬跋,一個(gè)胖子當(dāng)著我的面吹牛阶捆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播钦听,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼洒试,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了彪见?” 一聲冷哼從身側(cè)響起儡司,我...
    開(kāi)封第一講書(shū)人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎余指,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體跷坝,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡酵镜,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了柴钻。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淮韭。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖贴届,靈堂內(nèi)的尸體忽然破棺而出靠粪,到底是詐尸還是另有隱情蜡吧,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布占键,位于F島的核電站昔善,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏畔乙。R本人自食惡果不足惜君仆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望牲距。 院中可真熱鬧返咱,春花似錦、人聲如沸牍鞠。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)难述。三九已至萤晴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間龄广,已是汗流浹背硫眯。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留择同,地道東北人两入。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像敲才,于是被迫代替她去往敵國(guó)和親裹纳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,280評(píng)論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)紧武、插件剃氧、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,119評(píng)論 4 61
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 46,791評(píng)論 22 665
  • 一想到結(jié)婚懷孕生小孩朋鞍,然后煮飯洗衣養(yǎng)小孩!然后舍不得買(mǎi)口紅妥箕,沒(méi)法愉快跟朋友約出來(lái)吃火鍋滥酥,花錢(qián)買(mǎi)件衣服還得看人眼色…...
    神奇小逗閱讀 222評(píng)論 1 1
  • 新人訓(xùn)的時(shí)候坎吻,老師說(shuō),我給你們講個(gè)故事吧宇葱,故事的主人公是公司現(xiàn)任南開(kāi)區(qū)和混編區(qū)的李區(qū)總瘦真,他說(shuō)李總剛來(lái)公司的時(shí)候只有...
    小菊呀閱讀 622評(píng)論 0 3