實現一個可定製化的FlowLayout -- 原理篇

FlowLayout 繼承於 ViewGroup ,能夠快速幫您實現 Tablayout 以及 Label 標籤,內含多種效果,幫您快速實現 APP UI 功能,讓您專一代碼架構,告別繁瑣UI。android

若是你也想本身寫一個,能夠參考如下幾篇文章git

實現一個可定製化的TabFlowLayout(一) -- 測量與佈局github

實現一個可定製化的TabFlowLayout(二) -- 實現滾動和平滑過渡canvas

實現一個可定製化的TabFlowLayout(三) -- 動態數據添加與經常使用接口封裝bash

實現一個可定製化的TabFlowLayout(四) -- 與ViewPager 結合,實現炫酷效果架構

實現一個可定製化的TabFlowLayout -- 原理篇maven

實現一個可定製化的TabFlowLayout -- 說明文檔ide

FlowLayout 和 Recyclerview 實現雙聯表聯動佈局

若是您也想快速實現banner,能夠使用這個庫 github.com/LillteZheng…post

一 關聯

allprojects {
    repositories {
       ...
        maven { url 'https://jitpack.io' }
        
    }
}
複製代碼

最新版本請以工程爲準:實現一個可定製化的FlowLayout

implementation 'com.github.LillteZheng:FlowHelper:v1.17'
複製代碼

若是要支持 AndroidX ,若是你的工程已經有如下代碼,直接關聯便可:

android.useAndroidX=true
#自動支持 AndroidX 第三方庫
android.enableJetifier=true
複製代碼

效果

首先,就是 TabFlowLayout 的效果,它的佈局支持橫豎兩種方式,首先先看支持的效果:

沒有結合ViewPager 結合ViewPager
TabFlowLayout豎直,RecyclerView聯動效果

除了 TabFlowLayout,還有 LAbelFlowLayout 標籤式佈局,支持自動換行與顯示更多

LabelFlowLayout LabelFlowLayout 顯示更多

3、原理說明

這裏主要以 TabFlowLayout 來講明,至於 LabelFlowLayout,相信你們看完分析,也知道該怎麼去實現了。

3.1 測量與佈局

從上面的效果看,自定義有挺多種選擇,好比繼承 LinearLayout 或者 HorizontalScrollView … ,但其實直接繼承ViewGroup去動態測量更香; 首先,步驟也很簡單:

  1. 繼承 ViewGroup
  2. 重寫 onMeasure,計算子控件的大小從而肯定父控件的大小
  3. 重寫 onLayout ,肯定子控件的佈局

直接看第二步,因爲是橫向,在測量的時候,須要肯定子控件的寬度累加,而高度,則取子控件中,最大的那個便可,代碼以下所示:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childCount = getChildCount();
        int width = 0;
        int height = 0;
        /**
         * 計算寬高,因爲是橫向 width 應該是全部子控件的累加,不用管模式了
         */
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.GONE){
                continue;
            }
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
            //拿到 子控件寬度
            int cw = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
            int ch = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;

            width += cw;
            //拿到 子控件高度,拿到最大的那個高度
            height = Math.max(height, ch);

        }
        if (MeasureSpec.EXACTLY == heightMode) {
            height = heightSize;
        }
        setMeasuredDimension(width, height);
    }
複製代碼

上面中,子控件的寬度進行累加,高度則取子控件中最大的那個,再經過 setMeasuredDimension(width, height); 賦值給父控件。

接着第三步,重寫 onLayout ,肯定子控件的佈局,因爲是橫向,因此,只須要 child 的 left 一直累加便可:

@Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
       int count = getChildCount();
       int left = 0;
       int top = 0;
       for (int i = 0; i < count; i++) {
           View child = getChildAt(i);
           MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
           int cl = left + params.leftMargin;
           int ct = top + params.topMargin;
           int cr = cl + child.getMeasuredWidth() ;
           int cb = ct + child.getMeasuredHeight();
           //下個控件的起始位置
           left += child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
           child.layout(cl, ct, cr, cb);
       }
   }
