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µÄ¶àÑ¡¸ÅÄîÉè¼ÆʵÏÖ³öÀ´£»ËùÒÔÎÒ¼¸ºõÁ¢¼´¾Í¿ªÊ¼Á˱àдÕâ¸ö¿Ø¼þµÄ¹¤×÷¡£ÏÖÔÚÈÃÎÒÃÇÀ´¿´¿´Õâ¸ö°²×¿µÄ¶àÑ¡¶¯»ÊÇÈçºÎµ®ÉúµÄ¡£
ʵÏÖ
Õâ¸ö¿Ø¼þÓÐÒ»¸ö´øÁËÁ½¸öRecyclerViewµÄViewPager£¬ÎÒÃÇ¿ÉÒÔͨ¹ýÖØдgetPageWidth·½·¨·µ»ØÒ»¸ö0µ½1Ö®¼äµÄ¸¡µãÊýÀ´ÈÃViewPagerµÄÒ³ÃæСÓÚÆÁÄ»¡£
Ò»¸ö¾ßÓÐÁ½¸öÒ³ÃæµÄViewPager£¬Ã¿¸öÒ³Ãæ°üº¬Ò»¸öRecyclerView¡£Î´±»Ñ¡ÔñµÄitemÔÚ×ó±ßµÄÁÐ±í¡£Ñ¡ÖеÄitemÔÚÓұߵÄÁÐ±í¡£±ÈÈ磬Èç¹ûÄãµã»÷ÁËÒ»¸öδ±»Ñ¡ÔñµÄitem£¬½«·¢ÉúÒÔÏÂÊÂÇ飺
-
±»µã»÷µÄitem´Óδ±»Ñ¡ÖеÄitemÁбíÖÐÒƳý²¢±»Ìí¼Óµ½°üº¬ÁËÁ½¸öÁбíµÄÈÝÆ÷ÖС£
-
Ñ¡ÖеÄitemµÄλÖÃÊǹ̶¨µÄ¡££¨Î´±»Ñ¡ÖеÄÁбí×ÜÊÇ°´ÕÕ×Öĸ˳ÐòÅÅÁС£Ñ¡ÖÐÁÐ±í°´ÕÕ±»Ñ¡ÔñµÄÏȺó˳ÐòÅÅÁУ©
-
Ò»¸öÒþ²ØµÄitem±»Ìí¼Óµ½Ñ¡ÖÐÁбíÖС£
-
¶Ô±»µã»÷µÄitemÖ´Ðйý¶É¶¯»¡£
-
ɾ³ý±»µã»÷µÄ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µÄ¿í¶È£¨ÏÂͼËùʾ£©¡£
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¡£