Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android TagFlowLayout完全解析 一款針對Tag的布局

Android TagFlowLayout完全解析 一款針對Tag的布局

編輯:關於Android編程

 

一、概述

本文之前,先提一下關於上篇博文的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
{
    private List mTagDatas;
    private OnDataChangedListener mOnDataChangedListener;

    public TagAdapter(List datas)
    {
        mTagDatas = datas;
    }

    public TagAdapter(T[] datas)
    {
        mTagDatas = new ArrayList(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(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文件就可以了。

ok,到此我們的核心部分的剖析就結束了,接下來貼貼用法:


五、用法

用法其實很簡單,大家可以參考例子,我這裡大致貼一下:

(1)設置數據

mFlowLayout.setAdapter(new TagAdapter(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!



    
    

設置個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 selectPosSet)
  {
      getActivity().setTitle(choose: + selectPosSet.toString());
  }
});

選擇多個標簽時的回調。

最後肯定有人會問,支持字體變色嗎?ScrollView會沖突嗎?
附上最新效果圖:

大家就自行查看源碼了。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved