[趕時(shí)髦]我的一個(gè)自定義滑動(dòng)特效的Gallery及其Kotlin重構(gòu)

0x0 前言

今年五月底針對(duì)Android app開發(fā)的kotlin終于轉(zhuǎn)正成功,這掀起了一股學(xué)習(xí)kotlin的熱潮。于是我也趕了一下時(shí)髦,看了幾天官網(wǎng)上的文檔,大致了解了一下kotlin的語(yǔ)法,也手癢癢著想做點(diǎn)什么,正巧之前的項(xiàng)目里有一個(gè)自定義view想剝離出來(lái)寫成一個(gè)lib供以后其他項(xiàng)目復(fù)用,就用kotlin寫來(lái)練練手吧。

這個(gè)自定義view實(shí)現(xiàn)功能類似一個(gè)Gallery,可以左右滑動(dòng)切換圖片,每張圖片安裝原始分辨率顯示,如果圖片的分辨率高于屏幕,可通過(guò)手勢(shì)滑動(dòng)圖片顯示超出屏幕的部分。

之所以不用系統(tǒng)自帶的Gallery,是因?yàn)槲也惶珴M意那個(gè)gallery的滑動(dòng)特效,以及當(dāng)顯示圖片分辨率大于屏幕時(shí)默認(rèn)做縮小處理的特性,正巧我也需要一個(gè)機(jī)會(huì)學(xué)習(xí)寫自定義的view,于是就自己寫了。最終效果如下:

device-2017-08-22-000814.gif
device-2017-08-05-231517.gif

0x1 簡(jiǎn)單的實(shí)現(xiàn)原理

這個(gè)東西最初版本是用java寫的,我就先以java版本為例簡(jiǎn)要介紹一下它的實(shí)現(xiàn)原理,想要更深入的了解感興趣的細(xì)節(jié),可以參考我上傳到github上的源碼:

https://github.com/knightingal/KSlideView/tree/java_init

布局文件中加載YImageSlider這個(gè)view的子類,這個(gè)類初始化時(shí)會(huì)添加三個(gè)YImageViewhideLeftcontentView,hideRight。YImageSlider主要任務(wù)是管理這三個(gè)YImageView在滑動(dòng)中的切換。假設(shè)我們有一組數(shù)量為n的圖片pic[n],當(dāng)前顯示的是圖片中的第i張圖pic[i],則由contentView加載pic[i]顯示于屏幕上,而hideLefthideRight分別加載pic[i-1],pic[i+1]隱藏于屏幕的左右兩側(cè),當(dāng)我們通過(guò)手勢(shì)將contentView向左滑動(dòng)導(dǎo)致當(dāng)前顯示的圖片切換至pic[i+1]時(shí),hideRight變?yōu)樾碌?code>contentView,contentView變?yōu)樾碌?code>hideLeft,hideLeft變?yōu)樾碌?code>hideRight,在這個(gè)過(guò)程中,原先的hideLeft即新的hideRight會(huì)丟棄原先加載的pic[i-1],轉(zhuǎn)而加載pic[i+2]。由此可知無(wú)論n為多大,同一時(shí)間YImageSlider只需要加載三張圖片在內(nèi)存中,在一次切換過(guò)程中只重新加載一張圖片。

YImageView主要任務(wù)是根據(jù)自己的定位(hideLeftcontentView,hideRight),將圖片按照正確的大小顯示在正確的位置,以及處理在手勢(shì)滑動(dòng)和圖片切換時(shí)的動(dòng)畫處理。
YImageView首先override了setFrame方法,根據(jù)localIndex獲取自己在YImageSlider中的定位信息,再根據(jù)圖片的長(zhǎng)寬,計(jì)算lefttop,right,bottom數(shù)值,最后調(diào)用方法super.setFrame(0, top,bitmap_W, top +bitmap_H);將圖片安照原始分辨率顯示。

而手勢(shì)滑動(dòng)效果,和其他自定義view一樣,主要由override onTouchEvent(MotionEventevent)方法實(shí)現(xiàn),onTouchDown和onTouchMove完成對(duì)手勢(shì)的跟蹤,計(jì)算圖片當(dāng)前位置和運(yùn)動(dòng)速度,onTouchUp則略微復(fù)雜,它首先需要判斷是否需要進(jìn)行圖片的切換,如果不需要進(jìn)行圖片切換,則僅完成圖片的滑動(dòng)和回彈即可?;瑒?dòng)和回彈均模擬經(jīng)典力學(xué)中的勻變速運(yùn)動(dòng)。我設(shè)置了固定的動(dòng)畫運(yùn)行時(shí)間,那么在知道初始速度的情況下,套用勻變速運(yùn)動(dòng)公式就可以計(jì)算出當(dāng)動(dòng)畫停止時(shí),我們的物體,即圖片滑動(dòng)的距離。這里還有一個(gè)問(wèn)題,就是在滑動(dòng)過(guò)程中圖片的某一邊緣越過(guò)了屏幕的邊緣,需要進(jìn)行回彈。假設(shè)滑動(dòng)過(guò)程中只有圖片的左側(cè)邊緣越過(guò)屏幕左側(cè)邊緣,而上下邊緣都沒(méi)有越過(guò)屏幕邊緣,則只需要在x軸方向上進(jìn)行滑動(dòng)+回彈,y軸方向只需滑動(dòng)。為了使動(dòng)畫自然,x軸的滑動(dòng)+回彈動(dòng)畫必須和y軸的滑動(dòng)動(dòng)畫同時(shí)結(jié)束。那么在計(jì)算x軸的滑動(dòng)動(dòng)畫的參數(shù)時(shí)必須調(diào)整動(dòng)畫預(yù)設(shè)的持續(xù)時(shí)間,以便空出時(shí)間完成回彈的動(dòng)畫?;貜梽?dòng)畫的計(jì)算就很簡(jiǎn)單了,只需在滑動(dòng)動(dòng)畫結(jié)束以后(onAnimationEnd(Animatoranimation)回調(diào)方法中),再追加一個(gè)以當(dāng)前位置為起點(diǎn),以屏幕邊緣為終點(diǎn),總持續(xù)時(shí)間減去滑動(dòng)動(dòng)畫持續(xù)時(shí)間的剩余為持續(xù)時(shí)間的勻加速動(dòng)畫即可。

0x2 kotlin實(shí)現(xiàn)

最終用kotlin實(shí)現(xiàn)的版本在這里

https://github.com/knightingal/KSlideView

用kotlin重寫的KSlideView和java版本在邏輯上是一樣的。不同之處在于采用了一些kotlin特有的語(yǔ)法。

去getter和setter

這大概是kotlin的諸多語(yǔ)法糖中最為人熟知,也是最簡(jiǎn)便易用的。它的普遍適用性讓人甚至察覺不到它的存在。比如在java版本的YImageView中大量存在的this.getY(),this.getX()等,在kotlin中只需要一個(gè)yx即可。

setXE.setDuration(duration);
setXE.setInterpolator(new AccelerateInterpolator());

在kotlin中可以簡(jiǎn)化為

setXE.duration = duration
setXE.interpolator = AccelerateInterpolator()

盡管View.xView.y,AnimatorSet.durationAnimatorSet.interpolator本身依然是定義在java代碼中的私有成員變量。只要提供了標(biāo)準(zhǔn)的setter和getter方法,kotlin就可以按照公共成員變量一樣對(duì)其進(jìn)行訪問(wèn)。

這樣的語(yǔ)法糖在kotlin中隨處可見俯拾皆是,這里就不一一列舉了。

if expression

kotlin中另一個(gè)有趣的語(yǔ)法糖。它可以將java中的類似

int max;
if (a > b) {
    max = a;
} else {
    max = b;
}

的結(jié)構(gòu)轉(zhuǎn)化為

val max = if (a > b) a else b

有些類似java中的三元操作符?:,但是比三元操作符更具擴(kuò)展性,可以容納更多了邏輯操作,比如這樣

val max = if (a > b) {
    print("Choose a")
    a
} else {
    print("Choose b")
    b
}

于是我們可以看到原先java代碼中的這樣的代碼塊:

if (locationIndex == 1) {
    if (yImageSlider.getAlingLeftOrRight() == 0) {
        left = contentImageWidth + YImageSlider.SPLITE_W;
        top = 0;
        right = contentImageWidth + YImageSlider.SPLITE_W + bitmap_W;
        bottom = bitmap_H;
    } else {
        left = screamW + YImageSlider.SPLITE_W;
        top = 0;
        right = screamW + YImageSlider.SPLITE_W + bitmap_W;
        bottom = bitmap_H;
    }
} else if (locationIndex == -1) {
    if (yImageSlider.getAlingLeftOrRight() == 0) {
        left = -bitmap_W - YImageSlider.SPLITE_W;
        top = 0;
        right = -YImageSlider.SPLITE_W;
        bottom = bitmap_H;
    } else {
        left = -(bitmap_W + YImageSlider.SPLITE_W + contentImageWidth - screamW);
        top = 0;
        right = -(YImageSlider.SPLITE_W + contentImageWidth - screamW);
        bottom = bitmap_H;
    }
} else {
    if (yImageSlider.getAlingLeftOrRight() == 0) {
        left = 0;
        top = 0;
        right = bitmap_W;
        bottom = bitmap_H;
    } else {
        left = -(contentImageWidth - screamW);
        top = 0;
        right = screamW;
        bottom = bitmap_H;
    }
}

在kotlin中被簡(jiǎn)化為

val left = if (locationIndex == 1)
    if (yImageSlider.alingLeftOrRight == 0)
        contentImageWidth + YImageSlider.SPLITE_W
    else
        screamW + YImageSlider.SPLITE_W
else if (locationIndex == -1)
    if (yImageSlider.alingLeftOrRight == 0)
        -bitmap_W - YImageSlider.SPLITE_W
    else
        -(bitmap_W + YImageSlider.SPLITE_W + contentImageWidth - screamW)
else
    if (yImageSlider.alingLeftOrRight == 0)
        0
    else
        -(contentImageWidth - screamW)

val right = if (locationIndex == 1)
    if (yImageSlider.alingLeftOrRight == 0)
        contentImageWidth + YImageSlider.SPLITE_W + bitmap_W
    else
        screamW + YImageSlider.SPLITE_W + bitmap_W
else if (locationIndex == -1)
    if (yImageSlider.alingLeftOrRight == 0)
        -YImageSlider.SPLITE_W
    else
        -(YImageSlider.SPLITE_W + contentImageWidth - screamW)
else
    if (yImageSlider.alingLeftOrRight == 0)
        bitmap_W
    else
        screamW

什么,你不覺得這是簡(jiǎn)化?哈哈,其實(shí)寫完這段代碼以后我也覺得,這里有點(diǎn)濫用之嫌。反而原先java的版本更清晰易讀。

總之語(yǔ)法糖很好,不要濫用,一切奇技淫巧都要為清晰簡(jiǎn)潔服務(wù)。比如

val destX:Float = if (x > 0) 0.toFloat() else minX.toFloat()

這樣就很好嘛。

回調(diào)函數(shù)

這個(gè)就不再是語(yǔ)法糖之流的小打小鬧了。kotlin的函數(shù)式特性不僅體現(xiàn)在引入了lambda,它的函數(shù)定義也可以脫離類獨(dú)立存在,并且無(wú)論是獨(dú)立的函數(shù)還是類中的函數(shù),都可以作為對(duì)象進(jìn)行引用和傳遞,這一特性的引入還是將android開發(fā)中的回調(diào)場(chǎng)景從大量的listener中解放出來(lái)了。

以前我們用java開發(fā)android,為了解決回調(diào)問(wèn)題各類的listener層出不窮非常煩人。比如為了讓YImageSlider響應(yīng)YImageView越過(guò)屏幕邊緣時(shí)觸發(fā)的事件我不得不在YImageView中定義接口:

interface EdgeListener {
    void onXEdge(YImageView yImageView);

    void onYEdge(YImageView yImageView);

    void onGetBackImg(YImageView yImageView);

    void onGetNextImg(YImageView yImageView);
}

然后讓YImageSlider實(shí)現(xiàn)EdgeListener,定義EdgeListener中的四個(gè)回調(diào)操作。并且在每一個(gè)YImageView實(shí)例初始化時(shí)持有YImageSlider的引用edgeListener,之后通過(guò)edgeListener進(jìn)行回調(diào)操作。這種套路在座的各位Android開發(fā)時(shí)也算是見得多了,就不啰嗦了。主要說(shuō)一下在Kotlin中有了函數(shù)式加持之后的變化。

Kotlin中取消了YImageView.EdgeListener接口,取而代之的是四個(gè)成員函數(shù)的聲明

lateinit var postGetBackImg: ()->Unit
lateinit var postGetNextImg: ()->Unit

lateinit var postXEdgeEvent: ()->Unit
lateinit var postYEdgeEvent: ()->Unit

這里的聲明形式和普通的以fun關(guān)鍵字開始的函數(shù)聲明不太一樣,因?yàn)槠鋵?shí)這四個(gè)是lambda。我們可以看到,和java的interface類型中的方法一樣,只是聲明了名字和形式,沒(méi)有具體實(shí)現(xiàn),從形式上來(lái)看,這里更像是聲明了四個(gè)變量:postGetBackImgpostGetNextImg、postXEdgeEvent、postYEdgeEvent是這四個(gè)變量的名字,()->Unit是類型,代表了一個(gè)沒(méi)有入?yún)?,沒(méi)有返回值的函數(shù)。類型聲明中沒(méi)有?,代表這個(gè)變量不可以為null,但是需要在初始化后動(dòng)態(tài)的賦值,所以使用了lateinit關(guān)鍵字,表示初始化時(shí)不賦值。由于需要在初始化后動(dòng)態(tài)賦值,所以不可以聲明為常量,使用var關(guān)鍵字。這意味這放棄編譯器對(duì)變量是否為空,是否有重新賦值進(jìn)行檢查,程序員必須自己保證在四個(gè)函數(shù)在調(diào)用時(shí)已經(jīng)被賦值了,否則一個(gè)大大的空指針異常在等著你。

當(dāng)然我也可以在構(gòu)造函數(shù)中對(duì)這四個(gè)變量賦值,那么也就不需要什么lateinit修飾,不用擔(dān)心空指針異常,但是這樣會(huì)導(dǎo)致構(gòu)造函數(shù)變得像臭名昭著的mfc里面那些一樣復(fù)雜,想想還是算了。

下面的關(guān)注點(diǎn)是如何在YImageSlider中將回調(diào)函數(shù)賦進(jìn)去,以下四種賦值形式都是等效的。

contentView.postGetBackImg = this::onGetBackImg

其中YImageSlider.onGetBackImg是一個(gè)函數(shù):

fun onGetBackImg() {
    //略
}

這種形式是將函數(shù)的引用賦值給lambda,注意針對(duì)對(duì)象方法的引用,使用的是雙冒號(hào)符號(hào)。

contentView.postGetNextImg = { onGetNextImg() }

這種是通過(guò)創(chuàng)建匿名lambda調(diào)用函數(shù)的方式實(shí)現(xiàn)間接賦值。onGetNextImg是一個(gè)和onGetBackImg類似的方法。這里的onGetNextImg是在匿名lambda中被調(diào)用而非引用,所以完整的寫法是this.onGetNextImg(),和java一樣,這個(gè)this可以省略。

contentView.postXEdgeEvent = this.onXEdge

其中onXEdge是lambda

val onXEdge : ()->Unit = {
}

這種形式是將lambda的引用賦值給lambda變量,很好理解

contentView.postYEdgeEvent = { onYEdge() }

postGetNextImg一樣也是創(chuàng)建匿名lambda的方式實(shí)現(xiàn)間接引用,只不過(guò)這里調(diào)用的是另一個(gè)lambda。

0x3 結(jié)語(yǔ)

關(guān)于這個(gè)項(xiàng)目要說(shuō)的大約就是這些。kotlin還有一些其他有意思的地方,比如類似es6的字符串模板,打印日志的時(shí)候不要太好用,比起java還在傻傻的用+號(hào)拼接字符串,甚至更傻的用StringBuilder/StringBuffer來(lái)做這些事情,真是高不知道哪去了。

最后還有一些高級(jí)特性,暫時(shí)沒(méi)有使用到的場(chǎng)合,自然也沒(méi)有領(lǐng)會(huì)到這些高級(jí)特性的妙處,等以后用到了再說(shuō)吧。

最后編輯于
?著作權(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)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,034評(píng)論 25 709
  • 前言 人生苦多,快來(lái) Kotlin ,快速學(xué)習(xí)Kotlin! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,692評(píng)論 9 118
  • Google在今年的IO大會(huì)上宣布,將Android開發(fā)的官方語(yǔ)言更換為Kotlin,作為跟著Google玩兒An...
    藍(lán)灰_q閱讀 77,194評(píng)論 31 489
  • 不重要的廢話 前段時(shí)間看了一遍《Programming Kotlin》,主要目的是想提高自己的英文閱讀能力,能力提...
    珞澤珈群閱讀 3,575評(píng)論 1 7
  • 牛市特征就是慢漲快跌,這個(gè)過(guò)程伴隨了比特幣從1500到10000的全過(guò)程。值得注意的是,比特幣如果強(qiáng)勢(shì)上漲,一些弱...
    李寧117閱讀 159評(píng)論 0 0

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