Android Custom View series

  • (1) To achieve the effect of rounded corner mask
  • Android custom View Demo (2) to achieve the circular avatar effect
  • Android custom View Demo (3) to achieve the micro channel shot one shot of the animation effect

Android custom View to achieve a circular avatar effect

In our APP, we often encounter the need to show the circular profile picture, usually through Glide can achieve, but let’s make a circular profile picture, if let us customize to achieve this effect, how to do?

Ok, this article will do this in three ways!

Note: This is a hands-on Demo

1. Knowledge points to be learned from this article

  • Use of canvas. ClipPath API
  • The use of Xfermode
  • Paint’s Xfermode and ShaderAPI
  • Matrix translation and Canvans translation (in the source code, in order to display all three effects simultaneously in one View, the Canvas coordinates are translated)
  • Summarize the advantages and disadvantages of the three implementations

2. Create a circular avatar through a custom View

There are three ways to do this, and what are the three ways?

  • Through the canvas. ClipPath ()
  • Through the paint.xfermode = porterDuffXfermode
  • Through the paint.shader = bitmapShader

3. The first implementation

Use the Canvas clip method

private val AVATAR_SIZE = 240.dp // The size of the picture
private val RADIUS = AVATAR_SIZE / 2 // Cut the radius of the circle

class CircleAvatarView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private val paint = Paint()
    private var avatar = getAvatar(R.drawable.my_avatar, AVATAR_SIZE.toInt())// The bitmap of the image
    private val circlePath = Path()// The circular path
  
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        /** * Draw a circle */ where the center of the circle is x: half of the width of the View y: half of the height of the View and the radius is half of the image size
        circlePath.addCircle(width / 2f, height / 2f, RADIUS, Path.Direction.CW)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
         // The KTX extension method will automatically save the recovery Canvas
        canvas.withSave {
          	// Reset the brush
            paint.reset()
          	// Use the Canvas to cut the range to be drawn, namely the circle
            canvas.clipPath(circlePath)
          	// Draw the declared Bitmap after the crop
            canvas.drawBitmap(avatar, width / 2 - RADIUS, height / 2 - RADIUS, paint)
        }
    }
   
}
Copy the code

Explain the code and, of course, look at the code comments as well

  • Define the width of the package-level image to 240dp and the radius of the circle
  • Gets a Bitmap of the image to display
  • Declares the Path of a circle to crop
  • Add a circle to the onSizeChanged method
  • Use the Canvas to cut the range to be drawn, that is, the circle
  • Draw the declared Bitmap after the crop

The above is the first method of drawing the circular head, mainly using the canvas.clippath (circlePath) method, pay attention to the canvas save and restore

4. Second way of implementation

Through the paint.xfermode = porterDuffXfermode

PorterDuff.Mode

private val AVATAR_SIZE = 240.dp // The size of the picture
private val RADIUS = AVATAR_SIZE / 2 // Cut the radius of the circle

class CircleAvatarView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private val paint = Paint()
    private var avatar = getAvatar(R.drawable.my_avatar, AVATAR_SIZE.toInt())// The bitmap of the image
		private val circlePath = Path()// The circular path
    private val bounds = RectF()// Off-screen buffered bounds
    private val porterDuffXfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
  
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
      
        /** * Draw a circle */ where the center of the circle is x: half of the width of the View y: half of the height of the View and the radius is half of the image size
        circlePath.addCircle(width / 2f, height / 2f, RADIUS, Path.Direction.CW)
      
        /** * Set the off-screen buffered bounds to be small enough to affect performance */
        bounds.set(width / 2f - RADIUS, height / 2f - RADIUS,
                width / 2f + RADIUS, height / 2f + RADIUS)

    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)   
        // Enable off-screen buffering
        val count = canvas.saveLayer(bounds, null)
        paint.reset()
        canvas.drawPath(circlePath, paint)
        // Set paint's xferMode to porterduff.mode.src_in
        paint.xfermode = porterDuffXfermode
      	// Draw the Bitmap in the current Paint
        canvas.drawBitmap(avatar, width / 2 - RADIUS, height / 2 - RADIUS, paint)
        // Clear paint's xfermode
        paint.xfermode = null
        // Draw the off-screen cached content to the View
        canvas.restoreToCount(count)
      
    }
}
Copy the code