複製代碼

這樣,一個簡單的橫向佈局的 TabFlowLayout 即搞定了,我們寫一些控件實驗一下:

<com.zhengsr.tablib.TabFlowLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#15000000"
        >

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="001"/>
		....
    </com.zhengsr.tablib.TabFlowLayout>
複製代碼

效果:

至於給TabFlowLayout 加上 padding 的效果,能夠參考文章: 實現一個可定製化的TabFlowLayout(一) -- 測量與佈局

3.2 實現滾動和平滑過渡

前面中,咱們已經經過 FlowLayout 實現測量和佈局,此次新建一個類 ScrollFlowLayout 是專門實現滾動邏輯。 View 的事件傳遞,大概能夠這樣簡單描述:

當點擊一個控件的時候,它的向下傳遞過程大體以下: activity --> window – > viewGroud --> view 。固然第一次走的是 disPatchTouchEvent 方法;經過源碼知道,若是咱們對 onInterceptTouchEvent 返回true,則父控件接管當前觸摸事件,再也不往下傳遞,而是回調本身的 onTouchEvent 方法。

因爲繼承 ViewGroup ,因此咱們須要重寫它的 onInterceptTouchEvent 方法:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = ev.getX();
                //拿到上次的down座標
                mMoveX = ev.getX();
                break;

            case MotionEvent.ACTION_MOVE:
                float dx = ev.getX() - mLastX;
                if (Math.abs(dx) >= mTouchSlop) {
                    //由父控件接管觸摸事件
                    return true;
                }
                mLastX = ev.getX();
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
複製代碼

上面的代碼中,已經接管了 Touch 的事件,接着能夠在 onTouchEvent 中,拿到了移動的偏移量, 那怎麼實現 View 自身的移動呢? 沒錯,就是使用 ScrollerBy 和 ScrollerTo,它們只改變 View 的內容而不會改變 View 的座標 ,這正是咱們須要的,須要注意的是,向左滑爲正,向右爲負。

  • ScrollerTo(int x,int y) 絕對座標移動,以原點爲參考點
  • ScrollerBy(int x,int y) 相對座標移動,以上一次座標爲參考點
@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                //scroller 向右爲負,向左爲正
                int dx = (int) (mMoveX - event.getX());
                scrollBy(dx, 0);
                mMoveX = event.getX();
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;

        }
        return super.onTouchEvent(event);
    }

複製代碼

效果以下:

但看起來還存在一些問題:

  1. 邊界限制
  2. 滾動不夠流暢

上面問題的優化,請參考工程或者文章 實現一個可定製化的TabFlowLayout(二) -- 實現滾動和平滑過渡

3.3 動態數據添加與經常使用接口封裝

這裏參考至鴻洋的 FlowLayout FlowLayout

考慮到數據動態添加和方便客製化,這裏也採用 adapter 的方式去加載數據,頂部 tab 可能要有未讀消息,或者不一樣的控件,因此 layoutId 確定是要有的,datas 數據確定也是,且這個 data 類型用泛型修飾; 因此,大體的簡約代碼能夠這樣寫:

/**
 * @author by  zhengshaorui on 2019/10/8
 * Describe: 數據構建基類
 */
public abstract class TabAdapter<T> {
    private int mLayoutId;
    private List<T> mDatas;
    public TabAdapter(int layoutId, List<T> data) {
        mLayoutId = layoutId;
        mDatas = data;
    }

    /**
     * 獲取個數
     * @return
     */
    int getItemCount(){
        return mDatas == null ? 0 : mDatas.size();
    }

    /**
     * 獲取id
     * @return
     */
    int getLayoutId(){
        return mLayoutId;
    }

    /**
     * 獲取數據
     * @return
     */
    List<T> getDatas(){
        return mDatas;
    }

    /**
     * 公佈給外部的數據
     * @param view
     * @param data
     * @param position
     */
    public abstract void bindView(View view,T data,int position);

