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

相關文章
相關標籤/搜索