“手機(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)畫是如何誕生的慕蔚。
實(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的寬度(下圖所示)。
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