<異空間>項目技術分享系列——自定義View仿微信設置選項條目java
關於設置選項條目,在大部分App內仍是挺經常使用的,UI效果有左圖標文字,右文字箭頭、開關等等android
以微信設置頁面的各類條目爲例子:git
XML佈局裏面,每一行的條目,都使用一個線性佈局/相對佈局,包裹住全部的控件後,就能夠對控件大小/位置進行調整。github
缺點:同一頁面大量編寫這樣相似的佈局會致使開發者感受空虛煩躁無聊,還要對大量的這些佈局的控件設置id設置事件監聽很麻煩等等canvas
在作項目的過程當中發現常常地要寫各類各樣的點擊選項的條目,常見的"設置頁"的條目,通常的作法是每寫一個條目選項就要寫一個佈局而後裏面配置一堆的View,雖然也能完成效果,可是若是數量不少或者設計圖效果各異就會容易出錯浪費不少時間,同時一個頁面若是有過多的佈局嵌套也會影響效率。api
因而,我開始找一些定製性高且內部經過純Canvas就能完成全部繪製的框架。最後,我找到了由GitLqr做者開發的LQROptionItemView,大致知足需求,在此很是感謝做者GitLqr,可是在使用過程當中發現幾個小問題:微信
因爲原做者的項目近幾年好像都沒有繼續維護了,因而我打算本身動手改進以上的問題,並開源OptionBarViewapp
下圖列舉了幾種常見的條目效果,項目還支持更多不一樣的效果搭配。框架
在Project 的 build.gradlemaven
allprojects { repositories { ... maven { url 'https://jitpack.io' } } }
在Module 的 build.gradle
dependencies { implementation 'com.github.DMingOu:OptionBarView:1.1.0' }
屬性都可選,不設置的屬性則不顯示,⭐圖片與文字的距離若不設置會有一個默認的距離,可設置任意類型的圖片資源。
<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" />
方式與其餘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(); } });
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>
//中間標題 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
-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,內置了默認的邊距,但會優先使用本身所設置的屬性值:
按照繪製背景 - 繪製左區域 - 繪製右區域
在繪製左/右區域的控件時根據傳入的屬性選擇性的繪製
代碼及詳細註釋以下:
@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類型的資源,這時候就須要進行適配啦:
/** * 將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; }
這部分,具體可見OptionBarView.java