Android - 开源自定义View仿微信设置条目

<异空间>项目技术分享系列——自定义View仿微信设置选项条目java

关于设置选项条目,在大部分App内仍是挺经常使用的,UI效果有左图标文字,右文字箭头、开关等等android

以微信设置页面的各类条目为例子:git

961614650301_.pic_hd 951614650271_.pic_hd

最简单的方案:

XML布局里面,每一行的条目,都使用一个线性布局/相对布局,包裹住全部的控件后,就能够对控件大小/位置进行调整。github

缺点:同一页面大量编写这样相似的布局会致使开发者感受空虚烦躁无聊,还要对大量的这些布局的控件设置id设置事件监听很麻烦等等canvas

为何想要封装一个这样的View?

在作项目的过程当中发现常常地要写各类各样的点击选项的条目,常见的"设置页"的条目,通常的作法是每写一个条目选项就要写一个布局而后里面配置一堆的View,虽然也能完成效果,可是若是数量不少或者设计图效果各异就会容易出错浪费不少时间,同时一个页面若是有过多的布局嵌套也会影响效率。api

因而,我开始找一些定制性高且内部经过纯Canvas就能完成全部绘制的框架。最后,我找到了由GitLqr做者开发的LQROptionItemView,大致知足需求,在此很是感谢做者GitLqr,可是在使用过程当中发现几个小问题:微信

  • 图片均不能设置宽度和高度
  • 图片不支持直接设置Vector矢量资源
  • 不支持顶部/底部绘制分割线
  • 左 中 右 区域识别有偏差
  • 不支持右侧View为Switch这种常见状况

因为原做者的项目近几年好像都没有继续维护了,因而我打算本身动手改进以上的问题,并开源OptionBarViewapp

  • 绘制左、中、右侧的文字
  • 绘制左、右侧的图片
  • 定制右侧的Switch(IOS风格)
  • 设置顶部或底部的分割线
  • 定制View与文字的大小和距离
  • 识别左中右分区域的点击

效果演示

下图列举了几种常见的条目效果,项目还支持更多不一样的效果搭配。框架

img

Gradle集成方式

在Project 的 build.gradlemaven

allprojects {
    repositories {
		...
        maven { url 'https://jitpack.io' }
    }
}

在Module 的 build.gradle

dependencies {
	    implementation 'com.github.DMingOu:OptionBarView:1.1.0'
	}

快速上手

一、在XML布局中使用

属性都可选,不设置的属性则不显示,⭐图片与文字的距离若不设置会有一个默认的距离,可设置任意类型的图片资源。

<com.dmingo.optionbarview.OptionBarView
	 android:id="@+id/opv_1"
	 android:layout_width="match_parent"
	 android:layout_height="60dp"
	 android:layout_marginTop="30dp"
	 android:background="@android:color/white"
	 app:left_image_margin_left="20dp"
	 app:left_src="@mipmap/ic_launcher"
	 app:left_src_height="24dp"
	 app:left_src_width="24dp"
	 app:left_text="左标题1"
	 app:left_text_margin_left="5dp"
	 app:left_text_size="16sp"
	 app:title="中间标题1"
	 app:title_size="20sp"
	 app:title_color="@android:color/holo_red_light"
	 app:rightViewType="Image"
	 app:right_view_margin_right="20dp"
	 app:right_src="@mipmap/ic_launcher"
	 app:right_src_height="20dp"
	 app:right_src_width="20dp"
	 app:right_text="右方标题1"
	 app:right_text_size="16sp"
	 app:show_divide_line="true"
	 app:divide_line_color="@android:color/black"
	 app:divide_line_left_margin="20dp"
	 app:divide_line_right_margin="20dp"/>

或者右侧为一个Switch:

<com.dmingo.optionbarview.OptionBarView
	   android:id="@+id/opv_switch2"
	   android:layout_width="match_parent"
	   android:layout_height="60dp"
	   android:layout_marginTop="30dp"
	   android:background="@android:color/white"
	   app:right_text="switch"
	   app:right_view_margin_right="10dp"
	   app:right_view_margin_left="0dp"
	   app:rightViewType="Switch"
	   app:switch_background_width="50dp"
	   app:switch_checkline_width="20dp"
	   app:switch_uncheck_color="@android:color/holo_blue_bright"
	   app:switch_uncheckbutton_color="@android:color/holo_purple"
	   app:switch_checkedbutton_color="@android:color/holo_green_dark"
	   app:switch_checked_color="@android:color/holo_green_light"
	   app:switch_button_color="@android:color/white"
	   app:switch_checked="true"				  
	   />

二、在Java代码里动态添加

方式与其余View相同,也是肯定布局参数,经过api设置OptionBarView的属性,这里就不阐述了

三、条目点击事件

总体点击模式

默认开启的是总体点击模式,能够经过setSplitMode(false)手动开启

opv2.setOnClickListener(new View.OnClickListener() {
  @Override
   public void onClick(View view) {
       Toast.makeText(MainActivity.this,"OptionBarView Click",Toast.LENGTH_LONG).show();
   }
});

分区域点击模式

默认不会开启分区域点击模式,能够经过setSplitMode(true)开启,经过设置接口回调进行监听事件

opv1.setSplitMode(true);
opv1.setOnOptionItemClickListener(new OptionBarView.OnOptionItemClickListener() {
   @Override
    public void leftOnClick() {
        Toast.makeText(MainActivity.this,"Left Click",Toast.LENGTH_SHORT).show();
    }
   @Override
   public void centerOnClick() {
        Toast.makeText(MainActivity.this,"Center Click",Toast.LENGTH_SHORT).show();
   }
   @Override
   public void rightOnClick() {
        Toast.makeText(MainActivity.this,"Right Click",Toast.LENGTH_SHORT).show();
   }
 });

分区域点击模式下对Switch进行状态改变监听

opvSwitch = findViewById(R.id.opv_switch);
        opvSwitch.setSplitMode(true);
        opvSwitch.setOnSwitchCheckedChangeListener(new OptionBarView.OnSwitchCheckedChangeListener() {
            @Override
            public void onCheckedChanged(OptionBarView view, boolean isChecked) {
                Toast.makeText(MainActivity.this,"Switch是否被打开:"+isChecked,Toast.LENGTH_SHORT).show();
            }
        });

设置条目的背景触摸变色

也是很简单,只要在XML中给条目设置background属性就能够了

android:background="@drawable/sel_bg_press_white_gray"

参考:sel_bg_press_white_gray.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
	
    <item android:state_pressed="true"
        android:drawable="@color/optionbar_pressed_background">
    </item>

    <item
        android:state_pressed="false"
        android:drawable="@android:color/white"/>
</selector>

四、API

//中间标题
getTitleText()
setTitleText(String text)
setTitleText(int stringId)
setTitleColor(int color)
setTitleSize(int sp)

//左侧
getLeftText()
setLeftText(String text)
setLeftText(int stringId)
setLeftTextSize(int sp)
setLeftTextColor(int color)
setLeftTextMarginLeft(int dp)
setLeftImageMarginLeft(int dp)
setLeftImageMarginRight(int dp)
setLeftImage(Bitmap bitmap)
showLeftImg(boolean flag)
showLeftText(boolean flag)
setLeftImageWidthHeight(int width, int Height)

//右侧
getRightText()
setRightImage(Bitmap bitmap)
setRightText(String text)
setRightText(int stringId)
setRightTextColor(int color)
setRightTextSize(int sp)
setRightTextMarginRight(int dp)
setRightViewMarginLeft(int dp)
setRightViewMarginRight(int dp)
showRightImg(boolean flag)
showRightText(boolean flag)
setRightViewWidthHeight(int width, int height)
getRightViewType()
showRightView(boolean flag)
setChecked(boolean checked)
isChecked()
toggle(boolean animate)


//点击模式
setSplitMode(boolean splitMode)
getSplitMode()

//分割线
getIsShowDivideLine()
setShowDivideLine(Boolean showDivideLine)
setDivideLineColor(int divideLineColor)

五、特殊属性说明

主要是对一些图片文字的距离属性的说明。看图就能明白了。

属性更新说明:

right_image_margin_left 更新为 right_view_margin_left

right_image_margin_right 更新为 right_view_margin_right

img

混淆

-dontwarn com.dmingo.optionbarview.*
-keep class com.dmingo.optionbarview.*{*;}

关于具体实现

为了能在XML更加方便地使用一定少不了自定义属性

attrs.xml

<declare-styleable name="OptionBarView">
        <attr name="title" format="string"/>
        <attr name="title_size" format="dimension"/>
        <attr name="title_color" format="color"/>
        <attr name="left_src" format="reference|color"/>
        <attr name="left_text" format="string"/>
        <attr name="left_text_size" format="dimension"/>
        <attr name="left_src_width" format="dimension"/>
        <attr name="left_src_height" format="dimension"/>
        <attr name="left_image_margin_left" format="dimension"/>
        <attr name="left_text_margin_left" format="dimension"/>
        <attr name="left_image_margin_right" format="dimension"/>
        <attr name="left_text_color" format="color"/>
        <attr name="right_src" format="reference|color"/>
        <attr name="right_text" format="string"/>
        <attr name="right_text_size" format="dimension"/>
        <attr name="right_src_width" format="dimension"/>
        <attr name="right_src_height" format="dimension"/>
        <attr name="right_image_margin_left" format="dimension"/>
        <attr name="right_image_margin_right" format="dimension"/>
        <attr name="right_text_margin_right" format="dimension"/>
        <attr name="right_text_color" format="color"/>
        <attr name="split_mode" format="boolean"/>
        <attr name="show_divide_line" format="boolean"/>
        <attr name="divide_line_top_gravity" format="boolean"/>
        <attr name="divide_line_left_margin" format="dimension"/>
        <attr name="divide_line_right_margin" format="dimension"/>
        <attr name="divide_line_height" format="dimension"/>
        <attr name="divide_line_color" format="color"/>
    </declare-styleable>

继承View类,在构造函数中进行属性的初始化,减小在onDraw中建立对象,而且除了普通图片资源,还可使用Vector资源加载Bitmap,内置了默认的边距,但会优先使用本身所设置的属性值:

具体绘制部分(onDraw)

按照绘制背景 - 绘制左区域 - 绘制右区域

在绘制左/右区域的控件时根据传入的属性选择性的绘制

代码及详细注释以下:

@Override
protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mWidth = getWidth();
        mHeight = getHeight();
        leftBound = 0;
        rightBound = Integer.MAX_VALUE;

        //抗锯齿处理
        canvas.setDrawFilter(paintFlagsDrawFilter);

        optionRect.left = getPaddingLeft();
        optionRect.right = mWidth - getPaddingRight();
        optionRect.top = getPaddingTop();
        optionRect.bottom = mHeight - getPaddingBottom();
        //抗锯齿
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(titleTextSize > leftTextSize ? Math.max(titleTextSize, rightTextSize) : Math.max(leftTextSize, rightTextSize));
//        mPaint.setTextSize(titleTextSize);
        mPaint.setStyle(Paint.Style.FILL);
        //文字水平居中
        mPaint.setTextAlign(Paint.Align.CENTER);

        //计算垂直居中baseline
        Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
        int baseLine = (int) ((optionRect.bottom + optionRect.top - fontMetrics.bottom - fontMetrics.top) / 2);

        float distance=(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
        float baseline = optionRect.centerY()+distance;

        if (!title.trim().equals("")) {
            // 正常状况,将字体居中
            mPaint.setColor(titleTextColor);
            canvas.drawText(title, optionRect.centerX(), baseline, mPaint);
            optionRect.bottom -= mTextBound.height();
        }


        if (leftImage != null && isShowLeftImg) {
            // 计算左图范围
            optionRect.left = leftImageMarginLeft >= 0 ? leftImageMarginLeft : mWidth / 32;
            //计算 左右边界坐标值,如有设置左图偏移则使用,不然使用View的宽度/32
            if(leftImageWidth >= 0){
                optionRect.right = optionRect.left + leftImageWidth;
            }else {
                optionRect.right = optionRect.right + mHeight / 2;
            }
            //计算左图 上下边界的坐标值,若无设置右图高度,默认为高度的 1/2
            if(leftImageHeight >= 0){
                optionRect.top = ( mHeight - leftImageHeight) / 2;
                optionRect.bottom = leftImageHeight + optionRect.top;
            }else {
                optionRect.top = mHeight / 4;
                optionRect.bottom = mHeight * 3 / 4;
            }
            canvas.drawBitmap(leftImage, null, optionRect, mPaint);

            //有左侧图片,更新左区域的边界
            leftBound =  Math.max(leftBound ,optionRect.right);
        }
        if (rightImage != null && isShowRightView && rightViewType == RightViewType.IMAGE) {
            // 计算右图范围
            //计算 左右边界坐标值,如有设置右图偏移则使用,不然使用View的宽度/32
            optionRect.right = mWidth - (rightViewMarginRight >= 0 ? rightViewMarginRight : mWidth / 32);
            if(rightImageWidth >= 0){
                optionRect.left = optionRect.right - rightImageWidth;
            }else {
                optionRect.left = optionRect.right - mHeight / 2;
            }
            //计算右图 上下边界的坐标值,若无设置右图高度,默认为高度的 1/2
            if(rightImageHeight >= 0){
                optionRect.top = ( mHeight - rightImageHeight) / 2;
                optionRect.bottom = rightImageHeight + optionRect.top;
            }else {
                optionRect.top = mHeight / 4;
                optionRect.bottom = mHeight * 3 / 4;
            }
            canvas.drawBitmap(rightImage, null, optionRect, mPaint);

            //右侧图片,更新右区域边界
            rightBound = Math.min(rightBound , optionRect.left);
        }
        if (leftText != null && !leftText.equals("") && isShowLeftText) {
            mPaint.setTextSize(leftTextSize);
            mPaint.setColor(leftTextColor);
            int w = 0;
            if (leftImage != null) {
                w += leftImageMarginLeft >= 0 ? leftImageMarginLeft : (mHeight / 8);//增长左图左间距
                w += mHeight / 2;//图宽
                w += leftImageMarginRight >= 0 ? leftImageMarginRight : (mWidth / 32);// 增长左图右间距
                w += Math.max(leftTextMarginLeft, 0);//增长左字左间距
            } else {
                w += leftTextMarginLeft >= 0 ? leftTextMarginLeft : (mWidth / 32);//增长左字左间距
            }

            mPaint.setTextAlign(Paint.Align.LEFT);
            // 计算了描绘字体须要的范围
            mPaint.getTextBounds(leftText, 0, leftText.length(), mTextBound);

            canvas.drawText(leftText, w, baseline, mPaint);
            //有左侧文字,更新左区域的边界
            leftBound = Math.max(w + mTextBound.width() , leftBound);
        }
        if (rightText != null && !rightText.equals("") && isShowRightText) {
            mPaint.setTextSize(rightTextSize);
            mPaint.setColor(rightTextColor);

            int w = mWidth;
            //文字右侧有View
            if (rightViewType != -1) {
                w -= rightViewMarginRight >= 0 ? rightViewMarginRight : (mHeight / 8);//增长右图右间距
                w -= rightViewMarginLeft >= 0 ? rightViewMarginLeft : (mWidth / 32);//增长右图左间距
                w -= Math.max(rightTextMarginRight, 0);//增长右字右间距
                //扣去右侧View的宽度
                if(rightViewType == RightViewType.IMAGE){
                    w -= (optionRect.right - optionRect.left);
                }else if(rightViewType == RightViewType.SWITCH){
                    w -= (switchBackgroundRight - switchBackgroundLeft + viewRadius * .5f);
                }
            } else {
                w -= rightTextMarginRight >= 0 ? rightTextMarginRight : (mWidth / 32);//增长右字右间距
            }

            // 计算了描绘字体须要的范围
            mPaint.getTextBounds(rightText, 0, rightText.length(), mTextBound);
            canvas.drawText(rightText, w - mTextBound.width(), baseline, mPaint);

            //有右侧文字,更新右边区域边界
            rightBound = Math.min(rightBound , w - mTextBound.width());
        }

        //处理分隔线部分
        if(isShowDivideLine){
            int left = divide_line_left_margin;
            int right = mWidth - divide_line_right_margin;
            //绘制分割线时,高度默认为 1px
            if(divide_line_height <= 0){
                divide_line_height = 1;
            }
            if(divide_line_top_gravity){
                int top = 0;
                int bottom = divide_line_height;
                canvas.drawRect(left, top, right, bottom, dividePaint);
            }else {
                int top = mHeight - divide_line_height;
                int bottom = mHeight;
                canvas.drawRect(left, top, right, bottom, dividePaint);
            }
        }

        //判断绘制 Switch
        if(rightViewType == RightViewType.SWITCH && isShowRightView){
            //边框宽度
            switchBackgroundPaint.setStrokeWidth(switchBorderWidth);
            switchBackgroundPaint.setStyle(Paint.Style.FILL);

            //绘制关闭状态的背景
            switchBackgroundPaint.setColor(uncheckSwitchBackground);
            drawRoundRect(canvas,
                    switchBackgroundLeft, switchBackgroundTop, switchBackgroundRight, switchBackgroundBottom,
                    viewRadius, switchBackgroundPaint);
            //绘制关闭状态的边框
            switchBackgroundPaint.setStyle(Paint.Style.STROKE);
            switchBackgroundPaint.setColor(uncheckColor);
            drawRoundRect(canvas,
                    switchBackgroundLeft, switchBackgroundTop, switchBackgroundRight, switchBackgroundBottom,
                    viewRadius, switchBackgroundPaint);

            //绘制未选中时的指示器小圆圈
            if(showSwitchIndicator){
                drawUncheckIndicator(canvas);
            }

            //绘制开启时的背景色
            float des = switchCurrentViewState.radius * .5f;//[0-backgroundRadius*0.5f]
            switchBackgroundPaint.setStyle(Paint.Style.STROKE);
            switchBackgroundPaint.setColor(switchCurrentViewState.checkStateColor);
            switchBackgroundPaint.setStrokeWidth(switchBorderWidth + des * 2f);
            drawRoundRect(canvas,
                    switchBackgroundLeft+ des, switchBackgroundTop + des, switchBackgroundRight - des, switchBackgroundBottom - des,
                    viewRadius, switchBackgroundPaint);

            //绘制按钮左边的长条遮挡
            switchBackgroundPaint.setStyle(Paint.Style.FILL);
            switchBackgroundPaint.setStrokeWidth(1);
            drawArc(canvas,
                    switchBackgroundLeft, switchBackgroundTop,
                    switchBackgroundLeft+ 2 * viewRadius, switchBackgroundTop + 2 * viewRadius,
                    90, 180, switchBackgroundPaint);
            canvas.drawRect(
                    switchBackgroundLeft+ viewRadius, switchBackgroundTop,
                    switchCurrentViewState.buttonX, switchBackgroundTop + 2 * viewRadius,
                    switchBackgroundPaint);

            //绘制Switch的小线条
            if(showSwitchIndicator){
                drawCheckedIndicator(canvas);
            }

            //绘制Switch的按钮
            drawButton(canvas, switchCurrentViewState.buttonX, centerY);

            //更新右侧区域的边界
            rightBound = Math.min(rightBound , (int)switchBackgroundLeft);
        }

        //视图绘制后,计算 左区域的边界 以及 右区域的边界
        leftBound += 5;
        if(rightBound < mWidth / 2){
            rightBound = mWidth /2 + 5;
        }


    }

Vector资源转换为Bitmap

特别的,有时候须要加在vector类型的资源,这时候就须要进行适配啦:

/**
     * 将Vector类型的Drawable转换为Bitmap
     * @param vectorDrawableId vector资源id
     * @return bitmap
     */
    private Bitmap decodeVectorToBitmap(int vectorDrawableId ){
        Drawable vectorDrawable = null;
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
            vectorDrawable = this.mContext.getDrawable(vectorDrawableId);
        }else{
            vectorDrawable = getResources().getDrawable(vectorDrawableId);
        }
        if(vectorDrawable != null){
            //这里若使用Bitmap.Config.RGB565会致使图片资源黑底
            Bitmap b = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(),vectorDrawable.getMinimumHeight(), Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(b);
            vectorDrawable.setBounds(0,0,canvas.getWidth(),canvas.getHeight());
            vectorDrawable.draw(canvas);
            return b;
        }
        return null;
    }

Switch部分的代码

这部分,具体可见OptionBarView.java

相关文章
相关标签/搜索