【CustomView】扫码中的扫描框(ViewfinderView)-简单实现

在记录【CustomView】Android 自定义绘制 这篇文章时,发现此篇代码存在需优化的地方:(以下代码已优化)

我们在自定义绘制View的时候,需要注意以下三点:

⚠️ 提前创建对象是一项重要的优化措施。视图会非常频繁地重新绘制,并且许多绘制对象的初始化都需要占用很多资源。

⚠️ 请在 onSizeChanged() 中计算位置、尺寸以及其他与视图大小相关的任何值,而不要在每次绘制(onDraw)时都重新计算。

⚠️ 在 onDraw() 方法内创建绘制对象会显著降低性能并使界面显得卡顿。

 

项目中有的扫码功能中需要覆盖一层扫码框,如下所示:

20201016214724300

#### 自定义View,绘制4个角且需要圆角

* 这里绘制4个角是用画线条的方式实现,用strokeCap = Paint.Cap.ROUND来实现圆角。

private val paint by lazy {
        Paint().apply {
            strokeCap = Paint.Cap.ROUND
            isAntiAlias = true
        }
    }

canvas.drawLine(
            frame.left,
            frame.top,
            (frame.left + innerCornerLengthHorizontal),
            (frame.top),
            paint
        )

1.CustomView >>> ViewfinderView

/**
 * viewfinder window
 */
class ViewfinderView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = -1
) : View(context, attrs, defStyleAttr) {
    // viewfinder inner Width
    private var innerWidth = 0F

    // viewfinder inner Height
    private var innerHeight = 0F

    // view inner horizontal length
    private var innerCornerLengthHorizontal = 0F

    // view inner vertical length
    private var innerCornerLengthVertical = 0F

    // view inner corner width
    private var innerCornerWidth = 0F

    private var viewWidth: Float = 0F
    private var viewHeight: Float = 0F

    // view inner color
    private var innerCornerColor = 0

    private val viewfinderFrame: RectF = RectF(0f, 0f, 0f, 0f)

    private val paint by lazy {
        Paint().apply {
            strokeCap = Paint.Cap.ROUND
            isAntiAlias = true
            color = resources.getColor(R.color.viewfinder_mask)
        }
    }

    private val paintCorner by lazy {
        Paint().apply {
            strokeCap = Paint.Cap.ROUND
            isAntiAlias = true
            style = Paint.Style.FILL
            strokeWidth = innerCornerWidth
        }
    }

    init {
        initInnerRect(context, attrs)
    }

    /**
     * Initialize the size of the inner frame
     *
     * @param context
     * @param attrs
     */
    private fun initInnerRect(
        context: Context,
        attrs: AttributeSet?
    ) {
        val styledAttributes = context.obtainStyledAttributes(attrs, R.styleable.ViewfinderView)
        innerWidth = styledAttributes.getDimension(
            R.styleable.ViewfinderView_inner_width,
            resources.getDimension(R.dimen.default_inner_width)
        )
        innerHeight = styledAttributes.getDimension(
            R.styleable.ViewfinderView_inner_height,
            resources.getDimension(R.dimen.default_inner_height)
        )
        innerCornerColor = styledAttributes.getColor(
            R.styleable.ViewfinderView_inner_corner_color,
            resources.getColor(R.color.default_inner_color)
        )
        innerCornerLengthHorizontal = styledAttributes.getDimension(
            R.styleable.ViewfinderView_inner_corner_length_horizontal,
            resources.getDimension(R.dimen.default_inner_corner_length_horizontal)
        )
        innerCornerLengthVertical = styledAttributes.getDimension(
            R.styleable.ViewfinderView_inner_corner_length_vertical,
            resources.getDimension(R.dimen.default_inner_corner_length_vertical)
        )
        innerCornerWidth = styledAttributes.getDimension(
            R.styleable.ViewfinderView_inner_corner_width,
            resources.getDimension(R.dimen.default_inner_corner_width)
        )

        paintCorner.color = innerCornerColor
        styledAttributes.recycle()
    }

    // 视图不需要对其大小进行特殊控制,您只需替换一个方法,即 onSizeChanged()
    override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
        super.onSizeChanged(width, height, oldWidth, oldHeight)
        Log.d(
            "ViewfinderView",
            "onSizeChanged - viewfinderFrame w: ${width}, h: ${height}, oldw: ${oldWidth}, oldh: ${oldHeight}, measuredWidth: $measuredWidth , measuredHeight: $measuredHeight"
        )
        viewWidth = width.toFloat()
        viewHeight = height.toFloat()

        val xMid = (viewWidth / 2).toInt()
        val yMid = (viewHeight / 2).toInt()
        val leftOffset = xMid - (innerWidth / 2).toInt()
        val topOffset = yMid - (innerHeight / 2).toInt()
        viewfinderFrame.set(
            leftOffset.toFloat(),
            topOffset.toFloat(),
            (leftOffset + innerWidth),
            (topOffset + innerHeight)
        )
    }

    // 如果您需要更精细地控制视图的布局参数,请实现 onMeasure()。
    // 此方法的参数是 View.MeasureSpec 值,用于告诉您视图的父视图希望您的视图有多大,以及该大小是硬性最大值还是只是建议值。
    // 作为优化措施,这些值以打包整数形式存储,您可以使用 View.MeasureSpec 的静态方法解压缩每个整数中存储的信息。
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        Log.d(
            "ViewfinderView",
            "onMeasure - measuredWidth: $measuredWidth, measuredHeight: $measuredHeight"
        )
    }

    // 提前创建对象是一项重要的优化措施。视图会非常频繁地重新绘制,并且许多绘制对象的初始化都需要占用很多资源。
    // 在 onDraw() 方法内创建绘制对象会显著降低性能并使界面显得卡顿。
    public override fun onDraw(canvas: Canvas) {
        Log.d(
            "ViewfinderView",
            "onDraw - viewWidth: $viewWidth, viewHeight: $viewHeight , viewfinderFrame top: ${viewfinderFrame.top}, left: ${viewfinderFrame.left}, right: ${viewfinderFrame.right}, bottom: ${viewfinderFrame.bottom},"
        )
        // Draw the exterior (i.e. outside the framing rect) darkened
        canvas.drawRect(0f, 0f, viewWidth, viewfinderFrame.top, paint)
        canvas.drawRect(
            0f,
            viewfinderFrame.top,
            viewfinderFrame.left,
            viewfinderFrame.bottom,
            paint
        )
        canvas.drawRect(
            viewfinderFrame.right,
            viewfinderFrame.top,
            viewWidth,
            viewfinderFrame.bottom,
            paint
        )
        canvas.drawRect(
            0f,
            viewfinderFrame.bottom,
            viewWidth,
            viewHeight,
            paint
        )
        drawFrameBounds(canvas, viewfinderFrame)
    }

    /**
     * Draw frame border
     *
     * @param canvas
     * @param frame
     */
    private fun drawFrameBounds(
        canvas: Canvas,
        frame: RectF
    ) {

        // Upper left corner
        canvas.drawLine(
            frame.left,
            frame.top,
            (frame.left),
            (frame.top + innerCornerLengthVertical),
            paintCorner
        )
        canvas.drawLine(
            frame.left,
            frame.top,
            (frame.left + innerCornerLengthHorizontal),
            (frame.top),
            paintCorner
        )

        // Upper right corner
        canvas.drawLine(
            (frame.right),
            frame.top,
            frame.right,
            (frame.top + innerCornerLengthVertical),
            paintCorner
        )
        canvas.drawLine(
            frame.right - innerCornerLengthHorizontal,
            frame.top,
            frame.right,
            frame.top,
            paintCorner
        )
        // Lower left corner
        canvas.drawLine(
            frame.left,
            (frame.bottom - innerCornerLengthVertical),
            (frame.left),
            frame.bottom,
            paintCorner
        )
        canvas.drawLine(
            frame.left,
            (frame.bottom),
            (frame.left + innerCornerLengthHorizontal),
            frame.bottom,
            paintCorner
        )

        // Lower right corner
        canvas.drawLine(
            (frame.right),
            (frame.bottom - innerCornerLengthVertical),
            frame.right,
            frame.bottom,
            paintCorner
        )
        canvas.drawLine(
            (frame.right - innerCornerLengthHorizontal),
            (frame.bottom),
            frame.right,
            frame.bottom,
            paintCorner
        )
    }
}

源码上有段代码比较奇怪:

        val xMid = (viewWidth / 2).toInt()
        val yMid = (viewHeight / 2).toInt()
        val leftOffset = xMid - (innerWidth / 2).toInt()
        val topOffset = yMid - (innerHeight / 2).toInt()
        viewfinderFrame.set(
            leftOffset.toFloat(),
            topOffset.toFloat(),
            (leftOffset + innerWidth),
            (topOffset + innerHeight)
        )

⚠️ 为什么在原本就是Float的情况下要先转Int,再转Float,我想在代码审核的审核,谁也不允许多做这一步!

