最近因为工作原因,需要转入Android开发,此后不定时记录一些技巧。
要求:
1、设计一个界面,上半部不动,下半部为滑块,点击Tablayout标题,更换不同Fragment,禁止左右滑动更改,不需要更改界面动画;
2、第一个滑块使用FlowLayout显示小标,且为其设置不重复颜色的边框等样式;
3、第二个滑块一键删除EditView输入内容。
实现图示:
完整项目可看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点击图标清除文本内容