最近因為工作原因,需要轉入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點選圖示清除文本内容