具体分析详见:Android 自定义View出现的绘制背景区域断开的情况

 

2. styleable添加xml属性

<resources>
    <color name="viewfinder_mask">#80ffffff</color>
    <color name="default_inner_color">#4a008a</color>

    <declare-styleable name="ViewfinderView">
        <attr name="inner_width" format="dimension" />
        <attr name="inner_height" format="dimension" />
        <attr name="inner_corner_color" format="color" />
        <attr name="inner_corner_length_horizontal" format="dimension" />
        <attr name="inner_corner_length_vertical" format="dimension" />
        <attr name="inner_corner_width" format="dimension" />
    </declare-styleable>

    <dimen name="default_inner_width">409dp</dimen>
    <dimen name="default_inner_height">250dp</dimen>
    <dimen name="default_inner_corner_length_horizontal">34dp</dimen>
    <dimen name="default_inner_corner_length_vertical">23dp</dimen>
    <dimen name="default_inner_corner_width">4dp</dimen>
</resources>

 

使用示例:

<com.android.demo.view.ViewfinderView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:inner_corner_color="@color/colorAccent"
            app:inner_corner_length_horizontal="31dp"
            app:inner_corner_length_vertical="21dp"
            app:inner_corner_width="4dp"
            app:inner_height="71dp"
            app:inner_width="123dp" />

 效果:

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android可以使用GridLayout或者自定义布局实现九宫格布局。 1. 使用GridLayout GridLayout是Android提供的布局,可以实现网格布局。以下是一个九宫格布局的例子: ``` <GridLayout android:layout_width="match_parent" android:layout_height="match_parent" android:rowCount="3" android:columnCount="3" android:orientation="horizontal"> <Button android:text="Button 1" android:layout_row="0" android:layout_column="0"/> <Button android:text="Button 2" android:layout_row="0" android:layout_column="1"/> <Button android:text="Button 3" android:layout_row="0" android:layout_column="2"/> <Button android:text="Button 4" android:layout_row="1" android:layout_column="0"/> <Button android:text="Button 5" android:layout_row="1" android:layout_column="1"/> <Button android:text="Button 6" android:layout_row="1" android:layout_column="2"/> <Button android:text="Button 7" android:layout_row="2" android:layout_column="0"/> <Button android:text="Button 8" android:layout_row="2" android:layout_column="1"/> <Button android:text="Button 9" android:layout_row="2" android:layout_column="2"/> </GridLayout> ``` 2. 自定义布局 也可以使用自定义布局实现九宫格布局。以下是一个自定义九宫格布局的例子: ``` public class NineGridLayout extends LinearLayout { private static final int DEFAULT_COLUMN_COUNT = 3; private int mColumnCount; private List<View> mViewList; public NineGridLayout(Context context) { super(context); init(); } public NineGridLayout(Context context, AttributeSet attrs) { super(context, attrs); init(); } public NineGridLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { setOrientation(VERTICAL); mViewList = new ArrayList<>(); mColumnCount = DEFAULT_COLUMN_COUNT; } public void setColumnCount(int columnCount) { mColumnCount = columnCount; } public void setViews(List<View> viewList) { mViewList.clear(); mViewList.addAll(viewList); notifyDataSetChanged(); } public void notifyDataSetChanged() { removeAllViews(); int rowCount = (int) Math.ceil(mViewList.size() * 1.0 / mColumnCount); for (int i = 0; i < rowCount; i++) { LinearLayout rowLayout = new LinearLayout(getContext()); rowLayout.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); rowLayout.setOrientation(HORIZONTAL); for (int j = 0; j < mColumnCount; j++) { int index = i * mColumnCount + j; if (index < mViewList.size()) { View view = mViewList.get(index); view.setLayoutParams(new LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); rowLayout.addView(view); } else { View emptyView = new View(getContext()); emptyView.setLayoutParams(new LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); rowLayout.addView(emptyView); } } addView(rowLayout); } } } ``` 使用时可以在XML定义布局: ``` <com.example.customview.NineGridLayout android:id="@+id/nine_grid_layout" android:layout_width="match_parent" android:layout_height="wrap_content" app:columnCount="3"/> ``` 然后在代码设置View: ``` List<View> viewList = new ArrayList<>(); for (int i = 0; i < 9; i++) { Button button = new Button(this); button.setText("Button " + (i + 1)); viewList.add(button); } NineGridLayout nineGridLayout = findViewById(R.id.nine_grid_layout); nineGridLayout.setViews(viewList); ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值