Android 自定義SwitchButtonView實踐
1、文字繪製基線測量
文字繪製的方法是Canvas類的drawText,對於x點座標其實和正常流程類似,但Y座標的確定需要考慮Baseline問題
@param text The text to be drawn @param x X方向的座標,開始繪製的左上角橫軸座標點 @param y Y座標,該座標是Y軸方向上的”基線”座標 @param paint 畫筆工具 */ public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
基線到中線的距離=(Descent+Ascent)/2-Descent ,Android中,實際獲取到的Ascent是負數。
公式推導過程如下:
中線到BOTTOM的距離是(Descent+Ascent)/2,這個距離又等於Descent+中線到基線的距離,即(Descent+Ascent)/2=基線到中線的距離+Descent。
有了基線到中線的距離,我們只要知道任何一行文字中線的位置,就可以馬上得到基線的位置,從而得到Canvas的drawText方法中引數y的值。
/** * 計算繪製文字時的基線到中軸線的距離,Android獲取中線到基線距離的程式碼,Paint需要設定文字大小textsize。 * * @param p * @param centerY * @return 基線和centerY的距離 */ public static float getBaseline(Paint p) { FontMetrics fontMetrics = p.getFontMetrics(); return (fontMetrics.descent - fontMetrics.ascent) / 2 -fontMetrics.descent; }
說道這裡我們只是計算出了基線高度,Y座標一般區文字高度的中點位置。比如豎直方向,公式為。
Y = centerY + getBaseline(paint);
此外,對於寬度的測量,一般使用如下方法
mPaint.getTextBounds(text, 0, text.length(), mBounds);
float textwidth = mBounds.width();
2、Path閉合區域填充問題
在常見的繪製View的過程中,我們通過Path物件構建複雜的閉合影象,最後一般來通過Paint設定Style.FILL填充區域,但是對於閉合的Path填充,在Android某些版本中不支援填充Path的區域。實際上Path同樣提供了填充方法,可以做到很好的相容。
Android的Path.FillType除了支援上面兩種模式外,還支援了上面兩種模式的反模式,一共定義了EVEN_ODD, INVERSE_EVEN_ODD, WINDING, INVERSE_WINDING 四種模式。
實際上,WINDING類似Paint中的Style.FILL
3、Path 影象合成
一般情況下我們影象是將Bitmap合成,合成時使用Xfermodes,當然Path也可以轉為Bitmap影象資料。
但是Path同樣提供了一系列合成方法
DIFFERENCE:從path1中減去path2
INTERSECT:取path1和path2重合的部分
REVERCE_DIFFERENCE:從path2中減去path1
UNION:聯合path1和path2
XOR:取path1和path2不重合的部分
4、StrokeWidth與區域大小問題
對於帶邊框的View,StrokeWidth在很多情況下被認為不擠佔區域大小,實際上,與此相反,我們計算座標時一定要計算線寬問題。比如繪製線寬StrokeWidth的起點矩形,如果不這樣計算,繪製將會出現邊框寬度不一致的情況。
startX = StrokeWidth;
startY = StrokeWidth;
endX = getWidth() - StrokeWidth;
endY = getHeight- StrokeWidth;
5、觸控MOVE事件問題
很多時候繪製View我們需要處理TouchEvent事件,然而,Android中View預設無法監聽,需要設定一個莫名其妙的引數。
setClickable(true);
5、事件狀態轉移問題
很多時候,我們判斷到某一區域時達到某種條件需要主動結束事件事務,或者改變事件狀態如下然後在傳遞出去,方法如下
MotionEvent actionUP = MotionEvent.obtain(event); //增量式拷貝,比如修修改開始時間、修改修改時間序列
actionUP.setAction(MotionEvent.ACTION_UP);
dispatchTouchEvent(actionUP); //傳遞事件,注意不要造成死迴圈問題
基於以上問題的解決,實現了一個SwitchButton,雖然沒用到Path,但還是考慮了很多問題。
public class SwitchButtonView extends View {
// 例項化畫筆
private TextPaint mPaint = null;
private Path mPath;// 路徑物件
private int lineWidth = 1;
private final int STATUS_LEFT = 0x00;
private final int STATUS_RIGHT = 0x01;
private volatile int mStatus = STATUS_LEFT;
private int textSize = 18;
private volatile float startX = 0; //觸控開始位置
private volatile boolean isTouchState = false;
private volatile float currentX = 0;
private final String[] STATUS = {"開","關"};
private OnStatusChangedListener mOnStatusChangedListener;
public void setLeftText(String text){
STATUS[0] = text;
}
public void setRightText(String text){
STATUS[1] = text;
}
public SwitchButtonView(Context context) {
this(context,null);
}
public SwitchButtonView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public SwitchButtonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
setClickable(true); //設定此項true,否則無法滑動
}
private void initPaint() {
// 例項化畫筆並開啟抗鋸齒
mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG );
mPaint.setAntiAlias(true);
mPaint.setPathEffect(new CornerPathEffect(10)); //設定線條型別
mPaint.setStrokeWidth(dpTopx(lineWidth));
mPaint.setTextSize(dpTopx(textSize));
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if(widthMode!=MeasureSpec.EXACTLY){
width = (int) dpTopx(105*2);
}
if(heightMode!=MeasureSpec.EXACTLY){
height = (int) dpTopx(35*2);
}
setMeasuredDimension(width,height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if(width<=0 || height<=0) return;
int centerX = width/2;
int centerY = height/2;
int lineWidthPixies = (int) dpTopx(lineWidth);
int R = getHeight()/2;
mPaint.setStyle(Paint.Style.STROKE);
int startX = lineWidthPixies;
int startY = lineWidthPixies;
int endX = width - 2*lineWidthPixies; //寬度應該減去左右兩邊的線寬
int endY = height - 2*lineWidthPixies; //寬度應該減去上下兩邊的線寬
canvas.drawRoundRect(new RectF(startX,startY,endX,endY),R,R,mPaint);
//中間分割線
canvas.drawLine(centerX,height*2/5,centerX,height*3/5,mPaint);
drawText(canvas, width, centerY);
drawSlider(canvas,width,height,lineWidthPixies);
}
private void drawText(Canvas canvas, int width, int centerY) {
Rect mBounds = new Rect();
mPaint.getTextBounds(STATUS[0], 0, STATUS[0].length(), mBounds);
float textwidth = mBounds.width();
float textBaseline = centerY + getTextPaintBaseline(mPaint);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText(STATUS[0],width/4-textwidth/2,textBaseline,mPaint);
canvas.drawText(STATUS[1],width*3/4-textwidth/2, textBaseline,mPaint);//文字位置以基線為準
mPaint.setStyle(Paint.Style.STROKE);
}
/**
* 基線到中線的距離=(Descent+Ascent)/2-Descent
注意,實際獲取到的Ascent是負數。公式推導過程如下:
中線到BOTTOM的距離是(Descent+Ascent)/2,這個距離又等於Descent+中線到基線的距離,即(Descent+Ascent)/2=基線到中線的距離+Descent。
*/
public static float getTextPaintBaseline(Paint p) {
Paint.FontMetrics fontMetrics = p.getFontMetrics();
return (fontMetrics.descent - fontMetrics.ascent) / 2 -fontMetrics.descent;
}
private void drawSlider(Canvas canvas, int outwidth, int outheight, int lineWidthPixies) {
int color = mPaint.getColor();
mPaint.setColor(Color.GREEN);
mPaint.setStyle(Paint.Style.FILL);
float width = outwidth - 2*lineWidthPixies;
float height = outheight - 2 * lineWidthPixies;
int slideBarX = 2* lineWidthPixies;
int slideBarY = 2*lineWidthPixies;
int R = (int) (height/2);
if(isTouchState){
canvas.drawRoundRect(new RectF(currentX, slideBarY, currentX+width/2-3*lineWidthPixies, height - lineWidthPixies), R, R, mPaint);
}else {
if (mStatus == STATUS_RIGHT) {
slideBarX = (int) (slideBarY+width/2+lineWidthPixies);
canvas.drawRoundRect(new RectF(slideBarX, slideBarY, width - lineWidthPixies, height - lineWidthPixies), R, R, mPaint);
} else {
canvas.drawRoundRect(new RectF(slideBarX, slideBarY, width / 2 - lineWidthPixies, height - lineWidthPixies), R, R, mPaint);
}
}
mPaint.setColor(color);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float lineWidthPixies = dpTopx(lineWidth);
float width = (getWidth()- 2*lineWidthPixies);
float sliderWidth = width/2;
int actionMasked = event.getActionMasked();
switch (actionMasked){
case MotionEvent.ACTION_DOWN: {
isTouchState = true;
startX = event.getX();
if (startX > (width / 2) && startX<(width-lineWidthPixies) && mStatus == STATUS_LEFT) {
MotionEvent actionUP = MotionEvent.obtain(event);
actionUP.setAction(MotionEvent.ACTION_UP);
dispatchTouchEvent(actionUP);
} else if (startX > lineWidthPixies && (startX < width / 2 && mStatus == STATUS_RIGHT)) {
MotionEvent actionUP = MotionEvent.obtain(event);
actionUP.setAction(MotionEvent.ACTION_UP);
dispatchTouchEvent(actionUP);
}else if(startX<lineWidthPixies || startX>(width-lineWidthPixies)){
MotionEvent actionOUTSIDE = MotionEvent.obtain(event);
actionOUTSIDE.setAction(MotionEvent.ACTION_OUTSIDE);
dispatchTouchEvent(actionOUTSIDE);
}
}
break;
case MotionEvent.ACTION_MOVE:
currentX = event.getX()- sliderWidth/2;
//滑塊移動位置應該相對於中心位置為基準
if(currentX<(2* lineWidthPixies)){
currentX = 2* lineWidthPixies; //最左邊
}else if(currentX>((lineWidthPixies+sliderWidth)+2*lineWidthPixies)){ //最右邊
currentX = (sliderWidth)+2*lineWidthPixies;
}
postInvalidate();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
isTouchState = false;
float xPos = event.getX();
if((xPos>width/2&& mStatus==STATUS_LEFT)){
mStatus = STATUS_RIGHT;
onStatusChanged(mStatus);
}else if(xPos>lineWidthPixies && (xPos<width/2&& mStatus==STATUS_RIGHT)){
mStatus = STATUS_LEFT;
onStatusChanged(mStatus);
}
invalidate();
break;
}
return super.onTouchEvent(event);
}
private void onStatusChanged(int status) {
if(this.mOnStatusChangedListener!=null){
this.mOnStatusChangedListener.onStatusChanged(status);
}
}
private float dpTopx(int dp){
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp,getResources().getDisplayMetrics());
}
public void setOnStatusChangedListener(OnStatusChangedListener l){
this.mOnStatusChangedListener = l;
}
interface OnStatusChangedListener{
void onStatusChanged(int status);
}
}