6

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

 3 years ago
source link: https://blog.csdn.net/lmj623565791/article/details/48393217
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

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

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

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

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

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

20150912135251785

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

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

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

我们的效果图:

20150912135311973

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回调。是不是有点像事件分发,哈,我们这里可以称为点击分发。

那么,我们需要关注的就是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;
    }

可以看到我们这里巧妙的利用了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_suppoutmax_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会冲突吗?
附上最新效果图:

20150912145240019

大家就自行查看源码了。

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


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


群号:463081660,欢迎入群

微信公众号:hongyangAndroid
(欢迎关注,第一时间推送博文信息)
1422600516_2905.jpg


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK