先上圖理疙,看看今天開什么車从媚,坐穩(wěn)咯痹扇。拖拉機(jī)即將超速行使悯舟,請(qǐng)系好安全帶担租!
MultiSelecter
先來(lái)分析一下android中會(huì)遇到哪些選擇方面的需求:
- 單選--這個(gè)就不啰嗦了
- 多選:
- 全部數(shù)據(jù)都可以被選擇,這個(gè)比較簡(jiǎn)單
- 過(guò)濾掉一部分?jǐn)?shù)據(jù)抵怎,不讓顯示出來(lái)
- 過(guò)濾掉一部分?jǐn)?shù)據(jù)奋救,能顯示,但用戶不能選擇
- 默認(rèn)選擇一部分?jǐn)?shù)據(jù)反惕,用戶可以取消選擇
- 默認(rèn)選擇一部分?jǐn)?shù)據(jù)尝艘,用戶不可以取消選擇
原諒我還只遇到過(guò)這些,那這里都有哪些功能呢姿染?
- 多選時(shí)會(huì)有選擇過(guò)渡動(dòng)畫(如GIF里所見(jiàn)背亥,但肯定不能肯的這樣掉渣的)
- 點(diǎn)擊上面的頭像可以取消選擇
- 選中的數(shù)據(jù)條數(shù)提示
- 根據(jù)選擇的條目會(huì)將搜索欄向右移動(dòng),搜索樣最少占屏幕寬度的1/3
- 當(dāng)然還有模糊搜索功能啦
- 全選
啰啰嗦嗦講了一堆悬赏,看一下Gif圖就什么都知道了好么狡汉?
好吧,遇到像你們這些高智商的闽颇,還是直接開車好了
一盾戴、緣起:
早上9點(diǎn),程序猿小猿同學(xué)一如往常準(zhǔn)時(shí)的坐在自己的位置上进萄,又是燥熱的一天捻脖。擠了一個(gè)小時(shí)的地鐵,原本瘦消的身體越發(fā)覺(jué)得扁平了中鼠。小猿心想:我要再擠個(gè)一年,也會(huì)成A4腰哦沿癞,不是像A4紙那么寬援雇,而是像它那么扁。開機(jī)-洗水杯-吃早餐椎扬,小猿開啟了程式化的一天惫搏。
小猿剛叼起一個(gè)小籠包,還沒(méi)來(lái)得及體會(huì)里面的美味蚕涤,產(chǎn)品經(jīng)理輕輕拍了下他的肩膀筐赔。直勾勾的看著小猿眼睛說(shuō):“昨天那個(gè)選擇頁(yè)面要做一個(gè)已經(jīng)選擇條目頭像展示的功能∫就”小猿剛想說(shuō)我cao,產(chǎn)品經(jīng)理連忙說(shuō)茴丰,中午下班前看下效果。小猿差點(diǎn)沒(méi)把小籠包整個(gè)咽下去,早TM干嗎去了贿肩,昨天晚上加班趕出來(lái)的峦椰,又得改,操蛋玩意兒汰规,小猿心理罵道汤功。
快速解決了小籠包,迅速進(jìn)入到戰(zhàn)斗狀態(tài)溜哮。好在這個(gè)不是太難實(shí)現(xiàn)滔金,只是在數(shù)據(jù)傳遞方面要下點(diǎn)功夫。在飯點(diǎn)前總算一切搞掂茂嗓,翹著二郎腿對(duì)產(chǎn)品說(shuō)餐茵,你要的頭像展示功能OK了。產(chǎn)品聽(tīng)完悠悠的轉(zhuǎn)過(guò)身在抛,肯定道:嗯嗯钟病,不錯(cuò),是我想要的刚梭。不過(guò)感覺(jué)好像還少了點(diǎn)什么肠阱,哦,對(duì)了朴读,這個(gè)頁(yè)面條目太多屹徘,用戶可能看不過(guò)來(lái),需要一個(gè)搜索功能衅金。放在這些頭像同一排就好了噪伊,就微信那樣。說(shuō)完拍拍小猿的肩膀氮唯,跟其它同事出去吃飯去了鉴吹,留下小猿在風(fēng)中凌亂。
小猿點(diǎn)了個(gè)外賣惩琉,然后想著剛剛這個(gè)需求要怎么實(shí)現(xiàn)豆励。突然小猿用力的拍了一下桌子,這樣下去不是辦法瞒渠,以后還哪里敢吃小籠包良蒸,只能喝粥度日了。我得趕在那貨之前弄個(gè)比較全面的解決方案伍玖,小猿對(duì)自己斬釘截鐵的說(shuō)到嫩痰。
小猿在想著他的對(duì)策,首先不能跟某一塊耦合了窍箍,萬(wàn)一其它地方也要用到呢串纺,那不是尷尬了丽旅?功能得盡可能的全面些,多選造垛,數(shù)據(jù)過(guò)濾魔招,單選....甚至還可以來(lái)得動(dòng)畫。小猿仿佛看到了自己對(duì)于需求應(yīng)對(duì)自如的樣子....一絲冷風(fēng)吹過(guò)五辽,吹醒了正在做白日夢(mèng)的小猿办斑。
二、 抽象:
說(shuō)干就干杆逗,首先得解決的是如何解耦乡翅,得用接口,接口是對(duì)現(xiàn)實(shí)的抽象罪郊。有了思路剩下的就是手速了蠕蚜,像小猿這種單身20多年的,手速當(dāng)然快如閃電悔橄,要不能上磚石靶累?
- 先造一個(gè)能判斷數(shù)據(jù)是何種選擇類型的接口,返回不同條目類型(可選癣疟、不可選 ....)挣柬,像這樣:
public interface Filter {
//專為標(biāo)題而生,因?yàn)橄到y(tǒng)默認(rèn)給的是0睛挚,這樣title Bean類里面都不用做過(guò)多的修改了
int TITLE_NO_CHOICE = 0;
//默認(rèn)不選中邪蛔,可以選中
int NORMAL = 1;
//默認(rèn)選中,不可以取消選中
int SELECTED_NOCANCEL = 2;
//默認(rèn)不選中,不能被選中
int NO_CHOICE = 3;
//不顯示在列表
int NOT_SHOW = 4;
//本地圖片地址
int getImageResource();
//網(wǎng)絡(luò)圖片url
String getImageUrl();
//返回當(dāng)前條目的狀態(tài),就是上面定義的那些個(gè)常量扎狱,返回值會(huì)在BaseViewHolder里面用到
int filter();
//是否是選中狀態(tài)
boolean isSelected();
void setSelected(boolean isSelected);
//是否匹配搜索關(guān)鍵字侧到,用來(lái)處理搜索的,如果不要搜索功能淤击,可以不用處理
boolean isMatch(String condition);
}
這樣只要每個(gè)具體bean 類去實(shí)現(xiàn)
在filter方法里去根據(jù)不同的條件返回上面定義的常量
isMatch方法是針對(duì)模糊搜索設(shè)計(jì)的
機(jī)智如我匠抗,看你還怎么改需求,小猿暗自竊喜...
- 圖片加載框架現(xiàn)在有好幾個(gè)污抬,搞不好哪個(gè)以后就不維護(hù)了戈咳,我可不能在一棵樹上吊死,小猿警惕起來(lái)壕吹,順手?jǐn)]了個(gè)圖片加載框架的接口:
public interface IImageLoader {
void showImage(Context context, String url, ImageView imageView);
}
以后項(xiàng)目想用哪個(gè)圖片框架只需要根據(jù)當(dāng)前項(xiàng)目使用的圖片加載框架實(shí)現(xiàn)對(duì)應(yīng)的ImageLoader就可以了,就是這么任性
三删铃、實(shí)施:
大致思路是有了耳贬,具體要怎么實(shí)現(xiàn)這些功能呢?小猿摸著日益上揚(yáng)的發(fā)跡線猎唁,沉思良久....
- 不需要展示給用戶的數(shù)據(jù)過(guò)濾倒是好做,可以像下面這樣的呀:
protected List<T> getFilterItems(List<T> items) {
mSelectionList.clear();
if (items != null) {
List<T> data = new ArrayList<>();
for (T item : items) {
int type = item.filter();
if (type != Filter.NOT_SHOW) {
data.add(item);
if (type != Filter.NO_CHOICE&&type!=Filter.TITLE_NO_CHOICE) {
mSelectionList.add(item);
}
}
}
return data;
} else {
return null;
}
}
- 如果是正常的數(shù)據(jù)咒劲,既要支持多選又能支持單選,這個(gè)要怎么處理比較好呢?毫無(wú)頭緒腐魂,一不小心干掉幾根本來(lái)就屈指可數(shù)的頭發(fā)帐偎,抱著試試的心態(tài)寫了下面的代碼:
public void setData(T data) {
normalBackgroundResource = getNormalBackgroundResource();
noChoiceBackgroundResource = getNoChoiceBackgroundResource();
int type = data.filter();
if (mCheckBox != null) {
mCheckBox.setChecked(data.isSelected());
}
if (type == Filter.NORMAL) {
onNormal(data);
} else if (type == Filter.NO_CHOICE) {
onNoChoice();
}
}
private void setClickListener(final Filter data) {
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (selectType == SelectionFragment.MULTI_SELECT) {
multiSelect(data);
} else {
singleSelect(data);
}
}
});
}
private void singleSelect(Filter data) {
if (mListener == null) {
throw new IllegalStateException("沒(méi)有設(shè)置點(diǎn)擊事件監(jiān)聽(tīng)");
}
if (mCurrentCheckBox != null) {
mCurrentCheckBox.setChecked(false);
mCurrentItem.setSelected(false);
mListener.onItemClick(itemView, mCurrentItem, false);
}
data.setSelected(true);
mCheckBox.setChecked(true);
mListener.onItemClick(itemView, data, true);
mCurrentCheckBox = mCheckBox;
mCurrentItem = data;
}
寫完心理在打鼓,不知道好不好使蛔屹,反正試下又不要錢削樊。經(jīng)過(guò)漫長(zhǎng)的等待,程序總算部署完畢兔毒,這么個(gè)小項(xiàng)目編譯居然還兩分鐘漫贞,還天天嫌我效率低,小猿抱怨道育叁。試了下單選功能迅脐,Prefect!簡(jiǎn)直不要太如我意豪嗽,哈哈谴蔑。嘗到甜頭的小猿準(zhǔn)備再試試多選功能,我草龟梦,還是那么完美隐锭。機(jī)智如我,啦啦啦....差點(diǎn)唱起了歌
3 . 那如果是需要展示变秦,但是不能操作的呢成榜?這個(gè)就簡(jiǎn)單了,隨手就擼了一個(gè)出來(lái):
public void onNoChoice() {
if (mCheckBox != null) {
mCheckBox.setBackgroundResource(noChoiceBackgroundResource);
itemView.setOnClickListener(null);
}
}
機(jī)智如我蹦玫,啦啦啦....對(duì)于小猿大神我來(lái)說(shuō)赎婚,都是小菜一碟。小猿得意的哼起了歌樱溉,得意得有點(diǎn)欠揍
是時(shí)候解決產(chǎn)品提的那兩個(gè)需求了:
展示頭像倒是沒(méi)難度的挣输,關(guān)鍵是如何讓搜索根據(jù)選擇條目的數(shù)量動(dòng)態(tài)改變寬度?能不能計(jì)算出當(dāng)前RecyclerView占用的寬度呢福贞?如果能知道的話事情不就解決了嗎撩嚼?嗯嗯,擼串代碼測(cè)試一下:
private void refreshLayout(boolean isSelected) {
mIvSelectAll.setSelected(isSelected);
if (mAdapter.getSelectionList().size() == mIconListRvAdapter.getItemCount()) {
mIvSelectAll.setSelected(true);
}
int size = mSelectList.size();
mTvConfirm.setText(size == 0 ? "確定" : "確定(" + size + ")");
if (size == 0) {
mTvConfirm.setEnabled(false);
} else {
mTvConfirm.setEnabled(true);
}
int width = mItemWidth * size;
if (width > mMaxWidth) {
width = mMaxWidth;
}
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(width, LinearLayout.LayoutParams.WRAP_CONTENT);
mIconRecyclerView.setLayoutParams(params);
mContainer.requestLayout();
}
完美挖帘、完美完丽、完美、簡(jiǎn)直不要太完美拇舀。這里主要做三件事:
如果手動(dòng)選擇時(shí)逻族,所有的條目都被選中,全選按鈕也會(huì)變成選中狀態(tài);
設(shè)置確定按鈕的選中條目的數(shù)量骄崩,并根據(jù)數(shù)量設(shè)置確定按鈕是否可點(diǎn)擊;
動(dòng)態(tài)改變搜索欄的寬度
mItemWidth是怎么計(jì)算的聘鳞?
private void initData() {
int screenWidth = ScreenUtils.getScreenWidth(mContext);
mMaxWidth = screenWidth * 2 / 3;
mItemWidth = SizeUtils.dp2px(mContext, 45);
mWidth5 = SizeUtils.dp2px(mContext, 5);
}
你沒(méi)看錯(cuò)薄辅,寫死了,可惜不能插入捂臉的表情抠璃。RecyclerView一個(gè)item占用45dp站楚,這個(gè)在寫布局的時(shí)候就已經(jīng)知道了。當(dāng)然這種硬編碼可不是個(gè)好習(xí)慣哦搏嗡,不要學(xué)習(xí)窿春。
四、進(jìn)化:
嘗到甜頭的小猿現(xiàn)在感覺(jué)自己無(wú)所不能彻况,哎呀谁尸,這個(gè)效果我還是很不滿意呀,為了展示逼格得加個(gè)動(dòng)畫不可纽甘。經(jīng)過(guò)千辛萬(wàn)苦良蛮,各種嘗試,總算有了能用的代碼:
public void translationView(final View itemView, final Filter item) {
getParentPoint();
itemView.setClickable(false);
final FloatImgBean floatImg = getFloatImg();
floatImg.mImageView.setVisibility(View.VISIBLE);
mPlaceHolder.setVisibility(View.VISIBLE);
floatImg.mImageView.setImageResource(item.getImageResource());
MultiSelecter.mImageLoader.showImage(mContext, item.getImageUrl(), floatImg.mImageView);
floatImg.mIsAnimator = true;
int[] sourceLocation = new int[2];
mSourceView.getLocationOnScreen(sourceLocation);
int startX = sourceLocation[0];
int startY = sourceLocation[1];
int[] tagetLocation = new int[2];
mIconRecyclerView.getLocationOnScreen(tagetLocation);
int endX = tagetLocation[0] + mIconRecyclerView.getWidth() + mWidth5;
int endY = tagetLocation[1] + mWidth5 * 2;
animator(itemView, item, floatImg, startX, startY, endX, endY);
}
private void animator(final View itemView, final Filter item, final FloatImgBean floatImg, int startX, int startY, int endX, int endY) {
ObjectAnimator animatorX = ObjectAnimator.ofFloat(floatImg.mImageView, "translationX", startX - mStartX, endX - mStartX);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(floatImg.mImageView, "translationY", startY - mStartY, endY - mStartY);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animatorX, animatorY);
animatorSet.setDuration(calcDuration(startX - endX, startY - endY));
animatorSet.setInterpolator(new OvershootInterpolator(1.1f));
animatorSet.start();
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
floatImg.mImageView.setVisibility(View.GONE);
floatImg.mIsAnimator = false;
mIconListRvAdapter.add(item);
mIconRecyclerView.smoothScrollToPosition(mIconListRvAdapter.getItemCount());
mPlaceHolder.setVisibility(View.GONE);
refreshLayout(false);
itemView.setClickable(true);
}
});
}
這里小猿可是踩了無(wú)數(shù)坑,這里可以詳細(xì)說(shuō)下。敲黑板固逗,這里可以高潮部分,就不要打瞌睡啦皮胡!
首先是getLocationOnScreen 這個(gè)操蛋的方法,網(wǎng)上一致說(shuō)這個(gè)獲得的是當(dāng)前view相當(dāng)于整個(gè)屏的坐標(biāo)位置赏迟,根據(jù)android坐標(biāo)系的尿性屡贺,那屏幕最左上角就是原點(diǎn)咯,那我通過(guò)這個(gè)方法就是能獲得頂部頭像欄內(nèi)條目的絕對(duì)坐標(biāo)了咯锌杀,簡(jiǎn)直不要太爽甩栈。
然并卵,根本不是那么回事好嗎糕再?直到現(xiàn)在為止量没,小猿仍然沒(méi)有搞懂這個(gè)方法獲取的坐標(biāo)會(huì)受到哪些因素的影響。但是可以肯定的是如果獲取Item的坐標(biāo)會(huì)出現(xiàn)偏移突想,貌似跟padding有關(guān)殴蹄,有知道的可以在評(píng)論區(qū)解答。
但是獲取mIconRecyclerView的坐標(biāo)是沒(méi)有問(wèn)題的猾担,這樣我只要計(jì)算mIconRecyclerView的寬度就可以知道X坐標(biāo)了袭灯。
android的屬性動(dòng)畫執(zhí)行過(guò)程中,如果執(zhí)行了另外的屬性動(dòng)畫绑嘹,會(huì)中斷之前的執(zhí)行妓蛮,導(dǎo)致動(dòng)畫監(jiān)聽(tīng)發(fā)生錯(cuò)亂。
整個(gè)邏輯全都亂套了圾叼,為此小猿想出了一個(gè)奇淫巧技蛤克,java不是有線程池么?為什么我不能弄一個(gè)ImageView的Pool,裝上幾個(gè)ImageView夷蚊,沒(méi)有在執(zhí)行動(dòng)畫的取出來(lái)用构挤,執(zhí)行動(dòng)畫的讓它自己玩去。于是有了下面這個(gè):
private void initFloatPool() {
FloatImgBean bean1 = new FloatImgBean();
bean1.mImageView = mFloatImg_1;
FloatImgBean bean2 = new FloatImgBean();
bean2.mImageView = mFloatImg_2;
FloatImgBean bean3 = new FloatImgBean();
bean3.mImageView = mFloatImg_3;
mImagePool.add(bean1);
mImagePool.add(bean2);
mImagePool.add(bean3);
}
private FloatImgBean getFloatImg() {
for (FloatImgBean bean : mImagePool) {
if (!bean.mIsAnimator) {
return bean;
}
}
return null;
}
當(dāng)然這個(gè)用FloatImgBean 包裝了一下惕鼓,記錄了當(dāng)前ImageView是否有在執(zhí)行動(dòng)畫筋现。
為了讓動(dòng)畫的執(zhí)行更自然作出了一個(gè)犧牲,在mIconRecyclerView和搜索欄中間加了一個(gè)和mIconRecyclerView 的Item等寬的一個(gè)view,在動(dòng)畫執(zhí)行的過(guò)程中設(shè)置成VISIBLE或者GONE來(lái)迎合動(dòng)畫的執(zhí)行箱歧。
五矾飞、示例:
完成了所有的功能,小猿癱坐在電腦椅上洒沦,臉上露出滿意的微笑价淌。
忽然小猿虎軀一震蝉衣,不行病毡,我還得寫個(gè)示例啦膜,不然怎么在人前裝逼呢功戚?
先實(shí)現(xiàn)一個(gè)Bean 類:
public class UserBean implements Filter {
public String userName;
public int icon;
public int age;
public boolean isSelected;
public String iconUrl;
@Override
public boolean isSelected() {
return isSelected;
}
@Override
public void setSelected(boolean isSelected) {
this.isSelected = isSelected;
}
@Override
public int getImageResource() {
return icon;
}
@Override
public String getImageUrl() {
return iconUrl;
}
@Override
public int filter() {
if (age<3){
return Filter.NO_CHOICE;
}else if (age>=3&&age<100){
return Filter.NORMAL;
}
return 0;
}
@Override
public boolean isMatch(String condition) {
if (MatchUtils.isMatch(userName,condition)){
return true;
}
return false;
}
}
如何調(diào)用那封裝好的東西呢啸臀?
MultiSelectView selectView = new MultiSelecter.Builder(this, container)
.setImageLoader(new GlideImageLoader())
.setMultiAdapter(new UserAdapter())
.setSelectType(MultiSelecter.MULTI_SELECT)
.register(UserBean.class, R.layout.item_user)
.register(TitleBean.class, R.layout.item_title)
.build();
當(dāng)然還得來(lái)一個(gè)ImageLoader的示例:
public class GlideImageLoader implements IImageLoader {
@Override
public void showImage(Context context, String url, ImageView imageView) {
Glide.with(context)
.load(url)
.into(imageView);
}
}
哦了豌注,好累灯萍,小猿來(lái)了個(gè)葛優(yōu)躺....
用到的開源項(xiàng)目:
AutoRecycleView:自動(dòng)維護(hù)下拉刷新和上拉加載更多
SimpleMutiTypeAdapter :使用起來(lái)很簡(jiǎn)單的RecyclerView多條目
沒(méi)錯(cuò)齿风,都是小猿之前為了應(yīng)付需求改動(dòng)寫的。
源碼地址:MultiSelecter