Android clipChildren 使用與疑難點解析

前言

ClipXX 系列:

Android clipChildren 使用與疑難點解析
Android clipToPadding 使用與疑難點解析

我們知道桶现,通常來說當子布局的邊界處在父布局之外的時候粘秆,此時子布局超出的部分是無法顯示的午绳。想要顯示超出的部分,通過設(shè)置clipChildren 屬性可以解決此問題酪耕,本篇將會探究clipChildren 屬性的使用及其原理皮钠。
通過本篇文章识樱,你將了解到:

1、clipChildren 使用場景
2猴贰、clipChildren 如何使用
3对雪、clipChildren 設(shè)置在父布局為什么無效
4、子布局超出部分如何響應點擊事件
5米绕、總結(jié)

1瑟捣、clipChildren 使用場景

先來看圖:


圖.jpeg

如上圖所示,底部有三個按鈕栅干,它們是包裹在同一個父布局里的迈套,整體布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <LinearLayout
        android:background="@color/red"
        android:layout_gravity="bottom"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="200px">

        <Button
            android:id="@+id/btn1"
            android:layout_marginLeft="50px"
            android:text="button 1"
            android:layout_width="0px"
            android:layout_weight="1"
            android:background="@color/green"
            android:layout_height="match_parent">
        </Button>

        <Button
            android:id="@+id/btn2"
            android:layout_marginLeft="50px"
            android:text="button 2"
            android:layout_width="0px"
            android:layout_weight="1"
            android:background="@color/green"
            android:layout_height="match_parent">
        </Button>

        <Button
            android:id="@+id/btn3"
            android:layout_marginHorizontal="50px"
            android:text="button 3"
            android:layout_width="0px"
            android:layout_weight="1"
            android:background="@color/green"
            android:layout_height="match_parent">
        </Button>
    </LinearLayout>

</FrameLayout>

簡化結(jié)構(gòu)層次如下:


image.png

通過布局文件并結(jié)合上圖可知:

1、三個Button是放在一個橫向的LinearLayout里的碱鳞。
2桑李、LinearLayout(父布局)背景色為紅色。
3、Button高度與父布局高度一致芙扎。

現(xiàn)在想要一個效果:

點擊對應的Button星岗,使其往上移動,凸顯點擊效果戒洼。

效果如下:


tt0.top-475243.gif

然而俏橘,并未達到預期效果。
此時圈浇,輪到clipChildren 屬性出馬了寥掐。

2、clipChildren 如何使用

clipChildren 顧名思義:裁剪子布局磷蜀,使得其不超過父布局展示召耘,該屬性是ViewGroup里的屬性。
有兩種設(shè)置方式:動態(tài)設(shè)置和xml設(shè)置褐隆。

動態(tài)設(shè)置

#ViewGroup.java
    public void setClipChildren(boolean clipChildren) {
        boolean previousValue = (mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN;
        if (clipChildren != previousValue) {
            //標記不一樣污它,需要設(shè)置
            //設(shè)置FLAG_CLIP_CHILDREN 屬性
            setBooleanFlag(FLAG_CLIP_CHILDREN, clipChildren);
            for (int i = 0; i < mChildrenCount; ++i) {
                //遍歷子布局,限定繪制邊界
                View child = getChildAt(i);
                if (child.mRenderNode != null) {
                    child.mRenderNode.setClipToBounds(clipChildren);
                }
            }
            invalidate(true);
        }
    }

xml設(shè)置

android:clipChildren="true"
android:clipChildren="false"

默認值

#ViewGroup.java
    private void initViewGroup() {
        ...
        mGroupFlags |= FLAG_CLIP_CHILDREN;
        mGroupFlags |= FLAG_CLIP_TO_PADDING;
        ...
    }

clipChildren 屬性值默認為true庶弃。
綜合以上幾點可知衫贬,clipChildren值默認為true,也就是默認裁剪子布局歇攻,因此為了達到上述效果固惯,在上面布局文件里的FrameLayout布局下添加如下代碼即可:

android:clipChildren="false"

效果如下:

tt0.top-114249.gif

這正是開頭想要的效果。當然缴守,借助于clipChildren 特性葬毫,我們還可以對Button做動畫效果,比如點擊Button后屡穗,讓其移動到ViewGroup之外贴捡。

3、clipChildren 設(shè)置在父布局為什么無效

網(wǎng)上大部分的文章在分析clipChildren 時只會提到之前的兩點:使用場景與如何使用鸡捐。
思考一個問題:

既然是限制子布局的展示栈暇,而Button的父布局是LinearLayout,為啥不在LinearLayout 節(jié)點下設(shè)置android:clipChildren="false"箍镜,而要在爺爺布局FrameLayout節(jié)點下設(shè)置呢?

當然一開始按照正常的邏輯是設(shè)置在父布局節(jié)點下的煎源,然而卻沒什么效果色迂,接下來分析一下為啥沒效果。
想要知道為什么不生效手销,就需要找到clipChildren屬性值在哪被使用了歇僧。我們知道自定義View的三個過程:測量、擺放、繪制诈悍。因為涉及到展示祸轮,因此猜測是在繪制過程被裁剪了,而裁剪展示區(qū)域我們就想到了Canvas的裁剪侥钳。
通過前面的文章分析的繪制過程适袜,直接定位到如下代碼(軟件繪制為例):

#View.java
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        //沒有開啟硬件加速
        if (!drawingWithRenderNode) {
            //parentFlags 為父布局的flag
            //若是父布局需要裁剪子布局,也就是說clipChildren==true
            //那么就需要對canvas進行裁剪
            if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
                //軟件繪制offsetForScroll==true
                if (offsetForScroll) {
                    //裁剪canvas與子布局大小一致
                    //sx,sy 是scroll值舷夺,沒設(shè)置scroll時sx,sy都為0
                    canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());
                } else {
                    ...
                }
            }
            ...
        }
    }

由此可知:

1苦酱、若是clipChildren==true,那么將會裁剪子布局给猾,方式是通過裁剪Canvas疫萤。
2、若是clipChildren==false敢伸,那么將不會裁剪Canvas扯饶。

在父布局節(jié)點設(shè)置

爺爺布局:FrameLayout
父布局:LinearLayout
子布局:Button

image.png

當在父布局(LinearLayout)節(jié)點里設(shè)置clipChildren==false時,因為爺爺布局(FrameLayout)沒有設(shè)置該屬性池颈,因此還是會限定其子布局尾序,也就是圖上紅色部分(父布局LinearLayout)的繪制范圍為:canvas=[0,1080,800,1280]
此時,即使(父布局LinearLayout)沒對子布局(Button)進行限制(clipChildren==false)饶辙,但是因為canvas已經(jīng)在上個步驟被限制了蹲诀,因此子布局(Button)展示的范圍依然在:canvas=[0,1080,800,1280]。
最后呈現(xiàn)的效果即是子布局不能超出父布局展示弃揽。

在爺爺布局節(jié)點設(shè)置

image.png

當在爺爺布局(FrameLayout)節(jié)點里設(shè)置clipChildren==false時脯爪,爺爺布局不會限制其子布局(紅色部分父布局LinearLayout),因此父布局(LinearLayout)繪制范圍為:canvas=[0,0,800,1280]矿微。
而當父布局(LinearLayout)限制子布局(Button)的展示范圍時痕慢,Canvas進行clip操作,取交集涌矢,得出子布局(Button)繪制范圍為:canvas=[100,980,300,1280]掖举,超出的部分(980-800)即為多出的展示區(qū)域。
最后呈現(xiàn)的效果即是子布局能夠超出父布局展示娜庇。

一言蔽之:

想要超出父布局展示塔次,只需要子布局canvas繪制范圍超出父布局邊界即可。

注:上述以軟件繪制為例闡述的名秀,爺爺布局励负,父布局,子布局都是同一個Canvas對象匕得,而開啟硬件加速后Canvas不是同一對象继榆。具體的差別請查看之前的文章。

4、子布局超出部分如何響應點擊事件

在第三步已經(jīng)解決了如何超出父布局展示略吨,現(xiàn)在又引入了新的問題:

子布局超出的部分如何響應點擊事件集币?

老樣子,既然點擊無法響應翠忠,那么先看看影響點擊響應的因素是啥鞠苟。
還是要從事件分發(fā)開始說起,如果點擊的坐標落在目標View之內(nèi)(此處是子布局Button)负间,那么它是能夠響應的偶妖。
現(xiàn)在問題就轉(zhuǎn)為了:

點擊事件分發(fā)到哪一層了?

雖然父布局(LinearLayout)的Canvas改變了政溃,但是其頂點(left趾访、top、right董虱、bottom)坐標也沒變扼鞋,因此父布局也無法收到點擊事件》哂眨可以確認的是云头,點擊事件肯定是分發(fā)給了爺爺布局的。
問題又轉(zhuǎn)為了:

爺爺布局的事件如何傳遞給父布局淫半?
換句話說溃槐,父布局如何擴大點擊區(qū)域?

這讓我們想到了TouchDelegate---一個專注擴大目標View點擊區(qū)域的類科吭。
找到解決方案了昏滴,看代碼:

        //expand touch area
        llParent.post(() -> {
            Rect hitRect = new Rect();
            //獲取父布局當前有效可點擊區(qū)域
            llParent.getHitRect(hitRect);
            //擴大父布局點擊區(qū)域
            hitRect.top += translationY;
            TouchDelegate touchDelegate = new TouchDelegate(hitRect, llParent);
            llParent.setClickable(true);
            ViewParent viewParent = llParent.getParent();
            if (viewParent instanceof ViewGroup) {
                ((ViewGroup) viewParent).setClickable(true);
                //在爺爺布局里攔截事件分發(fā)
                ((ViewGroup) viewParent).setTouchDelegate(touchDelegate);
            }
        });

以上代碼目的是:

擴大父布局響應的點擊區(qū)域,在爺爺布局里將事件分發(fā)給父布局对人。

然而運行這段代碼谣殊,子布局(Button)依然無法響應點擊,于是到TouchDelegate 尋找答案牺弄。
當爺爺布局發(fā)現(xiàn)之前設(shè)置了TouchDelegate姻几,于是就會調(diào)用TouchDelegate.onTouchEvent(xx)檢測:

#TouchDelegate.java
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        int x = (int)event.getX();
        int y = (int)event.getY();
        boolean sendToDelegate = false;
        boolean hit = true;
        boolean handled = false;
        ...
        if (sendToDelegate) {
            if (hit) {
                //命中,則將MotionEvent 坐標移動到目標View的中心
                event.setLocation(mDelegateView.getWidth() / 2, mDelegateView.getHeight() / 2);
            } else {
                ...
            }
            handled = mDelegateView.dispatchTouchEvent(event);
        }
        return handled;
    }

找到問題根源了:雖然父布局(FrameLayout)收到了點擊事件势告,但是這個坐標是它的中心點蛇捌,而中心點不一定落在其子布局(Button)里,因此Button是無法收到點擊事件的咱台。
還好豁陆,TouchDelegate是public類型的,于是我們可以重寫TouchDelegate

#SimpleTouchDelegate.java
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        int x = (int)event.getX();
        int y = (int)event.getY();
        boolean sendToDelegate = false;
        boolean hit = true;
        boolean handled = false;
        ...
        if (sendToDelegate) {
            if (hit) {
              //命中后不做任何操作
            } else {
                ...
            }
            handled = mDelegateView.dispatchTouchEvent(event);
        }
        return handled;
    }

此時父布局(LinearLayout)可以收到點擊事件了吵护,但問題又來了:

父布局如何將事件傳遞給子布局,并且還要區(qū)分三個不同的Button。

父布局收到點擊事件后調(diào)用會流轉(zhuǎn)到onTouchEvent(xx)里馅而,因此需要在該方法內(nèi)做文章祥诽。試想,現(xiàn)在父布局的onTouchEvent(xx)方法可以拿到點擊的坐標瓮恭,那么只需要判斷該點是否落在各個子布局(Button)內(nèi)即可雄坪。當然不能單純依賴Button的四個頂點坐標,還需要配合View.getLocationOnScreen(xx)使用屯蹦。
因此需要重寫onTouchEvent(xx):

public class ClipViewGroup extends LinearLayout {
    public ClipViewGroup(Context context) {
        super(context);
    }

    public ClipViewGroup(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //獲取坐標相對屏幕的位置
        float rawX = event.getRawX();
        float rawY = event.getRawY();
        View child;
        //檢測坐標是否落在對應的子布局內(nèi)
        if ((child = checkChildTouch(rawX, rawY)) != null) {
            //若是則將坐標值修改為子布局中心點
            event.setLocation(child.getWidth() / 2, child.getHeight() / 2);
            //分發(fā)事件給子布局
            return child.dispatchTouchEvent(event);
        }
        return super.onTouchEvent(event);
    }

    private View checkChildTouch(float x, float y) {
        int outLocation[] = new int[2];
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == VISIBLE) {
                //獲取View 在屏幕上的可見坐標
                child.getLocationOnScreen(outLocation);
                //點擊坐標是否落在View 的可見區(qū)域维哈,若是則將事件分發(fā)給它
                boolean hit = (x >= outLocation[0] && y > outLocation[1]
                        && x <= outLocation[0] + child.getWidth() && y <= outLocation[1] + child.getHeight());
                if (hit)
                    return child;
            }
        }
        return null;
    }
}

使用ClipViewGroup 替代父布局(LinearLayout)。
最后看看效果:


tt0.top-473084.gif

注:為了更顯眼地表示點擊區(qū)域登澜,此處是將子布局往上全部移動超出父布局

5阔挠、總結(jié)

雖然 clipChildren屬性比較簡單,使用范圍也比較局限脑蠕,但是想要真正弄明白它需要結(jié)合測量购撼、擺放、繪制流程源碼分析谴仙,若是還想要對點擊區(qū)域做文章迂求,那么還需要對事件分發(fā)有一定的了解。
當然晃跺,這些基礎(chǔ)知識在前面的文章中已有系統(tǒng)的分析過揩局,若是看過之前的文章,那么理解clipChildren 更簡單了掀虎。

本文基于Android 10凌盯。
完整代碼演示 若是有幫助,給github 點個贊唄~

您若喜歡涩盾,請點贊十气、關(guān)注,您的鼓勵是我前進的動力

持續(xù)更新中春霍,和我一起步步為營系統(tǒng)砸西、深入學習Android/Java

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市址儒,隨后出現(xiàn)的幾起案子芹枷,更是在濱河造成了極大的恐慌,老刑警劉巖莲趣,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸳慈,死亡現(xiàn)場離奇詭異,居然都是意外死亡喧伞,警方通過查閱死者的電腦和手機走芋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門绩郎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人翁逞,你說我怎么就攤上這事肋杖。” “怎么了挖函?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵状植,是天一觀的道長。 經(jīng)常有香客問我怨喘,道長津畸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任必怜,我火速辦了婚禮肉拓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘棚赔。我一直安慰自己帝簇,他們只是感情好,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布靠益。 她就那樣靜靜地躺著丧肴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胧后。 梳的紋絲不亂的頭發(fā)上芋浮,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天,我揣著相機與錄音壳快,去河邊找鬼纸巷。 笑死,一個胖子當著我的面吹牛眶痰,可吹牛的內(nèi)容都是我干的瘤旨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼竖伯,長吁一口氣:“原來是場噩夢啊……” “哼存哲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起七婴,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤祟偷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后打厘,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體修肠,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年户盯,在試婚紗的時候發(fā)現(xiàn)自己被綠了嵌施。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饲化。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖艰管,靈堂內(nèi)的尸體忽然破棺而出滓侍,到底是詐尸還是另有隱情,我是刑警寧澤牲芋,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站捺球,受9級特大地震影響缸浦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜氮兵,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一裂逐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泣栈,春花似錦卜高、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至疼进,卻和暖如春薪缆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背伞广。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工拣帽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人嚼锄。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓减拭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親区丑。 傳聞我的和親對象是個殘疾皇子拧粪,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

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