Android中不規(guī)則形狀View的布局實(shí)現(xiàn)

Android中不規(guī)則形狀View的布局實(shí)現(xiàn)

在Android中不管是View還是ViewGroup,都是方的! 方的! 方的!

而對(duì)于非方形的,Android官方并沒(méi)有給出非常好的解決方案.有的無(wú)非就是自定義View了.
然而自定義View非常麻煩,需要重寫很多方法,而且稍微不注意可能就會(huì)喪失一些特性或者造成一些Bug.

而且即便是自定義View,其實(shí)那個(gè)自定義View還是方的!!!,自定義View所能做的也就是繪制非方的圖形,但是其觸摸區(qū)域還是方的,如果需要讓一些區(qū)域觸摸無(wú)效,需要在onTouchEvent中嚴(yán)謹(jǐn)?shù)挠?jì)算,而這只是僅僅針對(duì)View而言,如果這個(gè)View是ViewGroup,則需要重寫dispatchTouchEvent,dispatchToucEvent的邏輯相比于onTouchEvent的處理邏輯復(fù)雜多了.

而此時(shí)此刻,ClipPathLayout孕育而生,非常好的解決了這個(gè)問(wèn)題.

何為ClipPathLayout,顧名思義,這就是一個(gè)可以對(duì)子View的Path進(jìn)行裁剪的布局.

那么這個(gè)布局有什么作用呢?

問(wèn)的好,這個(gè)布局可以對(duì)其子View的繪制范圍和觸摸范圍進(jìn)行裁剪,進(jìn)而實(shí)現(xiàn)不規(guī)則形狀的View.

光說(shuō)有啥用.

那就亮出來(lái)給你們看看效果.

效果展示

將方形圖片裁剪成圓形并且讓圓形View的4角不接收觸摸事件

image

很多游戲都會(huì)有方向鍵,曾經(jīng)我也做過(guò)一個(gè)小游戲,但是在做方向鍵的時(shí)候遇到一個(gè)問(wèn)題,4個(gè)方向按鈕的位置會(huì)有重疊,導(dǎo)致局部地方會(huì)發(fā)生誤觸.
當(dāng)時(shí)沒(méi)有特別好的解決辦法,只能做自定義View,而自定義View特別麻煩,需要重寫onTouchEvent和onDraw計(jì)算落點(diǎn)屬于哪個(gè)方向,并增加點(diǎn)擊效果.
簡(jiǎn)單的自定義View會(huì)喪失很多Android自帶的一些特性,要支持這些特性又繁瑣而復(fù)雜.
下面借助于ClipPathLayout用4個(gè)菱形按鈕實(shí)現(xiàn)的方向控制鍵很好的解決了這個(gè)問(wèn)題

image

對(duì)于遙控器的按鍵的模擬同樣有上述問(wèn)題,一般只能采用自定義View實(shí)現(xiàn),較為繁瑣.
以下是借助于ClipPathLayout實(shí)現(xiàn)的遙控器按鈕,由于沒(méi)有美工切圖,比較丑,將就下吧

image

甚至我們可以將不連續(xù)的圖形變成一個(gè)View,比如做一個(gè)陰陽(yáng)魚的按鈕

image

使用

效果展示完了,那么如何使用呢?使用太麻煩也是白搭.

那么接下來(lái)就講下如何使用.

添加依賴

庫(kù)已經(jīng)上傳jcenter,Android Studio自帶jcenter依賴,
如果沒(méi)有添加,請(qǐng)?jiān)陧?xiàng)目根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.+'

其實(shí)ClipPathLayout只是一個(gè)接口,大部分的ViewGroup,實(shí)現(xiàn)這個(gè)接口都可以實(shí)現(xiàn)對(duì)不規(guī)則圖形的布局,并且保留父類ViewGroup的特性.

當(dāng)前實(shí)現(xiàn)了三個(gè)不規(guī)則圖形的布局,分別是

  • ClipPathFrameLayout
  • ClipPathLinearLayout
  • ClipPathRelativeLayout

如果有其他布局要求,請(qǐng)自定義,參見(jiàn)自定義ClipPathLayout

那么父布局要如何知道其子View應(yīng)該是何形狀呢?那必然需要給子View做自定義屬性吧,很顯然去重寫子View添加自定義屬性是不合理的.那么就采用外部關(guān)聯(lián)的方式好了.還有一個(gè)問(wèn)題,什么屬性可以定義各種各樣的形狀呢?思來(lái)想去怕是也只有閉合的Path了吧,嗯,沒(méi)錯(cuò),就是借助于Path,并且讓子View和這個(gè)Path關(guān)聯(lián),然后把這些信息告訴父布局,這樣父布局才知道應(yīng)該如何去控制這個(gè)子View的形狀.

光說(shuō)理論有什么用,來(lái)點(diǎn)實(shí)際的啊!

好,那就來(lái)點(diǎn)實(shí)際的.這里以最簡(jiǎn)單的圓形View為例.

在一個(gè)實(shí)現(xiàn)了ClipPathLayout接口的ViewGroup(以ClipPathFrameLayout為例)中添加一個(gè)子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)建一個(gè)PathInfo對(duì)象

new PathInfo.Builder(new CirclePathGenerator(), mImageView)
    .setApplyFlag(mApplyFlag)
    .setClipType(mClipType)
    .setAntiAlias(false)
    .create()
    .apply();

搞定!運(yùn)行就可以看到一個(gè)圓形的View.

image

和效果展示上的這個(gè)圖差不多,不過(guò)這張圖多了幾個(gè)按鈕,然后那個(gè)圓形View有個(gè)綠色背景,那個(gè)是用來(lái)做對(duì)比的,在那個(gè)View之下添加了一個(gè)綠色的View,不要在意這些細(xì)節(jié)......

對(duì)其中使用到的參數(shù)和方法做下說(shuō)明

PathInfo.Builder

PathInfo創(chuàng)建器,用于配置和生成PathInfo.

構(gòu)造方法定義如下

        /**
         * @param generator Path生成器
         * @param view 實(shí)現(xiàn)了ClipPathLayout接口的ViewGroup的子View
         */
        public Builder(PathGenerator generator, View view) {
        
        }

PathGenerator

CirclePathGenerator是一個(gè)PathGenerator接口的實(shí)現(xiàn)類,用于生成圓形的Path.

PathGenerator定義如下

public interface PathGenerator {

    /**
     * @param old 以前使用過(guò)的Path,如果以前為null,則可能為null
     * @param view Path關(guān)聯(lián)的子View對(duì)象
     * @param width 生成Path所限定的范圍寬度,一般是子View寬度
     * @param height 生成Path所限定的范圍高度,一般是子View高度
     * @return 返回一個(gè)Path對(duì)象,必須為閉合的Path,將用于裁剪子View
     * 
     * 其中Path的范圍即left : 0 , top : 0 , right : width , bottom : height
     */
    Path generatePath(Path old, View view, int width, int height);

}

PathGenerator是使用的核心,父布局將根據(jù)這個(gè)來(lái)對(duì)子View進(jìn)行裁剪來(lái)實(shí)現(xiàn)不規(guī)則圖形.

此庫(kù)內(nèi)置了4種Path生成器

  • CirclePathGenerator(圓形Path生成器)
  • OvalPathGenerator(橢圓Path生成器)
  • RhombusPathGenerator(菱形Path生成器)
  • OvalRingPathGenerator(橢圓環(huán)Path生成器)

如果有其他復(fù)雜的Path,可以自己實(shí)現(xiàn)PathGenerator,可以參考示例中的陰陽(yáng)魚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.

切換效果如下

image

ClipType

Path的裁剪模式,有如下兩種

  • CLIP_TYPE_IN(取Path內(nèi)范圍作為不規(guī)則圖形子View)
  • CLIP_TYPE_OUT(取Path外范圍作為不規(guī)則圖形子View)

默認(rèn)不設(shè)置為CLIP_TYPE_IN.

切換效果如下

image

AntiAlias

抗鋸齒,true表示開啟,false關(guān)閉,默認(rèn)關(guān)閉.