    /**
     * 通知數據改變
     */
    public void notifyDataChanged(){
        if (mListener != null) {
            mListener.notifyDataChanged();
        }
    }

    /**
     * 構建一個listener,用來改變數據
     */

    public AdapterListener mListener;
    void setListener(AdapterListener listener){
        mListener = listener;
    }
   ... 
}
複製代碼

因此,在TabFlowLayout 新增一個 setAdapter ,把數據設置進去便可:

TabFlowLayout flowLayout = findViewById(R.id.triflow);
flowLayout.setAdapter(new TabFlowAdapter<String>(R.layout.item_msg,mTitle2) {

    @Override
    public void bindView(View view, String data, int position) {
        //設置textview 的 text 和 color
        setText(view,R.id.item_text,data)
                .setTextColor(view,R.id.item_text,Color.BLACK);
    }
});
複製代碼

但裏面是怎麼實現的呢?其實就是從 adapter 中拿到 layoutId 和 count,再addView 便可

removeAllViews();
TabAdapter adapter = mAdapter;
int itemCount = adapter.getItemCount();
for (int i = 0; i < itemCount; i++) {
    View view = LayoutInflater.from(getContext()).inflate(adapter.getLayoutId(),this,false);
    adapter.bindView(view,adapter.getDatas().get(i),i);
    configClick(view,i);
    addView(view);
}

複製代碼

效果以下:

細節部分,參考這篇文章: 實現一個可定製化的TabFlowLayout(三) -- 動態數據添加與經常使用接口封裝

3.3.4 與ViewPager 結合,實現炫酷效果

首先要實現的效果以下:

能夠看到 ,上面實現了幾個效果:

  1. 子控件的背景跟着自身大小自動變化
  2. 背景跟着viewpager的滾動自動滑動
  3. 當移動到中間,若是後面有多餘的數據,則讓背景保持在中間,內容移動

首先,實現一個紅色背景框框;首先,思考一下,在 viewgroup 實現 canvas , 是在 onDraw(Canvas canvas) 繪製,仍是在 dispatchDraw(Canvas canvas) 呢?答案爲 dispathDraw ,爲何?

  1. onDraw 繪製內容 onDraw 爲實際要關心的東西,即全部繪製都在這裏。

  2. dispatchDraw 只對ViewGroup有意義 dispatchDraw 一般來說,能夠解釋成繪製 子 View View 繼承drawable,view 組件的繪製會先調用 draw(Canvas canvas) 方法,而後先繪製 Drawable背景,接着纔是調用 onDraw ,而後調用 dispatchDraw方法。dispatchDraw 會分發給組件去繪製。 不過 View 是沒有子 view 的,因此dispatchDraw對它來講沒意義。

因此,當自定義 ViewGroup 時,假如 ViewGroup 沒有背景,是不會回調 onDraw 方法的,只會回調dispatchDraw,有背景纔會走正常順序。(不信? 你能夠把你的 tabflowlayout 背景去掉,在 onDraw 繪製,看看有沒有用)

這樣,咱們先拿到,第一個子 view 的大小,肯定 rect:

View child = getChildAt(0);
if (child != null) {
    //拿到第一個數據
    MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
    mRect.set(getPaddingLeft()+params.leftMargin,
            getPaddingTop()+params.topMargin,
            child.getMeasuredWidth()-params.rightMargin,
            child.getMeasuredHeight() - params.bottomMargin);
}

複製代碼

接着在 dispatchDraw 中繪製圓角矩形:

@Override
protected void dispatchDraw(Canvas canvas) {
    //繪製一個矩形
    canvas.drawRoundRect(mRect, 10, 10, mPaint);
    super.dispatchDraw(canvas);
}
複製代碼

效果以下:

接着,怎麼讓這個背景跟着 viewpager 移動呢?

能夠從 viewpager 的頁面監聽中拿到 onPageScrolled 方法:

public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
複製代碼

