天天看点

Android使用Tablayout+ViewPager结合实例

最近因为工作原因,需要转入Android开发,此后不定时记录一些技巧。

要求:

1、设计一个界面,上半部不动,下半部为滑块,点击Tablayout标题,更换不同Fragment,禁止左右滑动更改,不需要更改界面动画;

2、第一个滑块使用FlowLayout显示小标,且为其设置不重复颜色的边框等样式;

3、第二个滑块一键删除EditView输入内容。

实现图示:

Android使用Tablayout+ViewPager结合实例
Android使用Tablayout+ViewPager结合实例

完整项目可看github

关于themes的一些小设计  
在themes.xml的style样式中可添加以下内容
<item name="windowActionBar">false</item>                   <!--取消标题栏,用于继承AppCompatActivity的活动-->
<item name="windowNoTitle">true</item>                      <!--不显示标题栏,用于继承Activity的活动-->
<item name="android:windowEnableSplitTouch">false</item>    <!--控制window,禁用多点触控,一次点击事件只传递一个View-->
<item name="android:splitMotionEvents">false</item>         <!--控制View,禁用多点触控,一次点击事件只传递一个View-->
<item name="android:windowFullscreen">true</item>           <!--全屏显示-->
           

工具类widget

重写ViewPager

设计为禁止左右滑动,取消滑动动画

package com.demo.widget

import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager

/**
 * 禁止ViewPager左右滑动更滑界面,取消滑动时的动画效果
 */
class BanScollViewPager : ViewPager {
    private var isCanScroll = true

    constructor(context: Context?) : super(context!!)
    constructor(context: Context?, attrs: AttributeSet?) : super(context!!, attrs)

    // 事件处理
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(arg0: MotionEvent?): Boolean {
        return if (isCanScroll) {
            false
        } else {
            super.onTouchEvent(arg0)
        }
    }

    //事件拦截
    override fun onInterceptTouchEvent(arg0: MotionEvent?): Boolean {
        return if (isCanScroll) {
            false
        } else {
            super.onInterceptTouchEvent(arg0)
        }
    }

    // 直接切换界面,不要动画
    override fun setCurrentItem(item: Int) {
        super.setCurrentItem(item, false)
    }
}
           

重写TextView

具体可见 Android 设计TextView的边框样式.

实现FlowLayout

package com.demo.widget

import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import kotlin.math.max

class FlowLayout : ViewGroup {

    private val mHorizontalSpacing = 20
    private val mVerticalSpacing = 10

    private var allLines: ArrayList<ArrayList<View>>? = null
    private var lineHeights: ArrayList<Int>? = null

    constructor(context: Context?) : super(context) {}
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {}
    constructor(
        context: Context?,
        attrs: AttributeSet?,
        defStyleAttr: Int) : super(context, attrs, defStyleAttr) {}

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    constructor(
        context: Context?,
        attrs: AttributeSet?,
        defStyleAttr: Int,
        defStyleRes: Int,
    ) : super(context, attrs, defStyleAttr, defStyleRes) {}

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        initMeasureParams()
        //度量孩子
        val childCount = childCount

        val paddingLeft = paddingLeft
        val paddingRight = paddingRight
        val paddingTop = paddingTop
        val paddingBottom = paddingBottom

        val selfWidth = MeasureSpec.getSize(widthMeasureSpec)   //ViewGroup解析的宽度
        val selfHeight = MeasureSpec.getSize(heightMeasureSpec) //ViewGroup解析的高度

        var lineViews = ArrayList<View>()   //保存一行中所有的view

        var lineWidthUsed = 0   //记录这行已经使用了多宽的size
        var lineHeight = 0  //一行的行高

        var parentNeededWidth = 0   //measure过程中,子view要求的父ViewGroup的宽
        var parentNeededHeight = 0  //measure过程中,子view要求的父ViewGroup的高

