前情提要
这个Demo是照搬博主红橙Darren所实现的,作为第一个自定义View学习使用(博主是用Java编写的,我是用Kotlin实现的)
自定义View的实现根据以下步骤处理:
- 分析需求
- 确定自定义属性(编写attrs.xml资源文件)
- 在自定义View中获取自定义属性
- 在布局中使用,绘制
下面提供实现效果和所有源码和部分注释(自己抄了一遍也是迷迷瞪瞪的)
1、自定义View运行效果
效果一:编写的变色自定义View(ColorTrackTextView)实现效果如下
2、使用该View实现变色导航效果
效果二:
使用该View实现变色导航运行效果如下:
正文开始:
【1】实现效果一
1、编写自定义属性
创建attrs.xml文件设置属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ColorTrackTextView">
<attr name="originColor" format="color"/>
<attr name="changeColor" format="color"/>
</declare-styleable>
</resources>
2、创建自定义View
创建自定义View(ColorTrackTextView)
package com.example.myapplicationchangetextcolor
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Paint.FontMetricsInt
import android.graphics.Rect
import android.util.AttributeSet
import android.util.Log
import androidx.appcompat.widget.AppCompatTextView
@SuppressLint("Recycle")
class ColorTrackTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
//创建画笔
private var mOriginPaint = Paint()
private var mChangePaint = Paint()
//设置进度
private var mCurrentProgress = 0f
//枚举类实现设置不同朝向
enum class Direction{
LEFT_TO_RIGHT, RIGHT_TO_LEFT
}
//设置初始朝向
private var mDirection:Direction = Direction.LEFT_TO_RIGHT
init {
// 获取自定义属性
val typedArray = context.theme.obtainStyledAttributes(
attrs,
R.styleable.ColorTrackTextView,
0, 0
)
with(typedArray) {
try {
// 使用资源ID获取自定义属性值
val originColor = getColor(R.styleable.ColorTrackTextView_originColor, Color.BLACK)
val changeColor = getColor(R.styleable.ColorTrackTextView_changeColor, Color.RED)
//设置画笔
mOriginPaint = getPaintByColor(originColor)
mChangePaint = getPaintByColor(changeColor)
} finally {
// 调用 recycle 释放资源
recycle()
}
}
}
override fun onDraw(canvas: Canvas) {
val middle:Int = (mCurrentProgress*getWidth()).toInt()
if(mDirection == Direction.LEFT_TO_RIGHT){
DrawText(canvas,mChangePaint,0,middle)
DrawText(canvas,mOriginPaint,middle,getWidth())
}else{
DrawText(canvas,mChangePaint,getWidth()-middle,getWidth())
DrawText(canvas,mOriginPaint,0,getWidth()-middle)
}
}
fun setDirection(direction: Direction){
this.mDirection = direction
}
fun setCurrentProgress(currentProgress:Float){
this.mCurrentProgress = currentProgress
Log.e(ContentValues.TAG, "Now currentProgress is $currentProgress")
invalidate()
}
private fun DrawText(canvas:Canvas,paint:Paint,start:Int,end:Int){
canvas.save()
// 绘制不变色
val rect_current = Rect(start,0,end,getHeight())
canvas.clipRect(rect_current)
val text = getText().toString()
val bounds = Rect()
paint.getTextBounds(text, 0, text.length, bounds)
val dx = getWidth() / 2 - bounds.width() / 2
val fontMetricsInt: FontMetricsInt = paint.getFontMetricsInt()
val dy = (fontMetricsInt.bottom - fontMetricsInt.top) / 2 - fontMetricsInt.bottom
val baseLine = getHeight() / 2 + dy
canvas.drawText(text, dx.toFloat(), baseLine.toFloat(), paint)
canvas.restore()
}
private fun getPaintByColor(mColor: Int): Paint {
val paint = Paint().apply {
// 设置颜色
color = mColor
// 设置抗锯齿
isAntiAlias = true
// textSize = TextView.textSize 获取不到设置字体大小,报错:Unresolved reference: textSize
//this@ColorTrackTextView 被用来引用当前的 ColorTrackTextView 实例。
// this 关键字是指向当前类实例的引用,而 @ColorTrackTextView 是一个限定符,
// 用来明确指出 this 属于 ColorTrackTextView 类。
textSize = this@ColorTrackTextView.textSize
isFakeBoldText = true
}
return paint
}
fun setChangeColor(changeColor:Int) {
this.mChangePaint.setColor(changeColor)
}
fun setOriginColor(originColor:Int) {
this.mOriginPaint.setColor(originColor)
}
}
3、编写Activity_main.xml
Activity_main.xml 文件布局是实现的是效果一的页面布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
xmlns:tool="http://schemas.android.com/tools"
android:layout_margin="30dp"
android:paddingLeft="16dp"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:paddingBottom="16dp"
tool:context="com.example.myapplicationchangetextcolor.MainActivity">
<com.example.myapplicationchangetextcolor.ColorTrackTextView
android:id="@+id/color_track_tv"
app:originColor="@color/black"
app:changeColor="#F44336"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Look the Fuck Resource Code!"
android:textStyle="bold"
android:textSize="26sp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="leftToRight"
android:text="从左到右"
android:layout_gravity="start"
tool:ignore="OnClick" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="rightToLeft"
android:text="从右到左"
tool:ignore="OnClick" />
</LinearLayout>
</LinearLayout>
4、编辑MainActivity
编辑MainActivity文件,获取控件实现动画效果
package com.example.myapplicationchangetextcolor
import android.animation.ObjectAnimator
import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var mColorTrackTextView:ColorTrackTextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
mColorTrackTextView = findViewById(R.id.color_track_tv)
}
//启动动画效果
fun leftToRight(view: View){
//设置动画方向
mColorTrackTextView.setDirection(ColorTrackTextView.Direction.LEFT_TO_RIGHT)
//从0到1的浮点数值动画是指动画的属性值从一个起始值(0)变化到一个结束值(1)。
//在这个过程中,动画的值会从0.0开始,逐渐增加,直到达到1.0。
//这种类型的动画通常用于表示某种进度或变化
val valueAnimator = ObjectAnimator.ofFloat(0f,1f).apply {
duration = 2000
//添加一个更新监听器 addUpdateListener,它将在动画的每次更新时被调用
addUpdateListener { animation->
//获取当前动画的值
val currentProgress = animation.animatedValue as Float
//调用 mColorTrackTextView 的 setCurrentProgress 方法,
// 传入 currentProgress 作为参数,用于更新视图的进度状态
mColorTrackTextView.setCurrentProgress(currentProgress)
}
}
valueAnimator.start()
}
//同上
fun rightToLeft(view: View){
mColorTrackTextView.setDirection(ColorTrackTextView.Direction.RIGHT_TO_LEFT)
val valueAnimator = ObjectAnimator.ofFloat(0f,1f).apply {
duration = 2000
addUpdateListener { animation->
val currentProgress = animation.animatedValue as Float
mColorTrackTextView.setCurrentProgress(currentProgress)
}
}
valueAnimator.start()
}
}
【2】实现效果二
1、创建ItemFragment
package com.example.myapplicationchangetextcolor
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
class ItemFragment : Fragment() {
// 其他代码...
companion object {
// 使用@JvmStatic注解确保可以从Java代码中调用这个静态方法
@JvmStatic
fun newInstance(item: String): ItemFragment {
val itemFragment = ItemFragment()
val bundle = Bundle()
bundle.putString("title", item) // 假设"title"是您想要传递的键名
itemFragment.arguments = bundle
return itemFragment
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// 使用LayoutInflater来加载布局
val view = inflater.inflate(R.layout.fragment_item, container, false)
// 通过ID查找TextView,并将其转换为TextView类型
val tv: TextView = view.findViewById(R.id.text)
// 获取传递给Fragment的参数
val bundle: Bundle? = arguments
// 从Bundle中获取字符串并设置到TextView上
bundle?.let {
tv.text = it.getString("title")
}
// 返回加载的视图
return view
}
}
2、创建对应的fragment_item
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/text"
android:text="Hello"
android:gravity="center"/>
</LinearLayout>
3、创建ViewPagerActivity
package com.example.myapplicationchangetextcolor
import android.content.ContentValues.TAG
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
class ViewPagerActivity : AppCompatActivity() {
// 定义一个字符串数组
private val items: Array<String> = arrayOf("直播", "推荐", "视频", "图片", "段子", "精华")
// 定义LinearLayout类型的成员变量,用于存放指示器容器
private lateinit var mIndicatorContainer: LinearLayout
// 定义List<ColorTrackTextView>类型的成员变量,用于存放指示器
private lateinit var mIndicators: MutableList<ColorTrackTextView>
// 定义ViewPager类型的成员变量
private lateinit var mViewPager: ViewPager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_view_pager)
// 初始化指示器列表
mIndicators = ArrayList()
// 通过findViewById获取LinearLayout和ViewPager,并进行类型转换
mIndicatorContainer = findViewById(R.id.indicator_view)
mViewPager = findViewById(R.id.view_pager)
// 调用方法初始化指示器和ViewPager
initIndicator()
initViewPager()
}
//初始化一个 ViewPager 组件,设置其适配器和页面滚动监听器,以及配置页面指示器的行为
private fun initViewPager() {
// 为mViewPager设置适配器,使用匿名对象继承自FragmentPagerAdapter
mViewPager.adapter = object : FragmentPagerAdapter(supportFragmentManager) {
override fun getItem(position: Int): Fragment {
// 根据当前position返回一个新的ItemFragment实例
return ItemFragment.newInstance(items[position])
}
override fun getCount(): Int {
// 返回items数组的大小
return items.size
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
// 空实现,如果不需要可以省略
}
}
// 为mViewPager添加页面滚动变化监听器
mViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
重写onPageScrolled方法,处理页面滚动时的事件
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
// 打印日志,显示当前页面位置和位置偏移量
Log.e(TAG, "position --> $position , positionOffset --> $positionOffset")
// 当positionOffset大于0,即页面开始滑动时
if (positionOffset > 0) {
// 获取当前位置的指示器,并设置其滚动方向和进度
val left = mIndicators[position]
left.setDirection(ColorTrackTextView.Direction.RIGHT_TO_LEFT)
left.setCurrentProgress(1 - positionOffset)
// 获取下一个位置的指示器,并设置其滚动方向和进度
val right = mIndicators[position + 1]
right.setDirection(ColorTrackTextView.Direction.LEFT_TO_RIGHT)
right.setCurrentProgress(positionOffset)
}
}
override fun onPageSelected(position: Int) {
// 可以在这里处理页面选中事件
}
override fun onPageScrollStateChanged(state: Int) {
// 可以在这里处理滚动状态改变事件
}
})
}
//初始化一个页面指示器界面元素集合,并配置每个指示器的属性
private fun initIndicator() {
//用于存储 ColorTrackTextView 的实例
mIndicators = mutableListOf()
for (i in items.indices) {
//为每个索引创建一个新的 ColorTrackTextView 实例
val colorTrackTextView = ColorTrackTextView(this)
colorTrackTextView.textSize = 20f
colorTrackTextView.setOriginColor(Color.BLACK)
colorTrackTextView.setChangeColor(Color.RED)
colorTrackTextView.text = items[i]
//用于设置和添加页面指示器(ColorTrackTextView)
// 到一个线性布局容器(mIndicatorContainer)中
val params = LinearLayout.LayoutParams(
// ViewGroup.LayoutParams.WRAP_CONTENT子视图的大小将根据其内容自动调整
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
weight = 1f
}
//应用布局参数到 ColorTrackTextView
colorTrackTextView.layoutParams = params
//将配置好的 colorTrackTextView 添加到 mIndicatorContainer 中
mIndicatorContainer.addView(colorTrackTextView)
//将 colorTrackTextView 实例添加到 mIndicators 列表中
mIndicators.add(colorTrackTextView)
}
}
}
4、创建对应的activity_view_pager
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:layout_marginTop="30dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:id="@+id/indicator_view"/>
<androidx.viewpager.widget.ViewPager
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/view_pager"
android:layout_weight="1"/>
</LinearLayout>
5、修改AndroidManifest文件
修改AndroidManifest文件运行即可,大功告成
<activity
android:name=".ViewPagerActivity"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>