請(qǐng)慎用此功能,此功能會(huì)關(guān)閉硬件加速并且會(huì)新建圖層,在View繪制期間還有一個(gè)圖片生成過(guò)程,所以此功能開啟會(huì)嚴(yán)重降低繪制性能,并且如果頻繁刷新界面會(huì)導(dǎo)致內(nèi)存抖動(dòng).所以這個(gè)功能只建議在靜態(tài)而且不常刷新的情況下使用.

自定義ClipPathLayout

只有三種父布局是不是有點(diǎn)坑?萬(wàn)一我要用ConstraintLayout呢?那豈不是涼涼.

沒(méi)有ConstraintLayout這都被你發(fā)現(xiàn)了.由于ConstraintLayout并不存在于系統(tǒng)標(biāo)準(zhǔn)庫(kù)中,而存在于支持庫(kù)中,為了減少不必要的引用,讓庫(kù)擁有良好的獨(dú)立性,故而沒(méi)有實(shí)現(xiàn)(其實(shí)是因?yàn)閼?..).

好了,其實(shí)也可以自己實(shí)現(xiàn)了,也是很簡(jiǎn)單的操作.

自定義一個(gè)ClipPathLayout很簡(jiǎn)單,首先選擇一個(gè)ViewGroup,然后實(shí)現(xiàn)ClipPathLayout接口.

然后再在自定義的ViewGroup中創(chuàng)建一個(gè)ClipPathLayoutDelegate對(duì)象.

ClipPathLayoutDelegate mClipPathLayoutDelegate = new ClipPathLayoutDelegate(this);

并將所有ClipPathLayout接口的實(shí)現(xiàn)都委派給ClipPathLayoutDelegate去實(shí)現(xiàn).

這里需要注意兩點(diǎn):

  • 需要重寫ViewGroup的drawChild,按如下實(shí)現(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共有的方法,這個(gè)方法會(huì)在父類的ViewGroup的構(gòu)造方法中調(diào)用,在父類構(gòu)造方法被調(diào)用時(shí),mClipPathLayoutDelegate還沒(méi)有初始化,如果直接調(diào)用會(huì)報(bà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();
    }

這里將整個(gè)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();
    }
}

原理實(shí)現(xiàn)

看完了使用,有沒(méi)有覺(jué)得非常之簡(jiǎn)單,簡(jiǎn)單是必須的.

那么想不想了解下原理呢?

不想!

不,我知道,你想!

既然你誠(chéng)心誠(chéng)意的想知道,那么我就大發(fā)慈悲的告訴你.

故事說(shuō)來(lái)話長(zhǎng),我們長(zhǎng)話短說(shuō),不,我們還是慢慢說(shuō)吧,很久很久以前,有這樣一位少年,這位少年苦修Android,立志要在Android上做一個(gè)貪吃蛇游戲,然后這位少年,終于神功有成,開始寫起了他的貪吃蛇游戲.

然而,當(dāng)他寫著寫著,他居然寫出來(lái)了.

操,點(diǎn)的按鍵明明是上鍵怎么沒(méi)有效果,log怎么打印是左鍵!!!

少年心中有一萬(wàn)匹草泥馬在心中奔騰.

然后少年開始分析,這是為什么,老天爺為什么要這樣對(duì)他.

哇,居然讓他分析出來(lái)了......

原來(lái)少年的方向按鍵是這個(gè)樣子的(原諒我沒(méi)有特別好的作圖工具,將就下吧)

image

很明顯,這4個(gè)方向鍵有很多重合的地方,重合的地方就會(huì)有一個(gè)問(wèn)題,在重合的地方只有上面的View收得到觸摸事件.那么少年的問(wèn)題就是觸摸到了重合的地方導(dǎo)致的.

當(dāng)時(shí)少年很郁悶啊,網(wǎng)上找了很久,都沒(méi)有解決這個(gè)問(wèn)題.然后只好用自定義View的方式,將4個(gè)方向鍵做成一個(gè)自定義View.問(wèn)題也算解決了,但是自定義View很麻煩,也不完美,這在少年心里一直是個(gè)疙瘩.

前段時(shí)間少年不小心給老板發(fā)了一張圖片

image

然后這位少年意外的獲得了自由,在獲得自由后,少年想起來(lái)了久久不能平靜的疙瘩.

少年決定一定要讓這個(gè)疙瘩平靜下去,于是少年開始了他新的腦細(xì)胞死亡之路.

少年很快的想到了Path這個(gè)可以實(shí)現(xiàn)不規(guī)則圖形的關(guān)鍵點(diǎn),但是要如何應(yīng)用這個(gè)Path呢?
應(yīng)用從兩個(gè)方面考慮,一個(gè)是繪制,一個(gè)是觸摸事件.

繪制

先說(shuō)繪制,繪制的過(guò)程比較簡(jiǎn)單,查閱下源碼無(wú)非就是以下兩種情況

類型 過(guò)程
View draw -> onDraw
ViewGroup draw ->dispatchDraw -> drawChild -> child.draw

draw是final方法沒(méi)法重寫,沒(méi)戲.View的onDraw,難道每個(gè)View都要重寫嗎?那怕不是石樂(lè)志.那么只能是diapatchDraw和drawChild了,dispatchDraw邏輯復(fù)雜,drawChild很簡(jiǎn)單.很自然的重寫drawChild了.

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

drawChild的實(shí)現(xiàn)非常簡(jiǎn)單,這是一個(gè)非常好的劫持繪制過(guò)程的時(shí)機(jī).

少年想到只要在這里將Canvas根據(jù)Path進(jìn)行裁剪,那么不管子View如何繪制,被裁剪掉的部分都不會(huì)顯示,這樣說(shuō)不定還能減少過(guò)度繪制的問(wèn)題.
然后少年修改了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,然后通過(guò)Canvas.clipPath對(duì)Canvas進(jìn)行裁剪,將裁剪后的Canvas再交給子View處理,完美!

觸摸

至于觸摸事件,那就麻煩了,麻煩到炸了好吧.如何應(yīng)用到Path到觸摸事件呢?重寫dispatchTouchEvent嗎?當(dāng)少年打開ViewGroup的源碼,看到200多行,里面還摻雜著各種hide,各種private的方法和成員變量時(shí),少年秒慫了.

但是前段時(shí)間知乎大佬出了一個(gè)嵌套滑動(dòng)的庫(kù)NestedTouchScrollingLayout給了少年一些靈感,干嘛不直接把onInterceptTouchEvent返回true,然后在onTouchEvent里重寫做事件分發(fā)呢?哇好像可以耶.但是少年又想了想,如果直接攔截,自己又重寫onTouchEvent,這樣子和直接重寫dispatchTouchEvent真的有區(qū)別嗎?在onTouchEvent里寫直接讓原來(lái)dispatchTouchEvent的邏輯廢了,還增加了一段流程,可能還會(huì)喪失很多特性,制造一些bug,而且onInterceptTouchEvent和onTouchEvent這兩個(gè)方法將被占用,后續(xù)繼承的子View可能不能很好的重寫.當(dāng)然直接廢棄掉原生代碼,自己寫一些簡(jiǎn)單的操作確實(shí)是可行的,但是作為一個(gè)有追求的少年,這樣做疙瘩是得不到平靜的.為了讓疙瘩平靜下來(lái),少年開始尋找dispatchTouchEvent中有沒(méi)有可以見(jiàn)縫插針的地方.

終于少年找到了這樣一段代碼

        //...................................
        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

        //...................................
        
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            //...............................
        }

其中canViewReceivePointerEvents是判斷子View是否有資格接收點(diǎn)擊事件的;isTransformedTouchPointInView是判斷觸摸點(diǎn)是否在View中的;而dispatchTransformedTouchEvent,就是判斷是否攔截事件或者分發(fā)給子View的地方.

少年的想法是對(duì)View根據(jù)Path進(jìn)行裁剪實(shí)現(xiàn)不規(guī)則形狀的View.那么如果能在isTransformedTouchPointInView中判斷是否在Path內(nèi),則可以實(shí)現(xiàn)讓不在Path內(nèi)的點(diǎn)的流程直接continue掉,從而不走dispatchTransformedTouchEvent.

找到一個(gè)非常好的想法,少年非常激動(dòng).然后點(diǎn)進(jìn)去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;
    }