        for(i in 0 until childCount){
            val childView = getChildAt(i)
            val childP = childView.layoutParams

            val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childP.width)
            val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childP.height)

            childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)

            //获取子view的宽高
            val childMeasureWidth = childView.measuredWidth
            val childMeasureHeight = childView.measuredHeight

            //通过宽度来判断是否需要换行,通过换行后的每行的行高来获取整个ViewGroup的行高
            //如果需要换行
            if(childMeasureWidth + lineWidthUsed + mHorizontalSpacing > selfWidth){
                allLines!!.add(lineViews)
                lineHeights!!.add(lineHeight)

                //一旦换行,我们就可以判断当前行需要的宽和高了,所以此时要记录下来
                parentNeededHeight += lineHeight + mVerticalSpacing
                parentNeededWidth = max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing)

                lineViews = ArrayList()
                lineWidthUsed = 0
                lineHeight = 0
            }

            //view是分行layout的,所以要记录每一行有哪些View,这样可以方便layout布局
            lineViews.add(childView)

            lineWidthUsed += childMeasureWidth + mHorizontalSpacing
            lineHeight = max(lineHeight, childMeasureHeight)
        }
        if(lineViews.size > 0){
            allLines!!.add(lineViews)
            lineHeights!!.add(lineHeight)
            parentNeededHeight += lineHeight + mVerticalSpacing
            parentNeededWidth = max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing)
        }

        //根据子View的度量结果,来重新度量自己ViewGroup
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)

        val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeededWidth
        val realHeight = if (heightMode == MeasureSpec.EXACTLY) selfHeight else parentNeededHeight

        setMeasuredDimension(realWidth, realHeight)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        val lineCount = allLines!!.size

        var curL = paddingLeft
        var curT = paddingTop

        for(i in 0 until lineCount){
            val lineViews = allLines!![i]
            val lineHeight = lineHeights!![i]
            for(j in 0 until lineViews.size){
                val view = lineViews[j]
                val left = curL
                val top = curT
                val right = left + view.measuredWidth
                val bottom = top + view.measuredHeight
                view.layout(left, top, right, bottom)
                curL = right + mHorizontalSpacing
            }
            curL = paddingLeft
            curT += lineHeight + mVerticalSpacing
        }

    }

    private fun initMeasureParams(){
        if (allLines == null) {
            allLines = ArrayList()
        }else{
            allLines!!.clear()
        }
        if (lineHeights == null) {
            lineHeights = ArrayList()
        } else {
            lineHeights!!.clear()
        }
    }
}
           

xml 设计

demo_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <ImageView
            android:layout_width="0dp"
            android:layout_height="200dp"
            android:id="@+id/top_layout"
            android:scaleType="fitXY"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            android:src="@drawable/top_bg"/>

        <com.google.android.material.tabs.TabLayout
            android:layout_width="0dp"
            android:layout_height="80dp"
            android:id="@+id/tablayout"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/top_layout"
            app:tabBackground="@color/white"    // 设置标题背景颜色
            android:background="@color/white"   // 设置标题栏背景颜色
            app:tabRippleColor="@android:color/transparent"   // 点击标题的阴影色彩,设为透明
            app:tabIndicator="@drawable/selector_indicator"   // 设置下方指示器图样
            app:tabPaddingEnd="50dp"
            app:tabPaddingStart="50dp"
            app:tabTextAppearance="@style/TablayoutTextStyle"    // 设置标题文字样式,tablayout中对于文字样式设置需在tabTextAppearance中
            style="@style/TablayoutStyle">    // 设置标题栏样式

            <com.google.android.material.tabs.TabItem
                android:id="@+id/tab_1"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

            <com.google.android.material.tabs.TabItem
                android:id="@+id/tab_2"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />

        </com.google.android.material.tabs.TabLayout>

        <com.demo.widget.BanScollViewPager
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:id="@+id/fragment_view"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tablayout" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>
           

两个滑块布局

两个界面方法一样,此处以first_fragment为例:

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/title1"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="AddTextViewBorderStyle"
            android:textColor="#de000000"
            android:textSize="30sp"
            android:textStyle="bold"
            android:layout_marginTop="40dp"
            android:layout_marginLeft="80dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <com.demo.widget.FlowLayout
            android:id="@+id/flowlayout"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:orientation="horizontal"
            android:layout_marginTop="80dp"
            android:layout_marginLeft="80dp"
            android:layout_marginRight="80dp"
            android:paddingRight="20dp"
            app:layout_constraintTop_toBottomOf="@+id/title1"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>
           

Style 样式

<?xml version="1.0" encoding="utf-8"?>
<resources>
    //TextBorderView
    <declare-styleable name="TextViewBorder">
        <attr name="borderColor" format="color"/>
        <attr name="borderRadius" format="integer"/>
        <attr name="borderStrokeWidth" format="integer"/>
    </declare-styleable>

    //TabLayoutStyle
    <style name="TablayoutStyle" parent="Base.Widget.Design.TabLayout">
        <item name="background">@color/white</item>
        <item name="tabIndicatorColor">#ff3333</item>
        <item name="tabSelectedTextColor">#ff3333</item>
    </style>

    //TabLayoutTextStyle
    <style name="TablayoutTextStyle" parent="TextAppearance.Design.Tab">
        <item name="android:textStyle">bold</item>
        <item name="android:textSize">26sp</item>
        <item name="textAllCaps">false</item>
        <item name="android:textColor">#de000000</item>
    </style>
</resources>
           

Drawable 图样

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 若不设置 gravity,则 Indicator 宽度会填满整个 item -->
    <item android:gravity="center_horizontal">
        <shape>
            <corners android:radius="3dp" />
            <size
                android:width="25dp"
                android:height="7dp" />
        </shape>
    </item>
</layer-list>
           

Animator 动画效果

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:state_pressed="true">
        <set>
            <objectAnimator
                android:duration="100"
                android:interpolator="@android:interpolator/decelerate_cubic"
                android:propertyName="scaleX"
                android:valueTo="0.9"
                android:valueType="floatType" />
            <objectAnimator
                android:duration="100"
                android:interpolator="@android:interpolator/decelerate_cubic"
                android:propertyName="scaleY"
                android:valueTo="0.9"
                android:valueType="floatType" />
        </set>
    </item>

    <item android:state_pressed="false">
        <set>
            <objectAnimator
                android:duration="@android:integer/config_mediumAnimTime"
                android:interpolator="@android:interpolator/bounce"
                android:propertyName="scaleX"

                android:valueTo="1"
                android:valueType="floatType" />
            <objectAnimator
                android:duration="@android:integer/config_mediumAnimTime"
                android:interpolator="@android:interpolator/bounce"
                android:propertyName="scaleY"

                android:valueTo="1"
                android:valueType="floatType" />
        </set>
    </item>

</selector>
           

Activity 设计

DemoActivity.kt

package com.demo.tablayout

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.content.Context
import android.content.Intent
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import com.google.android.material.tabs.TabLayout
import com.demo.fragment.FirstFragment
import com.demo.fragment.SecondFragment
import com.demo.tablayout.databinding.DemoActivityBinding

class DemoActivity : AppCompatActivity() {

    companion object {
        fun start(context: Context) {
            context.startActivity(
                Intent().apply {
                    setClass(context, DemoActivity::class.java)
                }
            )
        }
    }

    // 获取给定位置对应的Fragment
    class DemoAdapter(var fmList: List<Fragment>, fm: FragmentManager?) : FragmentStatePagerAdapter(
        fm!!
    ) {
        override fun getItem(item: Int) = fmList[item]
        override fun getCount() = fmList.size
    }

    private lateinit var binding: DemoActivityBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.demo_activity)
        binding.lifecycleOwner = this
        // 增加一个监听器,viewPager的Fragment界面随Tablayout改变而改变
        binding.fragmentView.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(binding.tablayout))
        init()
    }

    private fun init() {

        val fragment = listOf(FirstFragment(), SecondFragment())
        val tabTitle = listOf("Demo 1", "Demo 2")

        binding.fragmentView.adapter = DemoAdapter(fragment, supportFragmentManager)

        binding.tablayout.setupWithViewPager(binding.fragmentView)
        for(i in 0..fragment.size ){
            binding.tablayout.getTabAt(i)?.text = tabTitle.get(i)
        }
    }

}
           

滑块界面

FirstFragment.kt
package com.demo.fragment

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import com.demo.tablayout.R
import com.demo.tablayout.databinding.FirstFragmentBinding
import com.demo.widget.TextBorderView

class FirstFragment: Fragment() {
    lateinit var binding: FirstFragmentBinding

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        // Inflate the layout for this fragment
        binding= DataBindingUtil.inflate(inflater, R.layout.first_fragment,container,false);
        return binding.root

        // 另一写法
        /*return inflater.inflate(
             R.layout.tutor_introduce, container, false
         )*/
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        val labelLists = listOf("星期一", "星期二" ,"星期三", "星期四")
        val flowViewLists = setTutorLabel(labelLists)

        for(flowView in flowViewLists){
            binding.flowlayout.addView(flowView)
        }
    }

    /**
     * 设置多个文本的颜色,返回View,颜色不重复
     */
    fun setTutorLabel(labelList: List<String>) : ArrayList<View>{
        val labelView = ArrayList<View>()

        val colorList = mutableListOf(R.color.purple_200, R.color.teal_700, R.color.teal_200,
                                        R.color.gray, R.color.red)

        for (index in 0..labelList.size - 1) {
            val cIndex: Int = (colorList.size * Math.random()).toInt()
            val color = colorList[cIndex]

            val tv = TextBorderView(getContext()!!, null)
            tv.setBorderColor(ContextCompat.getColor(getContext()!!, color))
            tv.setTextColor(ContextCompat.getColor(getContext()!!, color))
            tv.setTextSize(30F)
            tv.setBorderRadius(50)
            tv.setPadding(24, 11, 23, 13)
            tv.text = labelList[index]
            labelView.add(tv)
            colorList.remove(color)

        }
    return labelView
    }

}
           
SecondFragment.kt

结构与滑块界面一类似,具体逻辑可看

Android EditText点击图标清除文本内容