流式布局FlowLayout.kt

流式布局-源码来自于https://github.com/androidneter/TaobaoHistorySearch
flowlayout-lib中的主要核心代码集合成该类(简化依赖),基本用法不变,去掉其自定义属性,若要使用,需代码动态设置(limitLineCount,isLimit,isOverFlow,mGravity,mSelectedMax)

import android.content.Context
import android.os.Bundle
import android.os.Parcelable
import android.text.TextUtils
import android.util.AttributeSet
import android.util.LayoutDirection
import android.util.Log
import android.view.View
import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import android.view.ViewGroup
import android.widget.Checkable
import android.widget.FrameLayout
import androidx.core.text.TextUtilsCompat
import java.util.*

/**
 * @author: fangyichao
 * @date: 2021/6/1
 *
 * @desc 流式布局-源码来自于https://github.com/androidneter/TaobaoHistorySearch
 *       flowlayout-lib中的主要核心代码集合成该类(简化依赖),基本用法不变
 *       去掉其自定义属性,若要使用,需代码动态设置(limitLineCount,isLimit,isOverFlow,mGravity,mSelectedMax)
 */

open class FlowLayout @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : ViewGroup(context, attrs, defStyle) {
    private var limitLineCount: Int = 3 //默认显示3行 断词条显示3行,长词条显示2行
    private var isLimit: Boolean = true//是否有行限制
    private var isOverFlow = false //是否溢出2行
    private var mGravity: Int = LEFT
    private var mSelectedMax = 1 //-1为不限制数量

    private var mAllViews: MutableList<MutableList<View?>> = ArrayList()
    private var mLineHeight: MutableList<Int> = ArrayList()
    private var mLineWidth: MutableList<Int> = ArrayList()
    private var lineViews: MutableList<View?> = ArrayList()

    private var mTagAdapter: TagAdapter<*>? = null

    private val mSelectedView: MutableSet<Int> = HashSet()

    private var mOnSelectListener: OnSelectListener? = null
    private var mOnTagClickListener: OnTagClickListener? = null
    private var mOnLongClickListener: OnLongClickListener? = null

    open fun isOverFlow(): Boolean {
        return isOverFlow
    }

    open fun setOverFlow(overFlow: Boolean) {
        isOverFlow = overFlow
    }

    fun isLimit(): Boolean {
        return isLimit
    }

    open fun setLimit(limit: Boolean) {
        if (!limit) {
            isOverFlow = false
        }
        isLimit = limit
    }

    open fun getLimitLineCount(): Int {
        return limitLineCount
    }

    open fun setLimitLineCount(count: Int) {
        limitLineCount = count
    }

    open fun getGravity(): Int {
        return mGravity
    }

    open fun setGravity(gravity: Int) {
        mGravity = gravity
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val sizeWidth = MeasureSpec.getSize(widthMeasureSpec)
        val modeWidth = MeasureSpec.getMode(widthMeasureSpec)
        val sizeHeight = MeasureSpec.getSize(heightMeasureSpec)
        val modeHeight = MeasureSpec.getMode(heightMeasureSpec)

        // wrap_content
        var width = 0
        var height = 0
        var lineWidth = 0
        var lineHeight = 0

        //在每一次换行之后记录,是否超过了行数
        var lineCount = 0 //记录当前的行数
        val cCount = childCount
        for (i in 0 until cCount) {
            val child = getChildAt(i)
            if (child.visibility == GONE) {
                if (i == cCount - 1) { //最后一个
                    if (isLimit) {
                        if (lineCount == limitLineCount) {
                            isOverFlow = true
                            break
                        } else {
                            isOverFlow = false
                        }
                    }
                    width = Math.max(lineWidth, width)
                    height += lineHeight
                    lineCount++
                }
                continue
            }
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
            val lp = child.layoutParams as MarginLayoutParams
            val childWidth = (child.measuredWidth + lp.leftMargin + lp.rightMargin)
            val childHeight = (child.measuredHeight + lp.topMargin + lp.bottomMargin)
            if (lineWidth + childWidth > sizeWidth - paddingLeft - paddingRight) {
                if (isLimit) {
                    if (lineCount == limitLineCount) {
                        isOverFlow = true
                        break
                    } else {
                        isOverFlow = false
                    }
                }
                width = Math.max(width, lineWidth)
                lineWidth = childWidth
                height += lineHeight
                lineHeight = childHeight
                lineCount++
            } else {
                lineWidth += childWidth
                lineHeight = Math.max(lineHeight, childHeight)
            }
            if (i == cCount - 1) {
                if (isLimit) {
                    if (lineCount == limitLineCount) {
                        isOverFlow = true
                        break
                    } else {
                        isOverFlow = false
                    }
                }
                width = Math.max(lineWidth, width)
                height += lineHeight
                lineCount++
            }
        }

        setMeasuredDimension(
            if (modeWidth == MeasureSpec.EXACTLY) sizeWidth else width + paddingLeft + paddingRight,
            if (modeHeight == MeasureSpec.EXACTLY) sizeHeight else height + paddingTop + paddingBottom
        )
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        mAllViews.clear()
        mLineHeight.clear()
        mLineWidth.clear()
        lineViews.clear()
        val width = width
        var lineWidth = 0
        var lineHeight = 0

        //如果超过规定的行数则不进行绘制
        var lineCount = 0 //记录当前的行数
        val cCount = childCount
        for (i in 0 until cCount) {
            val child = getChildAt(i)
            if (child.visibility == GONE) continue
            val lp = child
                .layoutParams as MarginLayoutParams
            val childWidth = child.measuredWidth
            val childHeight = child.measuredHeight
            if (childWidth + lineWidth + lp.leftMargin + lp.rightMargin > width - paddingLeft - paddingRight) {
                if (isLimit) {
                    if (lineCount == limitLineCount) {
                        break
                    }
                }
                mLineHeight.add(lineHeight)
                mAllViews.add(lineViews)
                mLineWidth.add(lineWidth)
                lineWidth = 0
                lineHeight = childHeight + lp.topMargin + lp.bottomMargin
                lineViews = ArrayList()
                lineCount++
            }
            lineWidth += childWidth + lp.leftMargin + lp.rightMargin
            lineHeight = Math.max(
                lineHeight, childHeight + lp.topMargin
                        + lp.bottomMargin
            )
            lineViews.add(child)
        }
        mLineHeight.add(lineHeight)
        mLineWidth.add(lineWidth)
        mAllViews.add(lineViews)
        var left = paddingLeft
        var top = paddingTop
        val lineNum = mAllViews.size
        for (i in 0 until lineNum) {
            lineViews = mAllViews[i]
            lineHeight = mLineHeight[i]

            // set gravity
            val currentLineWidth = mLineWidth[i]
            when (mGravity) {
                LEFT -> left = paddingLeft
                CENTER -> left = (width - currentLineWidth) / 2 + paddingLeft
                RIGHT -> {
                    //  适配了rtl,需要补偿一个padding值
                    left = width - (currentLineWidth + paddingLeft) - paddingRight
                    //  适配了rtl,需要把lineViews里面的数组倒序排
                    Collections.reverse(lineViews)
                }
            }
            for (j in lineViews.indices) {
                val child = lineViews[j]
                if (child!!.visibility == GONE) {
                    continue
                }
                val lp = child
                    .layoutParams as MarginLayoutParams
                val lc = left + lp.leftMargin
                val tc = top + lp.topMargin
                val rc = lc + child.measuredWidth
                val bc = tc + child.measuredHeight
                child.layout(lc, tc, rc, bc)
                left += (child.measuredWidth + lp.leftMargin
                        + lp.rightMargin)
            }
            top += lineHeight
        }
    }

    override fun generateLayoutParams(attrs: AttributeSet): LayoutParams {
        return MarginLayoutParams(context, attrs)
    }

    override fun generateDefaultLayoutParams(): LayoutParams {
        return MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
    }

    override fun generateLayoutParams(p: LayoutParams): LayoutParams {
        return MarginLayoutParams(p)
    }

    companion object {
        private const val TAG = "FlowLayout"
        private const val LEFT = -1
        private const val CENTER = 0
        private const val RIGHT = 1
    }

    init {
//        val ta = context.obtainStyledAttributes(attrs, R.styleable.TagFlowLayout)
//        mGravity = ta.getInt(R.styleable.TagFlowLayout_tag_gravity, LEFT)
//        limitLineCount = ta.getInt(R.styleable.TagFlowLayout_limit_line_count, 3)
//        isLimit = ta.getBoolean(R.styleable.TagFlowLayout_is_limit, false)
//        mSelectedMax = ta.getInt(R.styleable.TagFlowLayout_max_select, -1)
        val layoutDirection = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault())
        if (layoutDirection == LayoutDirection.RTL) {
            mGravity = if (mGravity == LEFT) {
                RIGHT
            } else {
                LEFT
            }
        }
//        ta.recycle()
    }


    open fun setOnSelectListener(onSelectListener: OnSelectListener) {
        mOnSelectListener = onSelectListener
    }


    open fun setOnTagClickListener(onTagClickListener: OnTagClickListener) {
        mOnTagClickListener = onTagClickListener
    }

    open fun setOnLongClickListener(onLongClickListener: OnLongClickListener) {
        mOnLongClickListener = onLongClickListener
    }

    open fun <T> setAdapter(adapter: TagAdapter<T>) {
        mTagAdapter = adapter as TagAdapter<*>?
//        mTagAdapter.setOnDataChangedListener(this)
        mTagAdapter!!.setOnDataChangedListener(object : TagAdapter.OnDataChangedListener {
            override fun onChanged() {
                mSelectedView.clear()
                changeAdapter<T>(true)
            }

            override fun onReChanged() {
                changeAdapter<T>(false)
            }

        })
        mSelectedView.clear()
        changeAdapter<T>(true)
    }

    private fun <T> changeAdapter(isChange: Boolean) {
        removeAllViews()
        val adapter = mTagAdapter!! as TagAdapter<T>
        var tagViewContainer: TagView? = null
        val preCheckedList: HashSet<Int>
        if (isChange) {
            preCheckedList = mTagAdapter!!.preCheckedList
        } else {
            preCheckedList = mSelectedView as HashSet<Int>
        }
        for (i in 0 until adapter.count) {
            val tagView = adapter.getView(this, i, adapter.getItem(i))
            tagViewContainer = TagView(context)
            tagView!!.isDuplicateParentStateEnabled = true
            if (tagView.layoutParams != null) {
                tagViewContainer.layoutParams = tagView.layoutParams
            } else {
                val lp = MarginLayoutParams(
                    LayoutParams.WRAP_CONTENT,
                    LayoutParams.WRAP_CONTENT
                )
                lp.setMargins(
                    dip2px(context, 5f),
                    dip2px(context, 5f),
                    dip2px(context, 5f),
                    dip2px(context, 5f)
                )
                tagViewContainer.layoutParams = lp
            }
            val lp = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
            tagView.layoutParams = lp
            tagViewContainer.addView(tagView)
            addView(tagViewContainer)
            val disableList: HashSet<Int> = mTagAdapter!!.getDisableList()
            if (disableList.contains(i)) {
                tagViewContainer.isEnabled = false
            } else {
                if (preCheckedList.contains(i)) {
                    setChildChecked(i, tagViewContainer)
                }
            }
            if (mTagAdapter!!.setSelected(i, adapter.getItem(i))) {
                setChildChecked(i, tagViewContainer)
            }
            tagView.isClickable = false
            val finalTagViewContainer: TagView = tagViewContainer
            tagViewContainer.setOnClickListener(OnClickListener {
                doSelect(finalTagViewContainer, i)
                mOnTagClickListener?.onTagClick(
                    finalTagViewContainer, i
                )
            })
            tagViewContainer.setOnLongClickListener(OnLongClickListener {
                if (mOnLongClickListener != null) {
                    mOnLongClickListener!!.onLongClick(finalTagViewContainer, i)
                    //消费事件,不让事件继续下去
                    return@OnLongClickListener true
                }
                false
            })
        }
        mSelectedView.addAll(preCheckedList)
    }

    open fun setMaxSelectCount(count: Int) {
        if (mSelectedView.size > count) {
            Log.w(TAG, "you has already select more than $count views , so it will be clear .")
            mSelectedView.clear()
        }
        mSelectedMax = count
    }

    open fun getSelectedList(): Set<Int?>? {
        return HashSet(mSelectedView)
    }

    open fun setChildChecked(position: Int, view: TagView) {
        view.isChecked = true
        mTagAdapter!!.onSelected(position, view.tagView)
    }

    open fun setChildUnChecked(position: Int, view: TagView) {
        view.isChecked = false
        mTagAdapter!!.unSelected(position, view.tagView)
    }

    open fun doSelect(child: TagView, position: Int) {
        if (!child.isChecked) {
            //处理max_select=1的情况
            if (mSelectedMax == 1 && mSelectedView.size == 1) {
                val iterator = mSelectedView.iterator()
                val preIndex = iterator.next()
                val pre = getChildAt(preIndex) as TagView
                setChildUnChecked(preIndex, pre)
                setChildChecked(position, child)
                mSelectedView.remove(preIndex)
                mSelectedView.add(position)
            } else {
                if (mSelectedMax > 0 && mSelectedView.size >= mSelectedMax) {
                    return
                }
                setChildChecked(position, child)
                mSelectedView.add(position)
            }
        } else {
            setChildUnChecked(position, child)
            mSelectedView.remove(position)
        }
        mOnSelectListener?.onSelected(HashSet(mSelectedView))
    }

    open fun getAdapter(): TagAdapter<*>? {
        return mTagAdapter
    }


    private val KEY_CHOOSE_POS = "key_choose_pos"
    private val KEY_DEFAULT = "key_default"


    override fun onSaveInstanceState(): Parcelable? {
        val bundle = Bundle()
        bundle.putParcelable(KEY_DEFAULT, super.onSaveInstanceState())
        var selectPos = ""
        if (mSelectedView.size > 0) {
            for (key in mSelectedView) {
                selectPos += "$key|"
            }
            selectPos = selectPos.substring(0, selectPos.length - 1)
        }
        bundle.putString(KEY_CHOOSE_POS, selectPos)
        return bundle
    }

    override fun onRestoreInstanceState(state: Parcelable?) {
        if (state is Bundle) {
            val bundle = state
            val mSelectPos = bundle.getString(KEY_CHOOSE_POS)
            if (!TextUtils.isEmpty(mSelectPos)) {
                val split = mSelectPos!!.split("\\|").toTypedArray()
                for (pos in split) {
                    val index = pos.toInt()
                    mSelectedView.add(index)
                    val tagView = getChildAt(index) as TagView
                    tagView.let { setChildChecked(index, it) }
                }
            }
            super.onRestoreInstanceState(bundle.getParcelable(KEY_DEFAULT))
            return
        }
        super.onRestoreInstanceState(state)
    }


    open fun dip2px(context: Context, dpValue: Float): Int {
        val scale = context.resources.displayMetrics.density
        return (dpValue * scale + 0.5f).toInt()
    }




    interface OnSelectListener {
        fun onSelected(selectPosSet: Set<Int?>?)
    }

    interface OnTagClickListener {
        fun onTagClick(view: View?, position: Int)
    }

    interface OnLongClickListener {
        fun onLongClick(view: View?, position: Int)
    }
}

abstract class TagAdapter<T> {
    private var mTagDatas: List<T>?
    private var mOnDataChangedListener: OnDataChangedListener? = null

    @get:Deprecated("")
    @Deprecated("")
    val preCheckedList = HashSet<Int>()

    private val mDisablePosList = HashSet<Int>()

    constructor(datas: List<T>?) {
        mTagDatas = datas
    }

    fun setData(datas: List<T>?) {
        mTagDatas = datas
    }

    fun getData(): List<T>?{
        return mTagDatas
    }

    @Deprecated("")
    constructor(datas: Array<T>) {
        mTagDatas = ArrayList(Arrays.asList(*datas))
    }

    fun setOnDataChangedListener(listener: OnDataChangedListener?) {
        mOnDataChangedListener = listener
    }

    @Deprecated("")
    fun setSelectedList(vararg poses: Int) {
        val set: MutableSet<Int> = HashSet()
        for (pos in poses) {
            set.add(pos)
        }
        setSelectedList(set)
    }

    @Deprecated("")
    fun setSelectedList(set: Set<Int>?) {
        preCheckedList.clear()
        if (set != null) {
            preCheckedList.addAll(set)
        }
        notifyDataChanged()
    }


    /*public void setEnableList(int... poses) {
    Set<Integer> set = new HashSet<>();
    for (int pos : poses) {
        set.add(pos);
    }
    setEnableList(set);
}

public void setEnableList(Set<Integer> set) {
    mEnablePosList.clear();
    if (set != null) {
        mEnablePosList.addAll(set);
    }
    notifyDataChanged();
}

HashSet<Integer> getEnableList() {
    return mEnablePosList;
}*/
    open fun setDisableList(vararg poses: Int) {
        val set: MutableSet<Int> = HashSet()
        for (pos in poses) {
            set.add(pos)
        }
        setDisableList(set)
    }