這個(gè)方法居然是hide的!!!!!少年有句mmp當(dāng)時(shí)就講了.過(guò)了一會(huì)少年心情稍微平靜下來(lái),等等,hide的方法只是不能調(diào)用,但是沒(méi)定義不能重寫啊,而且這個(gè)方法是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,這又是一個(gè)hide方法,但是這下需要的就不是重寫而是調(diào)用了........那么用反射調(diào)用嗎?反射會(huì)降低性能啊,Android p又禁反射了,而且各個(gè)版本系統(tǒng)代碼不一樣,還不一定有這個(gè)方法,呵呵呵,還真被少年猜中了,Android4.4的源碼中沒(méi)有這個(gè)方法............谷歌,少年一口鹽汽水噴死你!

既然沒(méi)有辦法調(diào)用就想想替代方案唄,了解下這個(gè)方法干嘛的,不用看都知道,這個(gè)方法是將點(diǎn)坐標(biāo)通過(guò)View變幻的逆矩陣映射回去看點(diǎn)是否在View內(nèi).很容易重寫嘛,然而谷歌爸爸會(huì)讓你這么簡(jiǎn)單成功嗎?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對(duì)象都不需要考慮了.

少年并沒(méi)有氣餒,不就是個(gè)逆矩陣嗎,少年默默在心里念著"谷歌,要是我搞不定,吃我翔".

