Inconsistent refresh mechanism under Android View double-buffered drawing

Inconsistent refresh mechanism under Android View double-buffered drawing

Preface

Students who often write custom Views will know that when the View changes, they want to actively refresh the View. There are nothing more than two methods: the main thread calls invalidate(), and the child thread calls postInvalidate(). After calling the above two methods, the system will refresh our View at an appropriate time, that is, call back the onDraw method. With this understanding, we will assume that when we exclude other interfering factors, as long as we don't call the invite() series method, our custom View will only keep the way it was when it was last refreshed. But Android will always bring us surprises ( o ), and the next test will refresh your cognition.

The show begins

Let's first create two custom Views:

/** * Custom 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 } } Copy code
/** * Custom View uses double buffer refresh */ 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) } } Copy code

The biggest difference between the above two custom views is that one uses double buffer refresh and the other does not.

Next, create our 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() } } } Copy code

And 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_toBot= " tomOf" 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> Copy code

You can see that the current interface is to display the above two custom Views. What the [Change Color] button does is to change the color, and [Invalidate] calls the invalidate() method of the View.

Come and run to see the normal effect:

It can be seen that I have clicked the [Change Color] button several times, but the two custom views did not change accordingly. Only when I clicked the [Invalidate] button, the two views changed. The cognition is the same.

Then the highlight: only change the click style of the [Change Color] button:

... <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" //modify point android:text= "Change Color" android:gravity= "center" android:textColor= "@drawable/selector_text_color" //modify point 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"/> <!-- Fillet--> <corners android:radius= "10dp"/> <!-- Margin--> <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"/> <!-- Fillet--> <corners android:radius= "10dp"/> <!-- Margin--> <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> Copy code

Let's take a look at the running effect:

Careful students have discovered the difference. After I clicked the [Change Color] button, MyView did not change, but MyViewUseBitmap refreshed automatically. After I clicked the [Invalidate] button, MyView refreshed, MyView The performance is in line with our expectations. That is, MyViewUseBitmap refreshed itself when we did not actively call invalidate(). Like me, students who are daring to guess may think that the refresh of the click style of the button causes the refresh of the View Tree, which in turn drives the refresh of MyViewUseBitmap. But I can tell you that after clicking the [Change Color] button, through debugging and log viewing, the onDraw method of MyViewUseBitmap is not called. That is to say, our View refreshed when onDraw() was not called back.

In some scenarios, I need my View to refresh according to my refresh timing, so the above phenomenon will become uncontrollable. When I encountered this situation at the beginning, I also looked confused, and for the Nth time, I had the idea that I don't know how to develop Android ( o ). Fortunately, I finally found a solution, which made me dispel the idea of changing careers. That is:

//Turn on the hardware acceleration of View setLayerType(LAYER_TYPE_HARDWARE, null ) or //Turn on the software acceleration of View setLayerType(LAYER_TYPE_SOFTWARE, null ) You can use LAYER_TYPE_SOFTWARE for compatibility, and LAYER_TYPE_HARDWARE for performance. Copy code

At this point, the problem is perfectly solved, and then run again to see:

Happy, and can write Android happily again.

summary

In the case that the custom View does not use the double buffering mechanism, the refresh of the View is controllable; but once the double buffering mechanism is used, changes in other parts of the interface may drive our View to refresh. Although the reason has not been found so far, fortunately, a solution has been found, that is, you need to call View.setLayerType(). The incoming parameter can be LAYER_TYPE_HARDWARE or LAYER_TYPE_SOFTWARE, but it cannot be LAYER_TYPE_NONE.

LAYER_TYPE_NONE: It is the default behavior of View. View will render normally and does not support off-screen buffer LAYER_TYPE_HARDWARE: hardware acceleration, View will be rendered as hardware texture in hardware LAYER_TYPE_SOFTWARE: software acceleration, View will be rendered as a bitmap in software Copy code

Welcome to discuss what is wrong with the above.

Please indicate the source for reprinting https://juejin.cn/post/6951298126418444324