使用Kotlin實現一個酷炫的多選操作

dyy380neu 8年前發布 | 19K 次閱讀 Kotlin 安卓開發

“手機上的多選很難操作”,我們的設計師Vitaly Rubtsov如是說。大多數應用中的多選方案Telegram, Apple Music, Spotify等等通常都不是那么靈活,用起來也不舒服。

比如,當你在Apple Music中創建自己的播放列表時,如果不切換屏幕或者無盡的滾動一遍被選中的歌曲,你都不清楚自己選擇了哪些歌曲。

如果我們想使用篩選功能事情就變得更糟糕了。應用了一個篩選條件之后,列表的結構可能會發生改變,選中的item也許根本就不會顯示。Vitaly決定使用他自己的多選概念設計(最早發布在 Dribbble )來解決這個問題。

他的想法非常聰明:把屏幕分成兩部分,就如Vitaly解釋的那樣,你總是能“看見和管理已經選擇的項目,而不需要離開當前的視圖”。而篩選只應用在主列表,不會影響已經選擇的item列表。

那時我明白了必須千方百計把Vitaly的多選概念設計實現出來;所以我幾乎立即就開始了編寫這個控件的工作。現在讓我們來看看這個安卓的多選動畫是如何誕生的。

實現

這個控件有一個帶了兩個RecyclerView的ViewPager,我們可以通過重寫getPageWidth方法返回一個0到1之間的浮點數來讓ViewPager的頁面小于屏幕。

一個具有兩個頁面的ViewPager,每個頁面包含一個RecyclerView。未被選擇的item在左邊的列表。選中的item在右邊的列表。比如,如果你點擊了一個未被選擇的item,將發生以下事情:

  1. 被點擊的item從未被選中的item列表中移除并被添加到包含了兩個列表的容器中。

  2. 選中的item的位置是固定的。(未被選中的列表總是按照字母順序排列。選中列表按照被選擇的先后順序排列)

  3. 一個隱藏的item被添加到選中列表中。

  4. 對被點擊的item執行過渡動畫。

  5. 刪除被點擊的item并顯示選中列表中隱藏的item。

這個過程中最技巧性的部分是把view從layout manager移除;否則layout manager 會嘗試回收它,因為已經從RecyclerView刪除了這個view,所以這會導致錯誤:

sourceRecycler.layoutManager.removeViewAt(position)

技術棧

我們選擇Kotlin語言來做這個工作。和Java相比,Kotlin最主要的優點是其簡明的語法和不會出現NullPointerException之類的崩潰。這里是我在實現這個庫的過程中,Kotlin的這些特性給我帶來了方便:

  • 擴展函數

Kotlin的擴展函數功能使得我們可以為現有的類添加新的函數,而不用修改原來的類。

就拿安卓的View來說。通常你需要把一個view從其父親那里移除并掛載到新的view上。

從view的父親移除自己:

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

定義了上面的方法之后,你就可以在項目的任何地方這樣調用它了:

 view.removeFromParent()

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

view.attachTo(newParent)

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

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

你可以在library源碼的 Extensions.kt文件中找到更多使用擴展函數的例子。

  • Null safety

Kotlin的null safety特性是一個規則改變者 ‘?.’操作符和 ‘.’ 一樣的意思只是如果對象是null而被調用的話不會拋出NullPointerException,而是返回null:

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

上面的代碼中,即使findViewHolderForAdapterPosition返回null也不會崩潰。

  • Collections

Kotlin comes with stdlib, 它包含了許多干凈利落的方法比如map和filter。這些方法非常普遍,而且不同編程語言都表現出相同的行為,包括Java 8 (streams)。不幸的是streams在安卓開發中還不能使用。

對我們的多選庫來說,我們需要對除了指定id的child之外的所有子view使用透明度動畫。下面的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上實現相同的事情可能會比這里的代碼多上一倍。

  • 更好的語法

通常來說,Kotlin的語法比Java更簡潔易讀。

一個例子是when表達式。不同于Java的switch,Kotlin的when表達式返回一個值,所以你需要把它賦予一個變量或者從一個函數返回它。這個特性以及其本身可以讓代碼更短更易讀:

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

如何使用MultiSelect

如果你想在項目中使用multiselect,這里是5個簡單的步驟。

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

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

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

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

2. 創建一個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()));
   }
}

注意這個靜態bind方法。有了它你就可以在兩個adapter中使用相同的viewholder。

3. 接下來,為未選中的列表和選中列表創建兩個adapter。第一個繼承BaseLeftAdapter,第二個繼承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繼承兩個不同基類的原因是未選中item是排好序的,而選中item按照被選擇的先后順序排列。

4.最后調用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。

  • 傳入你想把這個控件所要掛載到的view(通常為FrameLayout)。

  • 指定sidebar的寬度(下圖所示)。

5. 最后設置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);

現在你要做的就是調用builder.build(),它將返回MultiSelect<T>實例。

你可以在我們的GitHub倉庫找到MultiSelect庫以及更多的項目。也可以到Dribbble上查看我們的概念設計:

 

 

來自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2016/1102/6734.html

 

 本文由用戶 dyy380neu 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!