    open fun setDisableList(set: Set<Int>?) {
        mDisablePosList.clear()
        if (set != null) {
            mDisablePosList.addAll(set)
        }
        if (mOnDataChangedListener != null) mOnDataChangedListener?.onReChanged()
    }

    open fun getDisableList(): HashSet<Int> {
        return mDisablePosList
    }

    val count: Int
        get() = if (mTagDatas == null) 0 else mTagDatas!!.size

    fun notifyDataChanged() {
        if (mOnDataChangedListener != null) mOnDataChangedListener!!.onChanged()
    }

    fun getItem(position: Int): T {
        return mTagDatas!![position]
    }

    abstract fun getView(parent: FlowLayout?, position: Int, t: T): View?
    fun onSelected(position: Int, view: View?) {
        Log.d("zhy", "onSelected $position")
    }

    fun unSelected(position: Int, view: View?) {
        Log.d("zhy", "unSelected $position")
    }

    fun setSelected(position: Int, t: Any?): Boolean {
        return false
    }

    interface OnDataChangedListener {
        fun onChanged()
        fun onReChanged()
    }
}

class TagView(context: Context) : FrameLayout(context),
    Checkable {
    private var isChecked = false
    val tagView: View
        get() = getChildAt(0)

    public override fun onCreateDrawableState(extraSpace: Int): IntArray {
        val 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 fun setChecked(checked: Boolean) {
        if (isChecked != checked) {
            isChecked = checked
            refreshDrawableState()
        }
    }

    /**
     * @return The current checked state of the view
     */
    override fun isChecked(): Boolean {
        return isChecked
    }

    /**
     * Change the checked state of the view to the inverse of its current state
     */
    override fun toggle() {
        setChecked(!isChecked)
    }

    companion object {
        private val CHECK_STATE = intArrayOf(android.R.attr.state_checked)
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容