一次使用Kotlin實(shí)現(xiàn)酷炫多選操作的嘗試

“手機(jī)上的多選很難操作”胚股,我們的設(shè)計(jì)師Vitaly Rubtsov如是說雷客。大多數(shù)應(yīng)用中的多選方案 -Telegram, Apple Music, Spotify等等- 通常都不是那么靈活,用起來也不舒服牺堰。

比如佩微,當(dāng)你在Apple Music中創(chuàng)建自己的播放列表時(shí),如果不切換屏幕或者無盡的滾動(dòng)一遍被選中的歌曲萌焰,你都不清楚自己選擇了哪些歌曲。

如果我們想使用篩選功能事情就變得更糟糕了谷浅。應(yīng)用了一個(gè)篩選條件之后扒俯,列表的結(jié)構(gòu)可能會(huì)發(fā)生改變,選中的item也許根本就不會(huì)顯示一疯。Vitaly決定使用他自己的多選概念設(shè)計(jì)(最早發(fā)布在Dribbble)來解決這個(gè)問題撼玄。

他的想法非常聰明:把屏幕分成兩部分,就如Vitaly解釋的那樣墩邀,你總是能“看見和管理已經(jīng)選擇的項(xiàng)目掌猛,而不需要離開當(dāng)前的視圖”。而篩選只應(yīng)用在主列表眉睹,不會(huì)影響已經(jīng)選擇的item列表荔茬。

那時(shí)我明白了必須千方百計(jì)把Vitaly的多選概念設(shè)計(jì)實(shí)現(xiàn)出來;所以我?guī)缀趿⒓淳烷_始了編寫這個(gè)控件的工作≈窈#現(xiàn)在讓我們來看看這個(gè)安卓的多選動(dòng)畫是如何誕生的慕蔚。

1478063387383413.gif

實(shí)現(xiàn)

這個(gè)控件有一個(gè)帶了兩個(gè)RecyclerView的ViewPager,我們可以通過重寫getPageWidth方法返回一個(gè)0到1之間的浮點(diǎn)數(shù)來讓ViewPager的頁面小于屏幕斋配。

一個(gè)具有兩個(gè)頁面的ViewPager孔飒,每個(gè)頁面包含一個(gè)RecyclerView。未被選擇的item在左邊的列表艰争。選中的item在右邊的列表坏瞄。比如,如果你點(diǎn)擊了一個(gè)未被選擇的item甩卓,將發(fā)生以下事情:

  • 被點(diǎn)擊的item從未被選中的item列表中移除并被添加到包含了兩個(gè)列表的容器中鸠匀。
  • 選中的item的位置是固定的。(未被選中的列表總是按照字母順序排列猛频。選中列表按照被選擇的先后順序排列)
  • 一個(gè)隱藏的item被添加到選中列表中狮崩。
  • 對(duì)被點(diǎn)擊的item執(zhí)行過渡動(dòng)畫蛛勉。
  • 刪除被點(diǎn)擊的item并顯示選中列表中隱藏的item。

這個(gè)過程中最技巧性的部分是把view從layout manager移除睦柴;否則layout manager 會(huì)嘗試回收它诽凌,因?yàn)橐呀?jīng)從RecyclerView刪除了這個(gè)view,所以這會(huì)導(dǎo)致錯(cuò)誤:

sourceRecycler.layoutManager.removeViewAt(position)

技術(shù)棧

我們選擇Kotlin語言來做這個(gè)工作坦敌。和Java相比侣诵,Kotlin最主要的優(yōu)點(diǎn)是其簡(jiǎn)明的語法和不會(huì)出現(xiàn)NullPointerException之類的崩潰。這里是我在實(shí)現(xiàn)這個(gè)庫的過程中狱窘,Kotlin的這些特性給我?guī)砹朔奖悖?/p>

  • 1.擴(kuò)展函數(shù)

Kotlin的擴(kuò)展函數(shù)功能使得我們可以為現(xiàn)有的類添加新的函數(shù)杜顺,而不用修改原來的類。
就拿安卓的View來說蘸炸。通常你需要把一個(gè)view從其父親那里移除并掛載到新的view上躬络。
  
  從view的父親移除自己:

fun View.removeFromParent() {
   val parent = this.parent
   if (parent is ViewGroup) {
       parent.removeView(this)
   }
}

定義了上面的方法之后,你就可以在項(xiàng)目的任何地方這樣調(diào)用它了:

view.removeFromParent()

你甚至可以直接寫一個(gè)方法做完所有事情把一個(gè)view從當(dāng)前父親那里移除并掛載到新的view上:

view.attachTo(newParent)

另一個(gè)好處是你可以添加setScaleXY方法搭儒。很少見到使用了setScaleX而不用setScaleY的情況穷当,所以為什么不用一個(gè)方法設(shè)置兩個(gè)Scale呢?讓我們做一個(gè)這樣的函數(shù):

fun View.setScaleXY(scale: Float) {
   scaleX = scale
   scaleY = scale
}

你可以在library源碼的 Extensions.kt文件中找到更多使用擴(kuò)展函數(shù)的例子淹禾。

  • 2.Null safety

Kotlin的null safety特性是一個(gè)規(guī)則改變者 ‘?.’操作符和 ‘.’ 一樣的意思只是如果對(duì)象是null而被調(diào)用的話不會(huì)拋出NullPointerException馁菜,而是返回null:

var targetView: View? = targetRecycler.findViewHolderForAdapterPosition(prev)?.itemView

上面的代碼中,即使findViewHolderForAdapterPosition返回null也不會(huì)崩潰铃岔。

  • 3.Collections

Kotlin comes with stdlib, 它包含了許多干凈利落的方法比如map和filter汪疮。這些方法非常普遍,而且不同編程語言都表現(xiàn)出相同的行為毁习,包括Java 8 (streams)智嚷。不幸的是streams在安卓開發(fā)中還不能使用。
  對(duì)我們的多選庫來說蜓洪,我們需要對(duì)除了指定id的child之外的所有子view使用透明度動(dòng)畫纤勒。下面的Kotlin代碼可以很好的完成:

if (view is ViewGroup) {
   (0..view.childCount - 1)
        .map { view.getChildAt(it) }
        .filter { it.id != R.id.yal_ms_avatar }
        .forEach { it.alpha = value }
}

要在Java上實(shí)現(xiàn)相同的事情可能會(huì)比這里的代碼多上一倍。

  • 4.更好的語法

通常來說隆檀,Kotlin的語法比Java更簡(jiǎn)潔易讀摇天。
  一個(gè)例子是when表達(dá)式。不同于Java的switch恐仑,Kotlin的when表達(dá)式返回一個(gè)值泉坐,所以你需要把它賦予一個(gè)變量或者從一個(gè)函數(shù)返回它。這個(gè)特性以及其本身可以讓代碼更短更易讀:

private fun getView(position: Int, pager: ViewPager): View = when (position) {
   0 -> pageLeft
   1 -> pageRight
   else -> throw IllegalStateException()
}

如何使用MultiSelect

如果你想在項(xiàng)目中使用multiselect裳仆,這里是5個(gè)簡(jiǎn)單的步驟腕让。

1.首先,把下面的代碼添加到root build.gradle:

allprojects {
    repositories {
        ...
        maven { url "https://jitpack.io" }
    }
}

然后添加下面的代碼到 module build.gradle:

dependencies {
    compile 'com.github.yalantis:multi-selection:v0.1'
}

2.創(chuàng)建一個(gè)ViewHolder:

class ViewHolder extends RecyclerView.ViewHolder {
   TextView name;
   TextView comment;
   ImageView avatar;

   public ViewHolder(View view) {
       super(view);
       name = (TextView) view.findViewById(R.id.name);
       comment = (TextView) view.findViewById(R.id.comment);
       avatar = (ImageView) view.findViewById(R.id.yal_ms_avatar);
   }

   public static void bind(ViewHolder viewHolder, Contact contact) {
     viewHolder.name.setText(contact.getName());
     viewHolder.avatar.setImageURI(contact.getPhotoUri());
     viewHolder.comment.setText(String.valueOf(contact.getTimesContacted()));
   }
}

注意這個(gè)靜態(tài)bind方法。有了它你就可以在兩個(gè)adapter中使用相同的viewholder纯丸。

3.接下來偏形,為未選中的列表和選中列表創(chuàng)建兩個(gè)adapter。第一個(gè)繼承BaseLeftAdapter觉鼻,第二個(gè)繼承BaseRightAdapter:

public class LeftAdapter extends BaseLeftAdapter<Contact, ViewHolder>{

     private final Callback callback;

     public LeftAdapter(Callback callback) {
              super(Contact.class);
              this.callback = callback;
     }

     @Override
     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         View view =  LayoutInflater.from(parent.getContext()).inflate(R.layout.item_view, parent, false);
         return new ViewHolder(view);
     }

    @Override
    public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
        super.onBindViewHolder(holder, position);
        ViewHolder.bind(holder, getItemAt(position));
        holder.itemView.setOnClickListener(view -> {
           // ...
           callback.onClick(holder.getAdapterPosition());
        // ...
        }); 
    }
}

選中列表的adapter與之類似:

public class RightAdapter extends BaseRightAdapter<Contact, ViewHolder> {
 
   private final Callback callback;
 
   public RightAdapter(Callback callback) {
       this.callback = callback;
   }
 
   @Override
   public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
       View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_view, parent, false);
       return new ViewHolder(view);
   }
 
   @Override
   public void onBindViewHolder(@NotNull final ViewHolder holder, int position) {
       super.onBindViewHolder(holder, position);
 
       ViewHolder.bind(holder, getItemAt(position));
 
       holder.itemView.setOnClickListener(view -> {
           // ...
           callback.onClick(holder.getAdapterPosition());
           // ...
       });
   }
}

Adapter繼承兩個(gè)不同基類的原因是未選中item是排好序的俊扭,而選中item按照被選擇的先后順序排列。

4.最后調(diào)用builder:

MultiSelectBuilder<Contact> builder = new MultiSelectBuilder<>(Contac
   .withContext(this)
   .mountOn((ViewGroup) findViewById(R.id.mount_point))
   .withSidebarWidth(46 + 8 * 2); // ImageView width with paddings

你需要:

  • 傳入context坠陈。
  • 傳入你想把這個(gè)控件所要掛載到的view(通常為FrameLayout)萨惑。
  • 指定sidebar的寬度(下圖所示)。
how-we-build-a-multiselection-component-for-android-application

5.最后設(shè)置adapter:

   LeftAdapter leftAdapter = new LeftAdapter(position -> mMultiSelect.select(position));
   RightAdapter rightAdapter = new RightAdapter(position -> mMultiSelect.deselect(position));
   leftAdapter.addAll(contacts);
   builder.withLeftAdapter(leftAdapter)
       .withRightAdapter(rightAdapter);

現(xiàn)在你要做的就是調(diào)用builder.build()仇矾,它將返回MultiSelect<T>實(shí)例庸蔼。
你可以在我們的GitHub倉庫找到MultiSelect庫以及更多的項(xiàng)目。也可以到Dribbble上查看我們的概念設(shè)計(jì):
GitHub


原文:Our Experiment Building a Multiselection Solution for Android in Kotlin

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末贮匕,一起剝皮案震驚了整個(gè)濱河市姐仅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌刻盐,老刑警劉巖萍嬉,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異隙疚,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)磕道,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門供屉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人溺蕉,你說我怎么就攤上這事伶丐。” “怎么了疯特?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵哗魂,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我漓雅,道長(zhǎng)录别,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任邻吞,我火速辦了婚禮组题,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抱冷。我一直安慰自己崔列,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布旺遮。 她就那樣靜靜地躺著赵讯,像睡著了一般盈咳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上边翼,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天鱼响,我揣著相機(jī)與錄音,去河邊找鬼讯私。 笑死热押,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的斤寇。 我是一名探鬼主播桶癣,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼娘锁!你這毒婦竟也來了牙寞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤莫秆,失蹤者是張志新(化名)和其女友劉穎间雀,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體镊屎,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惹挟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缝驳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片连锯。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖用狱,靈堂內(nèi)的尸體忽然破棺而出运怖,到底是詐尸還是另有隱情,我是刑警寧澤夏伊,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布摇展,位于F島的核電站,受9級(jí)特大地震影響溺忧,放射性物質(zhì)發(fā)生泄漏咏连。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一鲁森、第九天 我趴在偏房一處隱蔽的房頂上張望捻勉。 院中可真熱鬧,春花似錦刀森、人聲如沸踱启。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽埠偿。三九已至透罢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冠蒋,已是汗流浹背羽圃。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抖剿,地道東北人朽寞。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像斩郎,于是被迫代替她去往敵國和親脑融。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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