Explain the code and, of course, look at the code comments as well

  • Set the extent of the off-screen buffer in the onSizeChanged method, which is the extent of the outer rectangle of the circular avatar
  • Turn on off-screen buffering in the onDraw method
  • Canvas first draws a circle (equivalent to Destination image in Porterduff.mode)
  • Set the xferMode of paint to porterduff.mode.src_in
  • Draw the Bitmap in the current Paint (note that this is the same as the Source image in porterduff.mode). Since we chose porterduff.mode.src_in, we draw the image we want
  • I’m going to draw the off-screen buffer to the View

Note: Be sure to turn on off-screen buffering, otherwise the result may not be what you expect. Off-screen buffering is equivalent to taking a transparent View and drawing the graph that the Canvas is drawing

5. Third way of implementation

Through the paint.shader = bitmapShader

private val AVATAR_SIZE = 240.dp // The size of the picture
private val RADIUS = AVATAR_SIZE / 2 // Cut the radius of the circle

class CircleAvatarView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
    private val paint = Paint()
    private var avatar = getAvatar(R.drawable.my_avatar, AVATAR_SIZE.toInt())// The bitmap of the image
    private val circlePath = Path()// The circular path
    private val bitmapShader = BitmapShader(avatar, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        /** * Draw a circle */ where the center of the circle is x: half of the width of the View y: half of the height of the View and the radius is half of the image size
        circlePath.addCircle(width / 2f, height / 2f, RADIUS, Path.Direction.CW)

    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        paint.reset()
        // Set Shader for paint
        paint.shader = bitmapShader
        // Draw the circle as a Shader
        canvas.drawPath(circlePath, paint)
    }
}
Copy the code

Explain the code and, of course, look at the code comments as well

This way, it’s easier

  • Declare a BitmapShader. Fill the bitmap of the avatar in the BitmapShader construct and fill in the value of the shader. TileMode parameter
  • Set the Shader for the paint in the onDraw method
  • Draw a circle in the form of a Shader and the result is a head with rounded corners

6. Summary

Summarize the advantages and disadvantages of the three approaches

1. Canvas. ClipPath () :

The canvas clip method has no anti-aliasing effect and will have burrs because it is precisely cut pixelsCopy the code

2.paint.xfermode = porterDuffXfermode

The xfermode method using Paint has anti-aliasing effect, no burrs, good effect, anti-aliasing treatment, fill and similar translucent color around etcCopy the code

3.paint.shader = bitmapShader

Shader with paint has anti-aliasing effect, no burrs, good effect but the image results are affected by shader. TileMode, which may not be what you expectCopy the code

To sum up: it’s better to use paint. Xfermode = porterDuffXfermode, because what we need is a perfectly displayed avatar

Also be careful to use off-screen buffering

7. Source address

Note: my source code is to draw the three ways in a View, and through the Matrix translation or Canvans translation to achieve the effect of the downward arrangement, the way to practice the Matrix translation and Canvans translation

CircleAvatarView.kt

8. The original address

Android custom View to achieve a circular avatar effect

9. Refer to articles

hencoder

PorterDuff.Mode

I recommend my open source project WanAndroid client

WanAndroidJetpack architecture diagram

  • A pure Android learning project, WanAndroid client.
  • Project useMVVMArchitecture,KotlinVoice writing.
  • Extensive use of Android Jetpack includes, but is not limited toLifecycle,LiveData,ViewModel,Databinding,Room,ConstraintLayoutAnd more in the future.
  • usingRetrofitKotlin-Coroutine coroutinesNetwork interaction.
  • Loading picturesGlideMainstream load picture frame.
  • Data storage is mainly usedRoomAnd tencentMMKV.

Kotlin + MVVM + Jetpack + Retrofit + Glide is a good project to learn MMVM architecture.

This project itself is also a special learning Android related knowledge of the APP, welcome to download the experience!

Source code address (with download link)

WanAndroidJetpack

Overview of the APP

Order if you likeStarsIf you have questions, please refer to the following questions: