Android中不規(guī)則形狀View的布局實現(xiàn)
在Android中不管是View還是ViewGroup,都是方的! 方的! 方的!
而對于非方形的,Android官方并沒有給出非常好的解決方案.有的無非就是自定義View了.
然而自定義View非常麻煩,需要重寫很多方法,而且稍微不注意可能就會喪失一些特性或者造成一些Bug.
而且即便是自定義View,其實那個自定義View還是方的!!!,自定義View所能做的也就是繪制非方的圖形,但是其觸摸區(qū)域還是方的,如果需要讓一些區(qū)域觸摸無效,需要在onTouchEvent中嚴(yán)謹(jǐn)?shù)挠嬎?而這只是僅僅針對View而言,如果這個View是ViewGroup,則需要重寫dispatchTouchEvent,dispatchToucEvent的邏輯相比于onTouchEvent的處理邏輯復(fù)雜多了.
而此時此刻,ClipPathLayout孕育而生,非常好的解決了這個問題.
何為ClipPathLayout,顧名思義,這就是一個可以對子View的Path進行裁剪的布局.
那么這個布局有什么作用呢?
問的好,這個布局可以對其子View的繪制范圍和觸摸范圍進行裁剪,進而實現(xiàn)不規(guī)則形狀的View.
光說有啥用.
那就亮出來給你們看看效果.
效果展示
將方形圖片裁剪成圓形并且讓圓形View的4角不接收觸摸事件
很多游戲都會有方向鍵,曾經(jīng)我也做過一個小游戲,但是在做方向鍵的時候遇到一個問題,4個方向按鈕的位置會有重疊,導(dǎo)致局部地方會發(fā)生誤觸.
當(dāng)時沒有特別好的解決辦法,只能做自定義View,而自定義View特別麻煩,需要重寫onTouchEvent和onDraw計算落點屬于哪個方向,并增加點擊效果.
簡單的自定義View會喪失很多Android自帶的一些特性,要支持這些特性又繁瑣而復(fù)雜.
下面借助于ClipPathLayout用4個菱形按鈕實現(xiàn)的方向控制鍵很好的解決了這個問題
對于遙控器的按鍵的模擬同樣有上述問題,一般只能采用自定義View實現(xiàn),較為繁瑣.
以下是借助于ClipPathLayout實現(xiàn)的遙控器按鈕,由于沒有美工切圖,比較丑,將就下吧
甚至我們可以將不連續(xù)的圖形變成一個View,比如做一個陰陽魚的按鈕
使用
效果展示完了,那么如何使用呢?使用太麻煩也是白搭.
那么接下來就講下如何使用.
添加依賴
庫已經(jīng)上傳jcenter,Android Studio自帶jcenter依賴,
如果沒有添加,請在項目根build.gradle中添加jcenter Maven
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
在app module中的build.gradle中添加依賴
implementation 'com.yxf:clippathlayout:1.0.+'
其實ClipPathLayout只是一個接口,大部分的ViewGroup,實現(xiàn)這個接口都可以實現(xiàn)對不規(guī)則圖形的布局,并且保留父類ViewGroup的特性.
當(dāng)前實現(xiàn)了三個不規(guī)則圖形的布局,分別是
- ClipPathFrameLayout
- ClipPathLinearLayout
- ClipPathRelativeLayout
如果有其他布局要求,請自定義,參見自定義ClipPathLayout
那么父布局要如何知道其子View應(yīng)該是何形狀呢?那必然需要給子View做自定義屬性吧,很顯然去重寫子View添加自定義屬性是不合理的.那么就采用外部關(guān)聯(lián)的方式好了.還有一個問題,什么屬性可以定義各種各樣的形狀呢?思來想去怕是也只有閉合的Path了吧,嗯,沒錯,就是借助于Path,并且讓子View和這個Path關(guān)聯(lián),然后把這些信息告訴父布局,這樣父布局才知道應(yīng)該如何去控制這個子View的形狀.
光說理論有什么用,來點實際的啊!
好,那就來點實際的.這里以最簡單的圓形View為例.
在一個實現(xiàn)了ClipPathLayout接口的ViewGroup(以ClipPathFrameLayout為例)中添加一個子View(ImageView).
<com.yxf.clippathlayout.impl.ClipPathFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/clip_path_frame_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_gravity="center"
android:src="@mipmap/image" />
</com.yxf.clippathlayout.impl.ClipPathFrameLayout>
mImageView = mLayout.findViewById(R.id.image);
然后構(gòu)建一個PathInfo對象
new PathInfo.Builder(new CirclePathGenerator(), mImageView)
.setApplyFlag(mApplyFlag)
.setClipType(mClipType)
.setAntiAlias(false)
.create()
.apply();
搞定!運行就可以看到一個圓形的View.
和效果展示上的這個圖差不多,不過這張圖多了幾個按鈕,然后那個圓形View有個綠色背景,那個是用來做對比的,在那個View之下添加了一個綠色的View,不要在意這些細(xì)節(jié)......
對其中使用到的參數(shù)和方法做下說明
PathInfo.Builder
PathInfo創(chuàng)建器,用于配置和生成PathInfo.
構(gòu)造方法定義如下
/**
* @param generator Path生成器
* @param view 實現(xiàn)了ClipPathLayout接口的ViewGroup的子View
*/
public Builder(PathGenerator generator, View view) {
}
PathGenerator
CirclePathGenerator是一個PathGenerator接口的實現(xiàn)類,用于生成圓形的Path.
PathGenerator定義如下
public interface PathGenerator {
/**
* @param old 以前使用過的Path,如果以前為null,則可能為null
* @param view Path關(guān)聯(lián)的子View對象
* @param width 生成Path所限定的范圍寬度,一般是子View寬度
* @param height 生成Path所限定的范圍高度,一般是子View高度
* @return 返回一個Path對象,必須為閉合的Path,將用于裁剪子View
*
* 其中Path的范圍即left : 0 , top : 0 , right : width , bottom : height
*/
Path generatePath(Path old, View view, int width, int height);
}
PathGenerator是使用的核心,父布局將根據(jù)這個來對子View進行裁剪來實現(xiàn)不規(guī)則圖形.
此庫內(nèi)置了4種Path生成器
- CirclePathGenerator(圓形Path生成器)
- OvalPathGenerator(橢圓Path生成器)
- RhombusPathGenerator(菱形Path生成器)
- OvalRingPathGenerator(橢圓環(huán)Path生成器)
如果有其他復(fù)雜的Path,可以自己實現(xiàn)PathGenerator,可以參考示例中的陰陽魚Path的生成.
ApplyFlag
Path的應(yīng)用標(biāo)志,有如下幾種
- APPLY_FLAG_DRAW_ONLY(只用于繪制)
- APPLY_FLAG_TOUCH_ONLY(只用于觸摸事件)
- APPLY_FLAG_DRAW_AND_TOUCH(繪制和觸摸事件一起應(yīng)用)
默認(rèn)不設(shè)置的話是APPLY_FLAG_DRAW_AND_TOUCH.
切換效果如下
ClipType
Path的裁剪模式,有如下兩種
- CLIP_TYPE_IN(取Path內(nèi)范圍作為不規(guī)則圖形子View)
- CLIP_TYPE_OUT(取Path外范圍作為不規(guī)則圖形子View)
默認(rèn)不設(shè)置為CLIP_TYPE_IN.
切換效果如下
AntiAlias
抗鋸齒,true表示開啟,false關(guān)閉,默認(rèn)關(guān)閉.
請慎用此功能,此功能會關(guān)閉硬件加速并且會新建圖層,在View繪制期間還有一個圖片生成過程,所以此功能開啟會嚴(yán)重降低繪制性能,并且如果頻繁刷新界面會導(dǎo)致內(nèi)存抖動.所以這個功能只建議在靜態(tài)而且不常刷新的情況下使用.
自定義ClipPathLayout
只有三種父布局是不是有點坑?萬一我要用ConstraintLayout呢?那豈不是涼涼.
沒有ConstraintLayout這都被你發(fā)現(xiàn)了.由于ConstraintLayout并不存在于系統(tǒng)標(biāo)準(zhǔn)庫中,而存在于支持庫中,為了減少不必要的引用,讓庫擁有良好的獨立性,故而沒有實現(xiàn)(其實是因為懶...).
好了,其實也可以自己實現(xiàn)了,也是很簡單的操作.
自定義一個ClipPathLayout很簡單,首先選擇一個ViewGroup,然后實現(xiàn)ClipPathLayout接口.
然后再在自定義的ViewGroup中創(chuàng)建一個ClipPathLayoutDelegate對象.
ClipPathLayoutDelegate mClipPathLayoutDelegate = new ClipPathLayoutDelegate(this);
并將所有ClipPathLayout接口的實現(xiàn)都委派給ClipPathLayoutDelegate去實現(xiàn).
這里需要注意兩點:
- 需要重寫ViewGroup的drawChild,按如下實現(xiàn)即可
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
beforeDrawChild(canvas, child, drawingTime);
boolean result = super.drawChild(canvas, child, drawingTime);
afterDrawChild(canvas, child, drawingTime);
return result;
}
- requestLayout方法也需要重寫,這屬于ViewGroup和ClipPathLayout共有的方法,這個方法會在父類的ViewGroup的構(gòu)造方法中調(diào)用,在父類構(gòu)造方法被調(diào)用時,mClipPathLayoutDelegate還沒有初始化,如果直接調(diào)用會報空指針,所以需要添加空判斷.
@Override
public void requestLayout() {
super.requestLayout();
// the request layout method would be invoked in the constructor of super class
if (mClipPathLayoutDelegate == null) {
return;
}
mClipPathLayoutDelegate.requestLayout();
}
這里將整個ClipPathFrameLayout源碼貼出作為參考
public class ClipPathFrameLayout extends FrameLayout implements ClipPathLayout {
ClipPathLayoutDelegate mClipPathLayoutDelegate = new ClipPathLayoutDelegate(this);
public ClipPathFrameLayout(@NonNull Context context) {
this(context, null);
}
public ClipPathFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ClipPathFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) {
return mClipPathLayoutDelegate.isTransformedTouchPointInView(x, y, child, outLocalPoint);
}
@Override
public void applyPathInfo(PathInfo info) {
mClipPathLayoutDelegate.applyPathInfo(info);
}
@Override
public void cancelPathInfo(View child) {
mClipPathLayoutDelegate.cancelPathInfo(child);
}
@Override
public void beforeDrawChild(Canvas canvas, View child, long drawingTime) {
mClipPathLayoutDelegate.beforeDrawChild(canvas, child, drawingTime);
}
@Override
public void afterDrawChild(Canvas canvas, View child, long drawingTime) {
mClipPathLayoutDelegate.afterDrawChild(canvas, child, drawingTime);
}
//the drawChild method is not belong to ClipPathLayout ,
//but you should rewrite it without changing the return value of the method
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
beforeDrawChild(canvas, child, drawingTime);
boolean result = super.drawChild(canvas, child, drawingTime);
afterDrawChild(canvas, child, drawingTime);
return result;
}
//do not forget to rewrite the method
@Override
public void requestLayout() {
super.requestLayout();
// the request layout method would be invoked in the constructor of super class
if (mClipPathLayoutDelegate == null) {
return;
}
mClipPathLayoutDelegate.requestLayout();
}
@Override
public void notifyPathChanged(View child) {
mClipPathLayoutDelegate.notifyPathChanged(child);
}
@Override
public void notifyAllPathChanged() {
mClipPathLayoutDelegate.notifyAllPathChanged();
}
}
原理實現(xiàn)
看完了使用,有沒有覺得非常之簡單,簡單是必須的.
那么想不想了解下原理呢?
不想!
不,我知道,你想!
既然你誠心誠意的想知道,那么我就大發(fā)慈悲的告訴你.
故事說來話長,我們長話短說,不,我們還是慢慢說吧,很久很久以前,有這樣一位少年,這位少年苦修Android,立志要在Android上做一個貪吃蛇游戲,然后這位少年,終于神功有成,開始寫起了他的貪吃蛇游戲.
然而,當(dāng)他寫著寫著,他居然寫出來了.
操,點的按鍵明明是上鍵怎么沒有效果,log怎么打印是左鍵!!!
少年心中有一萬匹草泥馬在心中奔騰.
然后少年開始分析,這是為什么,老天爺為什么要這樣對他.
哇,居然讓他分析出來了......
原來少年的方向按鍵是這個樣子的(原諒我沒有特別好的作圖工具,將就下吧)
很明顯,這4個方向鍵有很多重合的地方,重合的地方就會有一個問題,在重合的地方只有上面的View收得到觸摸事件.那么少年的問題就是觸摸到了重合的地方導(dǎo)致的.
當(dāng)時少年很郁悶啊,網(wǎng)上找了很久,都沒有解決這個問題.然后只好用自定義View的方式,將4個方向鍵做成一個自定義View.問題也算解決了,但是自定義View很麻煩,也不完美,這在少年心里一直是個疙瘩.
前段時間少年不小心給老板發(fā)了一張圖片
然后這位少年意外的獲得了自由,在獲得自由后,少年想起來了久久不能平靜的疙瘩.
少年決定一定要讓這個疙瘩平靜下去,于是少年開始了他新的腦細(xì)胞死亡之路.
少年很快的想到了Path這個可以實現(xiàn)不規(guī)則圖形的關(guān)鍵點,但是要如何應(yīng)用這個Path呢?
應(yīng)用從兩個方面考慮,一個是繪制,一個是觸摸事件.
繪制
先說繪制,繪制的過程比較簡單,查閱下源碼無非就是以下兩種情況
類型 | 過程 |
---|---|
View | draw -> onDraw |
ViewGroup | draw ->dispatchDraw -> drawChild -> child.draw |
draw是final方法沒法重寫,沒戲.View的onDraw,難道每個View都要重寫嗎?那怕不是石樂志.那么只能是diapatchDraw和drawChild了,dispatchDraw邏輯復(fù)雜,drawChild很簡單.很自然的重寫drawChild了.
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
drawChild的實現(xiàn)非常簡單,這是一個非常好的劫持繪制過程的時機.
少年想到只要在這里將Canvas根據(jù)Path進行裁剪,那么不管子View如何繪制,被裁剪掉的部分都不會顯示,這樣說不定還能減少過度繪制的問題.
然后少年修改了drawChild方法
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
beforeDrawChild(canvas, child, drawingTime);
boolean result = super.drawChild(canvas, child, drawingTime);
afterDrawChild(canvas, child, drawingTime);
return result;
}
@Override
public void beforeDrawChild(Canvas canvas, View child, long drawingTime) {
canvas.save();
canvas.translate(child.getLeft(), child.getTop());
if (hasLayoutRequest) {
hasLayoutRequest = false;
notifyAllPathChangedInternal(false);
}
ViewGetKey key = getTempViewGetKey(child.hashCode(), child);
PathInfo info = mPathInfoMap.get(key);
if (info != null) {
if ((info.getApplyFlag() & PathInfo.APPLY_FLAG_DRAW_ONLY) != 0) {
Path path = info.getPath();
if (path != null) {
Utils.clipPath(canvas, path, info.getClipType());
} else {
Log.d(TAG, "beforeDrawChild: path is null , hash code : " + info.hashCode());
}
}
}
resetTempViewGetKey();
canvas.translate(-child.getLeft(), -child.getTop());
}
@Override
public void afterDrawChild(Canvas canvas, View child, long drawingTime) {
canvas.restore();
}
少年成功的劫持了Canvas,然后通過Canvas.clipPath對Canvas進行裁剪,將裁剪后的Canvas再交給子View處理,完美!
觸摸
至于觸摸事件,那就麻煩了,麻煩到炸了好吧.如何應(yīng)用到Path到觸摸事件呢?重寫dispatchTouchEvent嗎?當(dāng)少年打開ViewGroup的源碼,看到200多行,里面還摻雜著各種hide,各種private的方法和成員變量時,少年秒慫了.
但是前段時間知乎大佬出了一個嵌套滑動的庫NestedTouchScrollingLayout給了少年一些靈感,干嘛不直接把onInterceptTouchEvent返回true,然后在onTouchEvent里重寫做事件分發(fā)呢?哇好像可以耶.但是少年又想了想,如果直接攔截,自己又重寫onTouchEvent,這樣子和直接重寫dispatchTouchEvent真的有區(qū)別嗎?在onTouchEvent里寫直接讓原來dispatchTouchEvent的邏輯廢了,還增加了一段流程,可能還會喪失很多特性,制造一些bug,而且onInterceptTouchEvent和onTouchEvent這兩個方法將被占用,后續(xù)繼承的子View可能不能很好的重寫.當(dāng)然直接廢棄掉原生代碼,自己寫一些簡單的操作確實是可行的,但是作為一個有追求的少年,這樣做疙瘩是得不到平靜的.為了讓疙瘩平靜下來,少年開始尋找dispatchTouchEvent中有沒有可以見縫插針的地方.
終于少年找到了這樣一段代碼
//...................................
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//...................................
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//...............................
}
其中canViewReceivePointerEvents是判斷子View是否有資格接收點擊事件的;isTransformedTouchPointInView是判斷觸摸點是否在View中的;而dispatchTransformedTouchEvent,就是判斷是否攔截事件或者分發(fā)給子View的地方.
少年的想法是對View根據(jù)Path進行裁剪實現(xiàn)不規(guī)則形狀的View.那么如果能在isTransformedTouchPointInView中判斷是否在Path內(nèi),則可以實現(xiàn)讓不在Path內(nèi)的點的流程直接continue掉,從而不走dispatchTransformedTouchEvent.
找到一個非常好的想法,少年非常激動.然后點進去isTransformedTouchPointInView方法被潑了一身冷水.
/**
* Returns true if a child view contains the specified point when transformed
* into its coordinate space.
* Child must not be null.
* @hide
*/
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempPoint();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
這個方法居然是hide的!!!!!少年有句mmp當(dāng)時就講了.過了一會少年心情稍微平靜下來,等等,hide的方法只是不能調(diào)用,但是沒定義不能重寫啊,而且這個方法是protected的,完全具備重寫條件.少年又有了激情.
少年繼續(xù)跟蹤里面的transformPointToViewLocal方法
/**
* @hide
*/
public void transformPointToViewLocal(float[] point, View child) {
point[0] += mScrollX - child.mLeft;
point[1] += mScrollY - child.mTop;
if (!child.hasIdentityMatrix()) {
child.getInverseMatrix().mapPoints(point);
}
}
mmp,這又是一個hide方法,但是這下需要的就不是重寫而是調(diào)用了........那么用反射調(diào)用嗎?反射會降低性能啊,Android p又禁反射了,而且各個版本系統(tǒng)代碼不一樣,還不一定有這個方法,呵呵呵,還真被少年猜中了,Android4.4的源碼中沒有這個方法............谷歌,少年一口鹽汽水噴死你!
既然沒有辦法調(diào)用就想想替代方案唄,了解下這個方法干嘛的,不用看都知道,這個方法是將點坐標(biāo)通過View變幻的逆矩陣映射回去看點是否在View內(nèi).很容易重寫嘛,然而谷歌爸爸會讓你這么簡單成功嗎?naive!
/**
* Utility method to retrieve the inverse of the current mMatrix property.
* We cache the matrix to avoid recalculating it when transform properties
* have not changed.
*
* @return The inverse of the current matrix of this view.
* @hide
*/
public final Matrix getInverseMatrix() {
ensureTransformationInfo();
if (mTransformationInfo.mInverseMatrix == null) {
mTransformationInfo.mInverseMatrix = new Matrix();
}
final Matrix matrix = mTransformationInfo.mInverseMatrix;
mRenderNode.getInverseMatrix(matrix);
return matrix;
}
View的getInverseMatrix方法是hide的,驚不驚喜,意不意外!
不是還有mRenderNode.getInverseMatrix嗎?
public void getInverseMatrix(@NonNull Matrix outMatrix) {
nGetInverseTransformMatrix(mNativeRenderNode, outMatrix.native_instance);
}
RenderNode的getInverseMatrix的方法是public的,是不是很高興?
*
* @hide
*/
public class RenderNode {
//...................
}
然而RenderNode連class都是hide的,是不是更高興了,連怎么獲取RenderNode對象都不需要考慮了.
少年并沒有氣餒,不就是個逆矩陣嗎,少年默默在心里念著"谷歌,要是我搞不定,吃我翔".
既然逆矩陣獲取不到那就獲得原矩陣嘛
/**
* The transform matrix of this view, which is calculated based on the current
* rotation, scale, and pivot properties.
*
* @see #getRotation()
* @see #getScaleX()
* @see #getScaleY()
* @see #getPivotX()
* @see #getPivotY()
* @return The current transform matrix for the view
*/
public Matrix getMatrix() {
ensureTransformationInfo();
final Matrix matrix = mTransformationInfo.mMatrix;
mRenderNode.getMatrix(matrix);
return matrix;
}
很幸運,View的getMatrix是public的,而且沒有hide.
逆的過程也很簡單,Android的Matrix提供了一個invert的方法,最終可以用如下方法代替transformPointToViewLocal
private void transformPointToViewLocal(float[] point, View child) {
point[0] += mParent.getScrollX() - child.getLeft();
point[1] += mParent.getScrollY() - child.getTop();
Matrix matrix = child.getMatrix();
if (!matrix.isIdentity()) {
Matrix invert = getTempMatrix();
boolean result = matrix.invert(invert);
if (result) {
invert.mapPoints(point);
}
}
}
然后還有一個問題,關(guān)于如何判斷點是否在Path內(nèi)呢?
這個問題少年只想到了一種比較耗費內(nèi)存的辦法,就是將Path用Canvas繪制成圖片,然后根據(jù)點是否符合圖片里Path內(nèi)的顏色來判斷.這是一種用內(nèi)存換時間的策略,臥槽,講道理豈止是浪費,簡直是鋪張浪費.少年為了節(jié)約內(nèi)存,將圖片大小縮小了16倍,這樣問題應(yīng)該不大了.少年百度查了下,貌似還有一個Region類可以實現(xiàn)是否在Path內(nèi)判斷,但是資料其實不多,而且估計每次點都需要計算是否在Path內(nèi).少年覺得這種方式?jīng)]有轉(zhuǎn)化成圖片穩(wěn),所以當(dāng)時默認(rèn)采用了圖片的方式作為判斷.
然后這里出現(xiàn)了一個轉(zhuǎn)折,鴻神看到這部分問題的時候給了少年一個方案,就是用自帶的Region類來實現(xiàn),既然大佬都覺得這個方式更為合適,少年決定去嘗試一波,通過Region類實現(xiàn)PathRegion接口替換掉原來的BitmapPathRegion,確實實現(xiàn)了對是否在Path閉合空間的判斷,不過少年有點在意其性能是否會比用Bitmap的方式更好呢?少年追蹤了下Region類的實現(xiàn),發(fā)現(xiàn)其實現(xiàn)基本上是調(diào)用jni實現(xiàn)的,然后jni中的Region類也只是對skia庫中SkRegion的裝封而已.也就是說最終實現(xiàn)是由skia庫的SkRegion實現(xiàn)的,以前沒怎么注意,追下源碼才發(fā)現(xiàn),Path類其實也是skia里的,百度查了下才知道,Android的2D繪圖都是skia實現(xiàn)的.大概的查閱了下SkRegion.contains的方法
bool SkRegion::contains(int32_t x, int32_t y) const {
SkDEBUGCODE(this->validate();)
if (!fBounds.contains(x, y)) {
return false;
}
if (this->isRect()) {
return true;
}
SkASSERT(this->isComplex());
const RunType* runs = fRunHead->findScanline(y);
// Skip the Bottom and IntervalCount
runs += 2;
// Just walk this scanline, checking each interval. The X-sentinel will
// appear as a left-inteval (runs[0]) and should abort the search.
//
// We could do a bsearch, using interval-count (runs[1]), but need to time
// when that would be worthwhile.
//
for (;;) {
if (x < runs[0]) {
break;
}
if (x < runs[1]) {
return true;
}
runs += 2;
}
return false;
}
發(fā)現(xiàn)其對于非矩形的區(qū)域的實現(xiàn)是以y作為掃描線,然后獲得這個掃描線上的數(shù)組,數(shù)組中兩個相鄰值儲存著一個區(qū)間,如果前一個區(qū)間沒找到則繼續(xù)在下一個區(qū)間尋找,找到則返回true,理解不深,不知道理解是否有不合理之處,歡迎指正.
這種方式比bitmap省了很多空間,然后2D繪制這些本就是skia這一套的東西,又是C++實現(xiàn),所以可以認(rèn)為這種方式確實比使用Bitmap更為合適,當(dāng)前已經(jīng)在源碼中默認(rèn)使用這種方式作為點是否在Path中的判斷.
那么原理就講到這里就講完了,具體如何實現(xiàn)的,自己看源碼去吧.文章底放GitHub地址.
轉(zhuǎn)場動畫擴展
基于ClipPathLayout還可以實現(xiàn)轉(zhuǎn)場動畫的擴展,先放些效果.
兩個View的場景切換效果,Android原生自帶的場景切換效果大部分是由動畫實現(xiàn)的平移,縮小,暗淡.
原生比較少帶有那種PPT播放的切換效果,一些第三方庫實現(xiàn)的效果一般是由在DecorView中添加一層View來實現(xiàn)較為和諧的切換,
滬江開心詞場里使用的就是這種動畫,這種動畫很棒,但是也有一個小缺點,就是在切換的過程中,切換用的View和即將要切換的View沒有什么關(guān)系,只是顏色類似.
借助于ClipPathLayout擴展的TransitionFrameLayout也可以實現(xiàn)較為和諧的切換效果,由于是示例,不寫太復(fù)雜的場景,以下僅用兩個TextView作為展示
在瀏覽QQ空間和使用QQ瀏覽器的過程看到騰訊的廣告切換效果也是很不錯的,這里借助于TransitionFrameLayout也可以實現(xiàn)這種效果
其實大部分的場景切換應(yīng)該是用在Fragment中,這里也用TransitionFragmentContainer實現(xiàn)了Fragment的場景切換效果
使用和實現(xiàn)部分放在下篇基于ClipPathLayout轉(zhuǎn)場動畫布局的實現(xiàn)講解.