Android TagFlowLayout完全解析 一款针对Tag的布局

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.****.net/jiangjunshow

也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

                     
 

转载请标明出处:
  http://blog.****.net/lmj623565791/article/details/48393217
  本文出自:【张鸿洋的博客】

一、概述

本文之前,先提一下关于上篇博文的100多万访问量请无视,博文被刷,我也很郁闷,本来想把那个文章放到草稿箱,结果放不进去,还把日期弄更新了,实属无奈。

ok,开始今天的博文,今天要说的是TagFlowLayout,说这个之前必须提一下FlowLayout,如果你不了解,可以先阅读之前的博文:Android 自定义ViewGroup 实战篇 -> 实现FlowLayout或者观看视频
打造Android中的流式布局和热门标签

因为本身FlowLayout本身的预期是提供一种新的布局的方式,但是呢,在实际的开发中,大家更多的是使用在商品标签,搜索关键字的场景,那么就涉及到一些交互:

  • 比如用户选择了某个标签,首先你要去改变标签的样子给用户一个反馈,其次你需要记录用户的选择。
  • 那么在选择过程中还有多选的情况,比如4选2,4选3等等。
  • 还有…

类似京东的这个选择商品的图:

Android TagFlowLayout完全解析 一款针对Tag的布局

对于上述的情况呢,FlowLayout只能说能够实现View的显示没有问题,而对于点击某个Tag,以及修改某个Tag的样子,可能需要编写大量的代码,且设计只要稍微的改下显示的效果,估计就得加班了。

既然这么多的不方便,那么我们现在就在FlowLayout的基础上,编写TagFlowLayout去完善,目前支持:

  • 以setAdapter形式注入数据
  • 直接设置selector为background即可完成标签选则的切换,类似CheckBox
  • 支持控制选择的Tag数量,比如:单选、多选
  • 支持setOnTagClickListener,当点击某个Tag回调
  • 支持setOnSelectListener,当选择某个Tag后回调
  • 支持adapter.notifyDataChanged
  • Activity重建(或者旋转)后,选择的状态自动保存

我们的效果图:

Android TagFlowLayout完全解析 一款针对Tag的布局

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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

可以看到很简单,这是一个抽象类,那么具体的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);   }}@Overridepublic void onChanged(){   changeAdapter();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

ok,可以看到当你调用setAdapter进来,首先我们会注册mTagAdapter.setOnDataChangedListener这个回调,主要是用于响应notifyDataSetChanged()。然后进入changeAdapter方法,在这里首先移除所有的子View,然后根据mAdapter.getView的返回,开始逐个构造子View,然后进行添加。

这里注意下:我们的上述的代码,对mAdapter.getView返回的View,外围报了一层TagView,这里暂时不要去想,我们后面会细说。

到此,我们的Adapter添加完毕。


三、支持onTagClickListener

ok,这个接口也非常重要,当然我私下了解了下,很多同学都加上了,但是基本都是对单个标签View去setOnClickListener,然后去比对Tag确定点击的是哪个标签,最后回调出来。当然,我们这里考虑一种更优雅的方式:

我们从父控件下手,当我们确定用户点击在我们的TagFlowLayout上时,我们根据用户点击的坐标,看看是否点击的是我们的某个View,然后进行click回调。是不是有点像事件分发,哈,我们这里可以称为点击分发。

那么,我们需要关注的就是onTouchEventperformClick方法。

 @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;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

可以看到我们这里巧妙的利用了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);    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

可以看到,如果设置了setOnTagClickListener,我们显示的设置了父ViewsetClickable(true)。以防万一父View不具备消费事件的能力。


四、全面支持Checked

这一节呢,主要包含支持几个功能:
* 直接设置selector为background即可完成标签选则的切换,类似CheckBox
* 支持控制选择的Tag数量,比如:单选、多选
* 支持setOnSelectListener,当选择某个Tag后回调

首先,我们提供了两个自定义的属性,multi_suppoutmax_select。一个是指出是否支持选择(如果为false,意味着你只能通过setOnTagClickListener去做一些操作),一个是设置最大的选择数量,-1为不限制数量。

ok,那么核心的代码依然在performClick中被调用的:

@Overridepublic 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));       }   }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

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);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

我们的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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这样,如果你的设计稿发生变化,大部分情况下,你只需要改改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;       }   });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

设置个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;  }});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

点击标签时的回调。

mFlowLayout.setOnSelectListener(new TagFlowLayout.OnSelectListener(){  @Override  public void onSelected(Set<Integer> selectPosSet)  {      getActivity().setTitle("choose:" + selectPosSet.toString());  }});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

选择多个标签时的回调。

最后肯定有人会问,支持字体变色吗?ScrollView会冲突吗?
附上最新效果图:

Android TagFlowLayout完全解析 一款针对Tag的布局

大家就自行查看源码了。

最后,源码下载地址:https://github.com/hongyangAndroid/FlowLayout


欢迎关注我的微博:
http://weibo.com/u/3165018720


 

群号:463081660,欢迎入群

   

微信公众号:hongyangAndroid
  (欢迎关注,第一时间推送博文信息)
  Android TagFlowLayout完全解析 一款针对Tag的布局

           

给我老师的人工智能教程打call!http://blog.****.net/jiangjunshow

Android TagFlowLayout完全解析 一款针对Tag的布局
                     
 

转载请标明出处:
  http://blog.****.net/lmj623565791/article/details/48393217
  本文出自:【张鸿洋的博客】

一、概述

本文之前,先提一下关于上篇博文的100多万访问量请无视,博文被刷,我也很郁闷,本来想把那个文章放到草稿箱,结果放不进去,还把日期弄更新了,实属无奈。

ok,开始今天的博文,今天要说的是TagFlowLayout,说这个之前必须提一下FlowLayout,如果你不了解,可以先阅读之前的博文:Android 自定义ViewGroup 实战篇 -> 实现FlowLayout或者观看视频
打造Android中的流式布局和热门标签

因为本身FlowLayout本身的预期是提供一种新的布局的方式,但是呢,在实际的开发中,大家更多的是使用在商品标签,搜索关键字的场景,那么就涉及到一些交互:

  • 比如用户选择了某个标签,首先你要去改变标签的样子给用户一个反馈,其次你需要记录用户的选择。
  • 那么在选择过程中还有多选的情况,比如4选2,4选3等等。
  • 还有…

类似京东的这个选择商品的图:

Android TagFlowLayout完全解析 一款针对Tag的布局

对于上述的情况呢,FlowLayout只能说能够实现View的显示没有问题,而对于点击某个Tag,以及修改某个Tag的样子,可能需要编写大量的代码,且设计只要稍微的改下显示的效果,估计就得加班了。

既然这么多的不方便,那么我们现在就在FlowLayout的基础上,编写TagFlowLayout去完善,目前支持:

  • 以setAdapter形式注入数据
  • 直接设置selector为background即可完成标签选则的切换,类似CheckBox
  • 支持控制选择的Tag数量,比如:单选、多选
  • 支持setOnTagClickListener,当点击某个Tag回调
  • 支持setOnSelectListener,当选择某个Tag后回调
  • 支持adapter.notifyDataChanged
  • Activity重建(或者旋转)后,选择的状态自动保存

我们的效果图:

Android TagFlowLayout完全解析 一款针对Tag的布局

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);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

可以看到很简单,这是一个抽象类,那么具体的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);   }}@Overridepublic void onChanged(){   changeAdapter();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

ok,可以看到当你调用setAdapter进来,首先我们会注册mTagAdapter.setOnDataChangedListener这个回调,主要是用于响应notifyDataSetChanged()。然后进入changeAdapter方法,在这里首先移除所有的子View,然后根据mAdapter.getView的返回,开始逐个构造子View,然后进行添加。

这里注意下:我们的上述的代码,对mAdapter.getView返回的View,外围报了一层TagView,这里暂时不要去想,我们后面会细说。

到此,我们的Adapter添加完毕。


三、支持onTagClickListener

ok,这个接口也非常重要,当然我私下了解了下,很多同学都加上了,但是基本都是对单个标签View去setOnClickListener,然后去比对Tag确定点击的是哪个标签,最后回调出来。当然,我们这里考虑一种更优雅的方式:

我们从父控件下手,当我们确定用户点击在我们的TagFlowLayout上时,我们根据用户点击的坐标,看看是否点击的是我们的某个View,然后进行click回调。是不是有点像事件分发,哈,我们这里可以称为点击分发。

那么,我们需要关注的就是onTouchEventperformClick方法。

 @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;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48

可以看到我们这里巧妙的利用了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);    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

可以看到,如果设置了setOnTagClickListener,我们显示的设置了父ViewsetClickable(true)。以防万一父View不具备消费事件的能力。


四、全面支持Checked

这一节呢,主要包含支持几个功能:
* 直接设置selector为background即可完成标签选则的切换,类似CheckBox
* 支持控制选择的Tag数量,比如:单选、多选
* 支持setOnSelectListener,当选择某个Tag后回调

首先,我们提供了两个自定义的属性,multi_suppoutmax_select。一个是指出是否支持选择(如果为false,意味着你只能通过setOnTagClickListener去做一些操作),一个是设置最大的选择数量,-1为不限制数量。

ok,那么核心的代码依然在performClick中被调用的:

@Overridepublic 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));       }   }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

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);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

我们的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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这样,如果你的设计稿发生变化,大部分情况下,你只需要改改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;       }   });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

设置个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;  }});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

点击标签时的回调。

mFlowLayout.setOnSelectListener(new TagFlowLayout.OnSelectListener(){  @Override  public void onSelected(Set<Integer> selectPosSet)  {      getActivity().setTitle("choose:" + selectPosSet.toString());  }});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

选择多个标签时的回调。

最后肯定有人会问,支持字体变色吗?ScrollView会冲突吗?
附上最新效果图:

Android TagFlowLayout完全解析 一款针对Tag的布局

大家就自行查看源码了。

最后,源码下载地址:https://github.com/hongyangAndroid/FlowLayout


欢迎关注我的微博:
http://weibo.com/u/3165018720


 

群号:463081660,欢迎入群

   

微信公众号:hongyangAndroid
  (欢迎关注,第一时间推送博文信息)
  Android TagFlowLayout完全解析 一款针对Tag的布局