Android TagFlowLayout完全解析 一款針對Tag的布局
轉載請標明出處:
http://blog.csdn.net/lmj623565791/article/details/48393217;
本文出自:【張鴻洋的博客】
一、概述
本文之前,先提一下關于上篇博文的100多萬訪問量請無視,博文被刷,我也很郁悶,本來想把那個文章放到草稿箱,結果放不進去,還把日期弄更新了,實屬無奈。
ok,開始今天的博文,今天要說的是TagFlowLayout,說這個之前必須提一下FlowLayout,如果你不了解,可以先閱讀之前的博文:Android 自定義ViewGroup 實戰篇 -> 實現FlowLayout或者觀看視頻
打造Android中的流式布局和熱門標簽 。
因為本身FlowLayout本身的預期是提供一種新的布局的方式,但是呢,在實際的開發中,大家更多的是使用在商品標簽,搜索關鍵字的場景,那么就涉及到一些交互:
- 比如用戶選擇了某個標簽,首先你要去改變標簽的樣子給用戶一個反饋,其次你需要記錄用戶的選擇。
- 那么在選擇過程中還有多選的情況,比如4選2,4選3等等。
- 還有…
類似京東的這個選擇商品的圖:
對于上述的情況呢,FlowLayout只能說能夠實現View的顯示沒有問題,而對于點擊某個Tag,以及修改某個Tag的樣子,可能需要編寫大量的代碼,且設計只要稍微的改下顯示的效果,估計就得加班了。
既然這么多的不方便,那么我們現在就在FlowLayout的基礎上,編寫TagFlowLayout去完善,目前支持:
- 以setAdapter形式注入數據
- 直接設置selector為background即可完成標簽選則的切換,類似CheckBox
- 支持控制選擇的Tag數量,比如:單選、多選
- 支持setOnTagClickListener,當點擊某個Tag回調
- 支持setOnSelectListener,當選擇某個Tag后回調
- 支持adapter.notifyDataChanged
- Activity重建(或者旋轉)后,選擇的狀態自動保存
我們的效果圖:
github地址:FlowLayout
我需要思考幾分鐘本文的敘述方式…
ok,由于本文并非從無到有的去構造一個新的東西,所以你肯定沒有辦法根據我的分析,然后就能完整的寫出來。這樣的話,就非常建議大家下載源碼,拿著源碼比對著看;或者看完本文后去下載源碼;或者僅僅是看看思路學學知識點(eclipse的用戶,拷貝幾個類不是難事,不要私聊我問我怎么整~)。
二、以setAdapter形式注入數據
首先我們完成的就是,去除大家痛苦的添加數據的方式。類似ListView,提供Adapter的方式,為我們的TagFlowLayout
去添加數據,這種方式,大家用的肯定比較熟練了,而且也比較靈活。
(1) TagAdapter
那么首先我們得有個Adapter,這里叫做TagAdapter
package com.zhy.view.flowlayout; import android.view.View; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public abstract class TagAdapter<T> { private List<T> mTagDatas; private OnDataChangedListener mOnDataChangedListener; public TagAdapter(List<T> datas) { mTagDatas = datas; } public TagAdapter(T[] datas) { mTagDatas = new ArrayList<T>(Arrays.asList(datas)); } static interface OnDataChangedListener { void onChanged(); } void setOnDataChangedListener(OnDataChangedListener listener) { mOnDataChangedListener = listener; } public int getCount() { return mTagDatas == null ? 0 : mTagDatas.size(); } public void notifyDataChanged() { mOnDataChangedListener.onChanged(); } public T getItem(int position) { return mTagDatas.get(position); } public abstract View getView(FlowLayout parent, int position, T t); }
可以看到很簡單,這是一個抽象類,那么具體的View的展示需要大家通過復寫getView,用法和ListView及其類似,同時我們提供了notifyDataChanged()的方法,當你的數據集發生變化的時候,你可以調用該方法,UI會自動刷新。
當然,僅僅有了Adapter是不行的,我們需要添加相應的代碼對其進行支持。
(2)TagFlowLayout對Adapter的支持
那么最主要就是提供一個setAdapter的方法:
public void setAdapter(TagAdapter adapter) { mTagAdapter = adapter; mTagAdapter.setOnDataChangedListener(this); changeAdapter(); } private void changeAdapter() { removeAllViews(); TagAdapter adapter = mTagAdapter; TagView tagViewContainer = null; for (int i = 0; i < adapter.getCount(); i++) { View tagView = adapter.getView(this, i, adapter.getItem(i)); tagView.setDuplicateParentStateEnabled(true); tagViewContainer.setLayoutParams(tagView.getLayoutParams()); tagViewContainer.addView(tagView); addView(tagViewContainer); } } @Override public void onChanged() { changeAdapter(); }
ok,可以看到當你調用setAdapter進來,首先我們會注冊mTagAdapter.setOnDataChangedListener
這個回調,主要是用于響應notifyDataSetChanged()
。然后進入changeAdapter方法
,在這里首先移除所有的子View,然后根據mAdapter.getView的返回,開始逐個構造子View,然后進行添加。
這里注意下:我們的上述的代碼,對mAdapter.getView返回的View,外圍報了一層TagView,這里暫時不要去想,我們后面會細說。
到此,我們的Adapter添加完畢。
三、支持onTagClickListener
ok,這個接口也非常重要,當然我私下了解了下,很多同學都加上了,但是基本都是對單個標簽View去setOnClickListener,然后去比對Tag確定點擊的是哪個標簽,最后回調出來。當然,我們這里考慮一種更優雅的方式:
我們從父控件下手,當我們確定用戶點擊在我們的TagFlowLayout上時,我們根據用戶點擊的坐標,看看是否點擊的是我們的某個View,然后進行click回調。是不是有點像事件分發,哈,我們這里可以稱為點擊分發。
那么,我們需要關注的就是onTouchEvent
和performClick
方法。
@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { mMotionEvent = MotionEvent.obtain(event); } return super.onTouchEvent(event); } @Override public boolean performClick() { if (mMotionEvent == null) return super.performClick(); int x = (int) mMotionEvent.getX(); int y = (int) mMotionEvent.getY(); mMotionEvent = null; TagView child = findChild(x, y); int pos = findPosByView(child); if (child != null) { doSelect(child, pos); if (mOnTagClickListener != null) { return mOnTagClickListener.onTagClick(child.getTagView(), pos, this); } } return super.performClick(); } private TagView findChild(int x, int y) { final int cCount = getChildCount(); for (int i = 0; i < cCount; i++) { TagView v = (TagView) getChildAt(i); if (v.getVisibility() == View.GONE) continue; Rect outRect = new Rect(); v.getHitRect(outRect); if (outRect.contains(x, y)) { return v; } } return null; }
可以看到我們這里巧妙的利用了performClick
這個回調,來確定的確是觸發了click事件,而不是自己去判斷什么算click的條件。但是呢,我們的performClick
沒有提供MotionEvent的參數,不過不要緊,我們都清楚click的事件發生在ACTION_UP之后,所以我們提供一個變量去記錄最后一次觸發ACTION_UP的mMotionEvent。
我們在performClick
里面,根據mMotionEvent,去查找是否落在某個子View身上,如果落在,那么就確定點擊在它身上了,直接回調即可,關于接口的定義如下,(ps:關于doSelect方法,我們后面說):
public interface OnTagClickListener { boolean onTagClick(View view, int position, FlowLayout parent); } private OnTagClickListener mOnTagClickListener; public void setOnTagClickListener(OnTagClickListener onTagClickListener) { mOnTagClickListener = onTagClickListener; if (onTagClickListener != null) setClickable(true); }
可以看到,如果設置了setOnTagClickListener
,我們顯示的設置了父ViewsetClickable(true)
。以防萬一父View不具備消費事件的能力。
四、全面支持Checked
這一節呢,主要包含支持幾個功能:
* 直接設置selector為background即可完成標簽選則的切換,類似CheckBox
* 支持控制選擇的Tag數量,比如:單選、多選
* 支持setOnSelectListener,當選擇某個Tag后回調
首先,我們提供了兩個自定義的屬性,multi_suppout
和max_select
。一個是指出是否支持選擇(如果為false,意味著你只能通過setOnTagClickListener去做一些操作),一個是設置最大的選擇數量,-1為不限制數量。
ok,那么核心的代碼依然在performClick中被調用的:
@Override public boolean performClick() { //省略了一些代碼... doSelect(child, pos); if (mOnTagClickListener != null) { return mOnTagClickListener.onTagClick(child.getTagView(), pos, this); } //省略了一些代碼... } private void doSelect(TagView child, int position) { if (mSupportMulSelected) { if (!child.isChecked()) { if (mSelectedMax > 0 && mSelectedView.size() >= mSelectedMax) return; child.setChecked(true); mSelectedView.add(position); } else { child.setChecked(false); mSelectedView.remove(position); } if (mOnSelectListener != null) { mOnSelectListener.onSelected(new HashSet<Integer>(mSelectedView)); } } }
ok,可以看到,如果點擊了某個標簽,進入doSelect方法,首先判斷你是否開啟了多選的支持(默認支持),然后判斷當前的View是否是非Checked
的狀態,如果是非Checked
狀態,則判斷最大的選擇數量,如果沒有達到,則設置checked=true,同時加入已選擇的集合;反之已經是checked狀態,就是取消選擇狀態了。同時如果設置了mOnSelectListener,回調一下。
ok,其實這里隱藏了一些東西,關于接口回調我們不多贅述了,大家都明白。這里主要看Checked。首先你肯定有幾個問題:
- childView哪來的isChecked(),setChecked()方法?
- 這么做就能改變UI了?
下面我一一解答:首先,我們并非知道adapter#getView返回的是什么View,但是可以肯定的是,大部分View都是沒有isChecked(),setChecked()方法的。但是我們需要有,怎么做?還記得我們setAdapter的時候,給getView外層包了一層TagView么,沒錯,就是TagView起到的作用:
package com.zhy.view.flowlayout; import android.content.Context; import android.view.View; import android.widget.Checkable; import android.widget.FrameLayout; /** * Created by zhy on 15/9/10. */ public class TagView extends FrameLayout implements Checkable { private boolean isChecked; private static final int[] CHECK_STATE = new int[]{android.R.attr.state_checked}; public TagView(Context context) { super(context); } public View getTagView() { return getChildAt(0); } @Override public int[] onCreateDrawableState(int extraSpace) { int[] states = super.onCreateDrawableState(extraSpace + 1); if (isChecked()) { mergeDrawableStates(states, CHECK_STATE); } return states; } /** * Change the checked state of the view * * @param checked The new checked state */ @Override public void setChecked(boolean checked) { if (this.isChecked != checked) { this.isChecked = checked; refreshDrawableState(); } } /** * @return The current checked state of the view */ @Override public boolean isChecked() { return isChecked; } /** * Change the checked state of the view to the inverse of its current state */ @Override public void toggle() { setChecked(!isChecked); } }
我們的TagView實現了Checkable接口,所以提供了問題一的方法。
下面解釋問題二: 這么做就能改變UI了?
我們繼續看TagView這個類,這個類中我們復寫了onCreateDrawableState
,在里面添加了CHECK_STATE
的支持。當我們調用setChecked方法的時候,我們會調用refreshDrawableState()
來更新我們的UI。
但是你可能又會問了,你這個是TagView支持了CHECKED狀態,關它的子View什么事?我們的background可是設置在子View上的。
沒錯,這個問題問的相當好,你還記得我們在setAdapter,addView之前有一行非常核心的代碼:#mAdapter.getView().setDuplicateParentStateEnabled(true);
,setDuplicateParentStateEnabled
這個方法允許我們的CHECKED狀態向下傳遞。
到這,你應該明白了吧~~
所以我們對于UI的變化,只需要設置View的Backgroud為:
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/checked_bg" android:state_checked="true"></item> <item android:drawable="@drawable/normal_bg"></item> </selector>
這樣,如果你的設計稿發生變化,大部分情況下,你只需要改改xml文件就可以了。
ok,到此我們的核心部分的剖析就結束了,接下來貼貼用法:
五、用法
用法其實很簡單,大家可以參考例子,我這里大致貼一下:
(1)設置數據
mFlowLayout.setAdapter(new TagAdapter<String>(mVals) { @Override public View getView(FlowLayout parent, int position, String s) { TextView tv = (TextView) mInflater.inflate(R.layout.tv, mFlowLayout, false); tv.setText(s); return tv; } });
getView中回調,類似ListView等用法。
(2)對于選中狀態
你還在復雜的寫代碼設置選中后標簽的顯示效果么,翔哥說No!
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="@color/tag_select_textcolor" android:drawable="@drawable/checked_bg" android:state_checked="true"></item> <item android:drawable="@drawable/normal_bg"></item> </selector>
設置個background,上面一個狀態為android:state_checked,另一個為正常。寫寫布局文件我都嫌慢,怎么能寫一堆代碼控制效果,設置改個效果,豈不是沒時間dota了。
(3)事件
mFlowLayout.setOnTagClickListener(new TagFlowLayout.OnTagClickListener() { @Override public boolean onTagClick(View view, int position, FlowLayout parent) { Toast.makeText(getActivity(), mVals[position], Toast.LENGTH_SHORT).show(); return true; } });
點擊標簽時的回調。
mFlowLayout.setOnSelectListener(new TagFlowLayout.OnSelectListener() { @Override public void onSelected(Set<Integer> selectPosSet) { getActivity().setTitle("choose:" + selectPosSet.toString()); } });
選擇多個標簽時的回調。
最后肯定有人會問,支持字體變色嗎?ScrollView會沖突嗎?
附上最新效果圖:
大家就自行查看源碼了。
最后,源碼下載地址:https://github.com/hongyangAndroid/FlowLayout
歡迎關注我的微博:
http://weibo.com/u/3165018720
群號:463081660,歡迎入群
微信公眾號:hongyangAndroid
(歡迎關注,第一時間推送博文信息)
來自: http://blog.csdn.net//lmj623565791/article/details/48393217