前言
经常写自定义View的同学都会知道,当View发生改变时,想要主动的去刷新View,无外乎两个方法:主线程调用invalidate(),子线程调用postInvalidate()。调用上述两个方法后,系统会在合适的时候去刷新我们的View,即回调onDraw方法。带着这样的认知,我们默认会认为在排除其他干扰因素的情况时,只要不调用invaidate()系列方法,我们的自定义View只会保持上一次刷新时的样子。可是Android就总是会给我们带来惊喜(ㄒoㄒ),接下来的测试将会刷新你的认知。
好戏开始
我们先来创建两个自定义View:
/**
* 自定义View
*/
class MyView : View {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
private var mColor: Int = Color.BLUE
private var mPaint: Paint = Paint()
private var mTextPaint: Paint = Paint()
init {
mPaint.color = mColor
mPaint.strokeWidth = 10f
mPaint.style = Paint.Style.STROKE
mTextPaint.color = Color.BLACK
mTextPaint.textSize = 50f
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
Log.e("MyView", "onDraw")
canvas?.drawRect(100f, 100f, 300f, 300f, mPaint)
canvas?.drawText("MyView", 50f, 50f, mTextPaint)
}
open fun setColor(@ColorInt color: Int) {
this.mColor = color
mPaint.color = mColor
}
}
/**
* 自定义View使用双缓冲刷新
*/
class MyViewUseBitmap : View {
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
private var mCanvas: Canvas? = null
private var mBitmap: Bitmap? = null
private var mColor: Int = Color.BLUE
private var mPaint: Paint = Paint()
private var mTextPaint: Paint = Paint()
init {
mPaint.color = mColor
mPaint.strokeWidth = 10f
mPaint.style = Paint.Style.STROKE
mTextPaint.color = Color.BLACK
mTextPaint.textSize = 50f
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (mBitmap == null) {
mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
mCanvas = Canvas(mBitmap!!)
mCanvas?.drawRect(100f, 100f, 300f, 300f, mPaint)
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
Log.e("MyViewUseBitmap", "onDraw")
mBitmap?.apply {
canvas?.drawBitmap(this, 0f, 0f, null)
}
canvas?.drawText("MyViewUseBitmap", 50f, 50f, mTextPaint)
}
open fun setColor(@ColorInt color: Int) {
this.mColor = color
mPaint.color = mColor
mBitmap?.eraseColor(Color.TRANSPARENT)
mCanvas?.drawRect(100f, 100f, 300f, 300f, mPaint)
}
}
上述两个自定义View的最大区别就是一个使用了双缓冲刷新,一个没有使用。
接下来创建我们的Activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myView = findViewById<MyView>(R.id.myView)
val myViewUseBitmap = findViewById<MyViewUseBitmap>(R.id.myViewUseBitmap)
findViewById<View>(R.id.changeColor).setOnClickListener {
myView.setColor(Color.RED)
myViewUseBitmap.setColor(Color.RED)
}
findViewById<View>(R.id.invalidate).setOnClickListener {
myView.invalidate()
myViewUseBitmap.invalidate()
}
}
}
以及layout
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.hardwareandinvalidate.MyView
android:id="@+id/myView"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/myViewUseBitmap"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.example.hardwareandinvalidate.MyViewUseBitmap
android:id="@+id/myViewUseBitmap"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/changeColor"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/myView" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/changeColor"
android:layout_width="150dp"
android:layout_height="60dp"
android:layout_margin="20dp"
android:background="@color/black"
android:text="Change Color"
android:gravity="center"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/invalidate"
android:layout_width="150dp"
android:layout_height="60dp"
android:layout_margin="20dp"
android:background="@color/black"
android:text="Invalidate"
android:gravity="center"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
可以看到当前的界面就是把上述两个自定义View显示出来,【Change Color】按钮做的事情就是改变颜色,【Invalidate】调用了View的invalidate()方法。
来跑起来看一下正常的效果:
可以看到我已经点击了好几次的【Change Color】按钮,但是两个自定义View并没有随之发生变化,知道我点击【Invalidate】按钮,两个View才发生了改变,这样的结果与我们的认知相同。
接下来才开始重头戏:只更改【Change Color】按钮的点击样式:
...
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/changeColor"
android:layout_width="150dp"
android:layout_height="60dp"
android:layout_margin="20dp"
android:background="@drawable/selector_press"//修改点
android:text="Change Color"
android:gravity="center"
android:textColor="@drawable/selector_text_color"//修改点
app:layout_constraintBottom_toBottomOf="parent
app:layout_constraintLeft_toLeftOf="parent" />
...
//selector_press.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/shape_press" android:state_pressed="true"/>
<item android:drawable="@drawable/shape_normal" android:state_pressed="false"/>
</selector>
//shape_press.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="#000000" />
<stroke android:width="1dip" android:color="#3F3F3F"/>
<!-- 圆角 -->
<corners android:radius="10dp" />
<!-- 边距 -->
<padding
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:top="5dp" />
</shape>
//shape_normal.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="#FFFFFF" />
<stroke android:width="2dp" android:color="#000000"/>
<!-- 圆角 -->
<corners android:radius="10dp" />
<!-- 边距 -->
<padding
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:top="5dp" />
</shape>
// selector_text_color.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/white" android:state_pressed="true"/>
<item android:color="@color/black" android:state_pressed="false"/>
</selector>
让我们看一下运行效果:
细心的同学已经发现了其中的不同之处,当我点击【Change Color】按钮后,MyView没有任何变化,但是MyViewUseBitmap却直接自动刷新了,等我点击【Invalidate】按钮后MyView才进行了刷新,MyView的表现是符合我们的预期的。也就是MyViewUseBitmap在我们没有主动调用 invalidate() 时自己进行了主动刷新。像我一样猜测大胆的同学可能会觉得,是按钮的点击样式刷新导致了View Tree的刷新,进而带动MyViewUseBitmap的刷新。但是我可以很负责的告诉你,点击【Change Color】按钮后,通过调试以及日志查看,MyViewUseBitmap的onDraw方法并没有被调用。也就是说在onDraw()没有被回调的情况下,我们的View进行了刷新😱。
在一些场景下,我需要我的View要按照我的刷新时机去刷新,所以对于上述现象就会变得不可控。一开始碰到这种情况,我也是一脸懵逼的,又第N次产生了我不会Android开发的想法(ㄒoㄒ)。好在我最后找到了解决方案,又让我打消了转行的想法。那就是:
// 开启View的硬件加速
setLayerType(LAYER_TYPE_HARDWARE, null)
或
// 开启View的软件加速
setLayerType(LAYER_TYPE_SOFTWARE, null)
为了兼容性可以使用 LAYER_TYPE_SOFTWARE,为了性能可以使用 LAYER_TYPE_HARDWARE。
到此问题就完美解决了,再跑一下看看:
😊开心,又可以愉快的写Android了。
总结
在自定义View没有使用双缓冲机制的情况下,View的刷新是可控的;但是一旦使用了双缓冲机制,界面其他部分的变动,可能会带动我们的View进行刷新。虽然目前没有找到原因,好在找到了解决方法,那就是需要调用View.setLayerType(),传入的参数可以是LAYER_TYPE_HARDWARE或者LAYER_TYPE_SOFTWARE,但是不能是LAYER_TYPE_NONE。
LAYER_TYPE_NONE:是View的默认行为,View会正常渲染,不支持离屏缓冲区
LAYER_TYPE_HARDWARE:硬件加速,View会以硬件的方式渲染为硬件纹理
LAYER_TYPE_SOFTWARE:软件加速,View会以软件的方式渲染为位图
以上有什么不对的地方欢迎探讨。
转载请注明出处https://juejin.cn/post/6951298126418444324