Android自定义控件入门 03 ViewGroup与View的onDraw()异同

77 阅读2分钟

ViewGroup与View的onDraw()异同

原本extends View的自定义控件改为extends ViewGroup后不正常显示,设置background后正常

原因:默认的ViewGroup不会调用onDraw()

画的其实是public void draw(@NonNull Canvas canvas) {},模板设计模式

if (!verticalEdges && !horizontalEdges) {  
    // Step 3, draw the content  
    onDraw(canvas);  
}
// Step 4, draw the children  
dispatchDraw(canvas);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);

mPrivateFlags到底怎么赋值的 ,在View的构造函数中调用computeOpaqueFlags()

/**  
* @hide  
*/  
@UnsupportedAppUsage  
protected void computeOpaqueFlags() {  
    // Opaque if:  
    // - Has a background  
    // - Background is opaque  
    // - Doesn't have scrollbars or scrollbars overlay  
  
    if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) {  
        mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;  
    } else {  
        mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;  
    }  
  
    final int flags = mViewFlags;  
    if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) ||  
        (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY ||  
        (flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) {  
        mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS;  
    } else {  
        mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS;  
    }  
}

ViewGroup为什么出不来-->initViewGroup();

image.png

private void initViewGroup() {  
    // ViewGroup doesn't draw by default  
    if (!isShowingLayoutBounds()) {  
        setFlags(WILL_NOT_DRAW, DRAW_MASK);  
    }  
}

导致mPrivateFlags会重新赋值

问题解决:思路:就是改变mPrivateFlags的状态

  • onDraw(canvas: Canvas)改为dispatchDraw(canvas: Canvas)
  • 设置透明背景
  • setWillNotDraw(false)

完整的自定义TextView代码如下 attrs.xml

<!-- 名字最好是自定义控件 的名字-->  
<declare-styleable name="TextView">  
    <!--  
    name属性名称  
    format格式  
        string 文字  
        color 颜色  
        dimension 宽高,字体大小  
        integer数字  
        reference资源(drawable)  
    -->  
    <attr name="darrenText" format="string" />  
    <attr name="darrenTextColor" format="color" />  
    <attr name="darrenTextSize" format="dimension" />  
    <attr name="darrenMaxLength" format="integer" />  
    <!-- 枚举-->  
    <attr name="darrenInputType">  
        <enum name="number" value="1" />  
        <enum name="text" value="2" />  
        <enum name="password" value="3" />  
    </attr>  
</declare-styleable>

TextView.kt

class TextView @JvmOverloads constructor(  
    context: Context,  
    attrs: AttributeSet? = null,  
    defStyleAttr: Int = 0  
) : View(context, attrs, defStyleAttr) {  
    private val text: String  
    private val textSize: Int  
    private val textColor: Int  
    private val paint: Paint  
    private val bounds by lazy { Rect() }  
  
    init {  
    //获取自定义属性  
    val array = context.obtainStyledAttributes(attrs, R.styleable.TextView)  
    text = array.getString(R.styleable.TextView_darrenText).toString()  
    textColor = array.getColor(R.styleable.TextView_darrenTextColor, Color.BLACK)  
    textSize =  array.getDimensionPixelSize(R.styleable.TextView_darrenTextSize, context.sp2px(15))  
    //回收  
    array.recycle()  
    paint = with(Paint()) {  
            //抗锯齿  
            isAntiAlias = true  
            //设置字体大小,颜色  
            textSize = this@TextView.textSize.toFloat()  
            color = textColor  
            this  
        }  
    }  
  
  
    /**  
    * 自定义view的测量方法  
    * */  
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {  
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)  
        //布局的宽高都是由这个方法指定  
        //指定控件的宽高,需要测量  
        //获取宽高的模式  
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)  
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)  


        val widthSize = measureWidth(widthMeasureSpec, heightMeasureSpec, true)  
        val heightSize = measureWidth(widthMeasureSpec, heightMeasureSpec)  
        setMeasuredDimension(widthSize, heightSize)  
    }  
  
    /**  
    * @param isWidth 是否计算宽度  
    * */  
    private fun measureWidth(  
        widthMeasureSpec: Int,  
        heightMeasureSpec: Int,  
        isWidth: Boolean = false  
    ): Int {  
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)  
        return if (widthMode == MeasureSpec.AT_MOST) {  
            //2.给的是 wrap_content 需要计算  
            //计算的宽度与字体的长度,大小有关,用画笔测量  
            paint.getTextBounds(text, 0, text.length, bounds)  
            if (isWidth) {  
                bounds.width() + paddingLeft + paddingRight  
            } else {  
                bounds.height() + paddingTop + paddingBottom  
            }  
        } else {  
            //1.确定的值,这个时候不需要计算,给的多少就是多少  
            val widthSize = MeasureSpec.getSize(widthMeasureSpec)  
            val heightSize = MeasureSpec.getSize(heightMeasureSpec)  
            if (isWidth) {  
                widthSize  
            } else {  
                heightSize  
            }  
        }  
    }  
  
    override fun dispatchDraw(canvas: Canvas) {  
        super.dispatchDraw(canvas)  
        //x 开始的位置  
        //y 基线 baseLine  
        //dy 代表的是高度的一半到baseLine的距离  
        val dy = with(paint.fontMetricsInt) {  
            (bottom - top) / 2 - bottom  
        }  
        val baseLine = (height / 2).toFloat() + dy  
        val x = paddingLeft  
        canvas.drawText(text, x.toFloat(), baseLine, paint)  
    }  
}