前言
ClipXX 系列:
Android clipChildren 使用與疑難點解析
Android clipToPadding 使用與疑難點解析
我們知道桶现,通常來說當子布局的邊界處在父布局之外的時候粘秆,此時子布局超出的部分是無法顯示的午绳。想要顯示超出的部分,通過設(shè)置clipChildren 屬性可以解決此問題酪耕,本篇將會探究clipChildren 屬性的使用及其原理皮钠。
通過本篇文章识樱,你將了解到:
1、clipChildren 使用場景
2猴贰、clipChildren 如何使用
3对雪、clipChildren 設(shè)置在父布局為什么無效
4、子布局超出部分如何響應點擊事件
5米绕、總結(jié)
1瑟捣、clipChildren 使用場景
先來看圖:
如上圖所示,底部有三個按鈕栅干,它們是包裹在同一個父布局里的迈套,整體布局文件如下:
<?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)層次如下:
通過布局文件并結(jié)合上圖可知:
1、三個Button是放在一個橫向的LinearLayout里的碱鳞。
2桑李、LinearLayout(父布局)背景色為紅色。
3、Button高度與父布局高度一致芙扎。
現(xiàn)在想要一個效果:
點擊對應的Button星岗,使其往上移動,凸顯點擊效果戒洼。
效果如下:
然而俏橘,并未達到預期效果。
此時圈浇,輪到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"
效果如下:
這正是開頭想要的效果。當然缴守,借助于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
當在父布局(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è)置
當在爺爺布局(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)。
最后看看效果:
注:為了更顯眼地表示點擊區(qū)域登澜,此處是將子布局往上全部移動超出父布局
5阔挠、總結(jié)
雖然 clipChildren屬性比較簡單,使用范圍也比較局限脑蠕,但是想要真正弄明白它需要結(jié)合測量购撼、擺放、繪制流程源碼分析谴仙,若是還想要對點擊區(qū)域做文章迂求,那么還需要對事件分發(fā)有一定的了解。
當然晃跺,這些基礎(chǔ)知識在前面的文章中已有系統(tǒng)的分析過揩局,若是看過之前的文章,那么理解clipChildren 更簡單了掀虎。
本文基于Android 10凌盯。
完整代碼演示 若是有幫助,給github 點個贊唄~