既然逆矩陣獲取不到那就獲得原矩陣嘛

    /**
     * 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;
    }

很幸運(yùn),View的getMatrix是public的,而且沒(méi)有hide.

逆的過(guò)程也很簡(jiǎn)單,Android的Matrix提供了一個(gè)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);
            }
        }
    }

然后還有一個(gè)問(wèn)題,關(guān)于如何判斷點(diǎn)是否在Path內(nèi)呢?

這個(gè)問(wèn)題少年只想到了一種比較耗費(fèi)內(nèi)存的辦法,就是將Path用Canvas繪制成圖片,然后根據(jù)點(diǎn)是否符合圖片里Path內(nèi)的顏色來(lái)判斷.這是一種用內(nèi)存換時(shí)間的策略,臥槽,講道理豈止是浪費(fèi),簡(jiǎn)直是鋪張浪費(fèi).少年為了節(jié)約內(nèi)存,將圖片大小縮小了16倍,這樣問(wèn)題應(yīng)該不大了.少年百度查了下,貌似還有一個(gè)Region類可以實(shí)現(xiàn)是否在Path內(nèi)判斷,但是資料其實(shí)不多,而且估計(jì)每次點(diǎn)都需要計(jì)算是否在Path內(nèi).少年覺(jué)得這種方式?jīng)]有轉(zhuǎn)化成圖片穩(wěn),所以當(dāng)時(shí)默認(rèn)采用了圖片的方式作為判斷.

然后這里出現(xiàn)了一個(gè)轉(zhuǎn)折,鴻神看到這部分問(wèn)題的時(shí)候給了少年一個(gè)方案,就是用自帶的Region類來(lái)實(shí)現(xiàn),既然大佬都覺(jué)得這個(gè)方式更為合適,少年決定去嘗試一波,通過(guò)Region類實(shí)現(xiàn)PathRegion接口替換掉原來(lái)的BitmapPathRegion,確實(shí)實(shí)現(xiàn)了對(duì)是否在Path閉合空間的判斷,不過(guò)少年有點(diǎn)在意其性能是否會(huì)比用Bitmap的方式更好呢?少年追蹤了下Region類的實(shí)現(xiàn),發(fā)現(xiàn)其實(shí)現(xiàn)基本上是調(diào)用jni實(shí)現(xiàn)的,然后jni中的Region類也只是對(duì)skia庫(kù)中SkRegion的裝封而已.也就是說(shuō)最終實(shí)現(xiàn)是由skia庫(kù)的SkRegion實(shí)現(xiàn)的,以前沒(méi)怎么注意,追下源碼才發(fā)現(xiàn),Path類其實(shí)也是skia里的,百度查了下才知道,Android的2D繪圖都是skia實(shí)現(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)其對(duì)于非矩形的區(qū)域的實(shí)現(xiàn)是以y作為掃描線,然后獲得這個(gè)掃描線上的數(shù)組,數(shù)組中兩個(gè)相鄰值儲(chǔ)存著一個(gè)區(qū)間,如果前一個(gè)區(qū)間沒(méi)找到則繼續(xù)在下一個(gè)區(qū)間尋找,找到則返回true,理解不深,不知道理解是否有不合理之處,歡迎指正.

這種方式比bitmap省了很多空間,然后2D繪制這些本就是skia這一套的東西,又是C++實(shí)現(xiàn),所以可以認(rèn)為這種方式確實(shí)比使用Bitmap更為合適,當(dāng)前已經(jīng)在源碼中默認(rèn)使用這種方式作為點(diǎn)是否在Path中的判斷.

那么原理就講到這里就講完了,具體如何實(shí)現(xiàn)的,自己看源碼去吧.文章底放GitHub地址.

轉(zhuǎn)場(chǎng)動(dòng)畫擴(kuò)展

基于ClipPathLayout還可以實(shí)現(xiàn)轉(zhuǎn)場(chǎng)動(dòng)畫的擴(kuò)展,先放些效果.

兩個(gè)View的場(chǎng)景切換效果,Android原生自帶的場(chǎng)景切換效果大部分是由動(dòng)畫實(shí)現(xiàn)的平移,縮小,暗淡.
原生比較少帶有那種PPT播放的切換效果,一些第三方庫(kù)實(shí)現(xiàn)的效果一般是由在DecorView中添加一層View來(lái)實(shí)現(xiàn)較為和諧的切換,
滬江開心詞場(chǎng)里使用的就是這種動(dòng)畫,這種動(dòng)畫很棒,但是也有一個(gè)小缺點(diǎn),就是在切換的過(guò)程中,切換用的View和即將要切換的View沒(méi)有什么關(guān)系,只是顏色類似.
借助于ClipPathLayout擴(kuò)展的TransitionFrameLayout也可以實(shí)現(xiàn)較為和諧的切換效果,由于是示例,不寫太復(fù)雜的場(chǎng)景,以下僅用兩個(gè)TextView作為展示

image

在瀏覽QQ空間和使用QQ瀏覽器的過(guò)程看到騰訊的廣告切換效果也是很不錯(cuò)的,這里借助于TransitionFrameLayout也可以實(shí)現(xiàn)這種效果

image

其實(shí)大部分的場(chǎng)景切換應(yīng)該是用在Fragment中,這里也用TransitionFragmentContainer實(shí)現(xiàn)了Fragment的場(chǎng)景切換效果

image

使用和實(shí)現(xiàn)部分放在下篇基于ClipPathLayout轉(zhuǎn)場(chǎng)動(dòng)畫布局的實(shí)現(xiàn)講解.

GitHub地址

ClipPathLayout

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 你問(wèn)我出生前在做什么,我答我在天上挑媽媽??匆?jiàn)你了,覺(jué)得你特別好,想做你的兒子。又覺(jué)得自己可能沒(méi)那個(gè)運(yùn)氣。沒(méi)想到,...
    甘南2018閱讀 3,121評(píng)論 3 14
  • 01 如果不是認(rèn)識(shí)他媽,我簡(jiǎn)直不敢相信這孩子是20歲。 他從不直視我的眼睛,眼神好像神游于現(xiàn)世之外。他說(shuō)的話,像是...
    院長(zhǎng)X大叔閱讀 1,586評(píng)論 20 41
  • 熊志軍~【日精進(jìn)打卡第569】 12月9號(hào)卡 付達(dá)新商貿(mào)~眾德?tīng)I(yíng)銷 沈陽(yáng)盛和塾道盛組/稻芽七組 【知~學(xué)習(xí)】 ■早...
    熊志軍閱讀 245評(píng)論 0 0
  • 今天一樣忙著給tp4打課前電話,空擋時(shí)間給谷開峰電話了,問(wèn)他最近怎么樣,宣言書上做到多少,都是表現(xiàn)出一個(gè)很好的態(tài)度...
    Hi_張閱讀 207評(píng)論 0 0

友情鏈接更多精彩內(nèi)容