三個參數解釋以下:

  • position :當前第一頁的索引,比較有意思的是,當右滑時,position 表示當前頁面,當左滑時,爲當前頁面減1;
  • positionOffset:當前頁面移動的百分比,[0,1]之間;右滑0-1,左滑 1-0;
  • positionOffsetPixels:當前頁面移動的像素

從上面能夠看到,咱們只須要 position 和 positionOffset 便可,即上一個 左邊爲要移動的偏移量,加上 子 view 的寬度變化便可:

@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    /**
     * position 當前第一頁的索引,比較有意思的是,當右滑時,position 表示當前頁面,當左滑時,爲當前頁面減1;
     * positionOffset 當前頁面移動的百分比
     * positionOffsetPixels 當前頁面移動的像素
     */
    if (position < getChildCount() - 1) {
        //上一個view
        final View lastView = getChildAt(position);
        //當前view
        final View curView = getChildAt(position + 1);
        //左邊偏移量
        float left = lastView.getLeft() + positionOffset * (curView.getLeft() - lastView.getLeft());
        //右邊表示寬度變化
        float right = lastView.getRight() + positionOffset * (curView.getRight() - lastView.getRight());
        mRect.left = left;
        mRect.right = right;
        postInvalidate();
  }
}
複製代碼

這樣就能夠移動了,細節部分請參考這篇文章:實現一個可定製化的TabFlowLayout(四) -- 與ViewPager 結合,實現炫酷效果

擴展

瞭解了 TabFlowLayout 實現過程,那麼實現 LabelFlowLayout 也能照壺畫瓢了。無非就是測量的時候,判斷是否要換行,而後再在 onLayout 去排列子控件的位置。

這裏來了解一下,LabelFlowLayout 顯示更多的漸隱效果怎麼實現的。

首先,當咱們限制爲 2 行時,須要顯示一個更多的效果,這裏爲了方便客製化,添加一個 layoutId 讓用戶去配置。

那怎麼讓它顯示在下面呢?

首先,拿到 layoutId 以後,先轉換爲view,爲了拿到 view 的正確寬高,須要把它給 LabelFlowLayout 去協助測量,並增長 view 的高度一半用來顯示,因此在 onMeasure 中,能夠這樣去寫:

/**
 * layoutId 須要父控件即 LabelFlowLayout 去幫助測量,才能經過
 * getMeasuredxxx 拿到正確的寬高、
 */
if (mView != null) {
    measureChild(mView, widthMeasureSpec, heightMeasureSpec);
    //添加它的 1/2 來變模糊
    mViewHeight += mView.getMeasuredHeight() / 2;
    setMeasuredDimension(mLineWidth, mViewHeight);
}
複製代碼

那虛化效果怎麼弄呢?其實能夠從 paint 下手。

首先,把 view 轉換成 bitmap,接着對 paint 設置一個 shader ,上半部分爲透明色,下半部分則是和背景色一直,以下:

/**
 * 拿到 view 的 bitmap
 */
mView.layout(0, 0, getWidth(), mView.getMeasuredHeight());
mView.buildDrawingCache();
mBitmap = mView.getDrawingCache();
/**
 * 同時加上一個 shader,讓它有模糊效果
 */
Shader shader = new LinearGradient(0, 0, 0,
        getHeight(), Color.TRANSPARENT, mShowMoreColor, Shader.TileMode.CLAMP);
mPaint.setShader(shader);
mBitRect.set(l, getHeight() - mView.getMeasuredHeight(), r, getHeight());
複製代碼

而後再 dispatchDraw 中把效果和 bitmap 繪製上去便可:

@Override
protected void dispatchDraw(Canvas canvas) {
    super.dispatchDraw(canvas);
    if (isLabelMoreLine() && mBitmap != null) {
        canvas.drawPaint(mPaint);
        canvas.drawBitmap(mBitmap, mBitRect.left, mBitRect.top, null);
    }

}
複製代碼

至此,FLowHelper 的原理就基本分析完了,你們能夠先本身實現一遍,而後再參考工程代碼。

相關文章
相關標籤/搜索