Yalantis:一次使用Kotlin实现酷炫多选操作的尝试

Ô­ÎÄ£ºOur Experiment Building a Multiselection Solution for Android in Kotlin 

¡°ÊÖ»úÉϵĶàÑ¡ºÜÄѲÙ×÷¡±£¬ÎÒÃǵÄÉè¼ÆʦVitaly RubtsovÈçÊÇ˵¡£´ó¶àÊýÓ¦ÓÃÖеĶàÑ¡·½°¸  £­Telegram, Apple Music, SpotifyµÈµÈ£­  Í¨³£¶¼²»ÊÇÄÇôÁé»î£¬ÓÃÆðÀ´Ò²²»Êæ·þ¡£

±ÈÈ磬µ±ÄãÔÚApple MusicÖд´½¨×Ô¼ºµÄ²¥·ÅÁбíʱ£¬Èç¹û²»Çл»ÆÁÄ»»òÕßÎÞ¾¡µÄ¹ö¶¯Ò»±é±»Ñ¡ÖеĸèÇú£¬Ä㶼²»Çå³þ×Ô¼ºÑ¡ÔñÁËÄÄЩ¸èÇú¡£

Èç¹ûÎÒÃÇÏëʹÓÃɸѡ¹¦ÄÜÊÂÇé¾Í±äµÃ¸üÔã¸âÁË¡£Ó¦ÓÃÁËÒ»¸öɸѡÌõ¼þÖ®ºó£¬ÁбíµÄ½á¹¹¿ÉÄܻᷢÉú¸Ä±ä£¬Ñ¡ÖеÄitemÒ²Ðí¸ù±¾¾Í²»»áÏÔʾ¡£Vitaly¾ö¶¨Ê¹ÓÃËû×Ô¼ºµÄ¶àÑ¡¸ÅÄîÉè¼Æ£¨×îÔç·¢²¼ÔÚDribbble£©À´½â¾öÕâ¸öÎÊÌâ¡£

ËûµÄÏë·¨·Ç³£´ÏÃ÷£º°ÑÆÁÄ»·Ö³ÉÁ½²¿·Ö£¬¾ÍÈçVitaly½âÊ͵ÄÄÇÑù£¬Äã×ÜÊÇÄÜ¡°¿´¼ûºÍ¹ÜÀíÒѾ­Ñ¡ÔñµÄÏîÄ¿£¬¶ø²»ÐèÒªÀ뿪µ±Ç°µÄÊÓͼ¡±¡£¶øɸѡֻӦÓÃÔÚÖ÷ÁÐ±í£¬²»»áÓ°ÏìÒѾ­Ñ¡ÔñµÄitemÁÐ±í¡£

ÄÇʱÎÒÃ÷°×Á˱ØÐëǧ·½°Ù¼Æ°ÑVitalyµÄ¶àÑ¡¸ÅÄîÉè¼ÆʵÏÖ³öÀ´£»ËùÒÔÎÒ¼¸ºõÁ¢¼´¾Í¿ªÊ¼Á˱àдÕâ¸ö¿Ø¼þµÄ¹¤×÷¡£ÏÖÔÚÈÃÎÒÃÇÀ´¿´¿´Õâ¸ö°²×¿µÄ¶àÑ¡¶¯»­ÊÇÈçºÎµ®ÉúµÄ¡£

04 (3).gif

ʵÏÖ

Õâ¸ö¿Ø¼þÓÐÒ»¸ö´øÁËÁ½¸öRecyclerViewµÄViewPager£¬ÎÒÃÇ¿ÉÒÔͨ¹ýÖØдgetPageWidth·½·¨·µ»ØÒ»¸ö0µ½1Ö®¼äµÄ¸¡µãÊýÀ´ÈÃViewPagerµÄÒ³ÃæСÓÚÆÁÄ»¡£

Ò»¸ö¾ßÓÐÁ½¸öÒ³ÃæµÄViewPager£¬Ã¿¸öÒ³Ãæ°üº¬Ò»¸öRecyclerView¡£Î´±»Ñ¡ÔñµÄitemÔÚ×ó±ßµÄÁÐ±í¡£Ñ¡ÖеÄitemÔÚÓұߵÄÁÐ±í¡£±ÈÈ磬Èç¹ûÄãµã»÷ÁËÒ»¸öδ±»Ñ¡ÔñµÄitem£¬½«·¢ÉúÒÔÏÂÊÂÇ飺

  1. ±»µã»÷µÄitem´Óδ±»Ñ¡ÖеÄitemÁбíÖÐÒƳý²¢±»Ìí¼Óµ½°üº¬ÁËÁ½¸öÁбíµÄÈÝÆ÷ÖС£

  2. Ñ¡ÖеÄitemµÄλÖÃÊǹ̶¨µÄ¡££¨Î´±»Ñ¡ÖеÄÁбí×ÜÊÇ°´ÕÕ×Öĸ˳ÐòÅÅÁС£Ñ¡ÖÐÁÐ±í°´ÕÕ±»Ñ¡ÔñµÄÏȺó˳ÐòÅÅÁУ©

  3. Ò»¸öÒþ²ØµÄitem±»Ìí¼Óµ½Ñ¡ÖÐÁбíÖС£

  4. ¶Ô±»µã»÷µÄitemÖ´Ðйý¶É¶¯»­¡£

  5. ɾ³ý±»µã»÷µÄitem²¢ÏÔʾѡÖÐÁбíÖÐÒþ²ØµÄitem¡£

Õâ¸ö¹ý³ÌÖÐ×î¼¼ÇÉÐԵIJ¿·ÖÊÇ°Ñ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µÄ¿í¶È£¨ÏÂͼËùʾ£©¡£

how-we-build-a-multiselection-component-for-android-application

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ʵÀý¡£

Äã¿ÉÒÔÔÚÎÒÃǵÄGitHub²Ö¿âÕÒµ½MultiSelect¿âÒÔ¼°¸ü¶àµÄÏîÄ¿¡£Ò²¿ÉÒÔµ½DribbbleÉϲ鿴ÎÒÃǵĸÅÄîÉè¼Æ£º

Ä㻹¿ÉÒÔÔÚGoogle Play StoreÉϵõ½Ê¹ÓÃÁËÕâ¸ö¿Ø¼þµÄdemo¡£