編輯:關於Android編程
前段時間群裡兄弟項目中有類似這樣的需求
我看到兄弟受苦受難,於心不忍。又因事不關己,打算高高掛起。正在愛恨糾結之時,日神對我說:沒事多造點輪子,你的人生會有很多收獲。這波雞湯讓我深受觸動,於是決定拯救兄弟於水生火熱之中。
重寫onMeasure 決策自身大小
顯而易見當可以拖拽的范圍極限為零時,也就是RangeSeeBar正常顯示能夠接受的極限,粗略一看:Width > 2 * Height
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (heightSize * 2 > widthSize) { setMeasuredDimension(widthSize, widthSize / 2); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }
public class RangeSeekBar extends View { private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private int lineTop, lineBottom, lineLeft, lineRight; private int lineCorners; private int lineWidth; private RectF line = new RectF(); public RangeSeekBar(Context context) { this(context, null); } public RangeSeekBar(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (heightSize * 2 > widthSize) { setMeasuredDimension(widthSize, (int) (widthSize / 2)); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); int seekBarRadius = h / 2; /** * 屬性 left right top bottom 描述了SeekBar按鈕的位置 * 藍後根據它們預先設置確定出 RectF line 背景的三維 * lineCorners 圓滑的邊緣似乎會比直角更好看 */ lineLeft = seekBarRadius; lineRight = w - seekBarRadius; lineTop = seekBarRadius - seekBarRadius / 4; lineBottom = seekBarRadius + seekBarRadius / 4; lineWidth = lineRight - lineLeft; line.set(lineLeft, lineTop, lineRight, lineBottom); lineCorners = (int) ((lineBottom - lineTop) * 0.45f); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); paint.setStyle(Paint.Style.FILL); paint.setColor(0xFFD7D7D7); canvas.drawRoundRect(line, lineCorners, lineCorners, paint); } }
拖動舞台已經備好,SeekBar按鈕半徑也已定好。順水推舟,下一步就繪制SeekBar把。
SeekBar按鈕 擁有對象是極好的
粗略一想:按鈕有顏色、有大小、有變色、被繪制,碰撞檢測、邊界檢測、被拖拽等,最關鍵的是有多個。因此SeekBar按鈕可以說是一個復雜的集合體,是時候來發對象了。
private class SeekBar { int widthSize; int left, right, top, bottom; Bitmap bmp; /** * 當RangeSeekBar尺寸發生變化時,SeekBar按鈕尺寸隨之變化 * * @param centerX SeekBar按鈕的X中心在RangeSeekBar中的相對位置 * @param centerY SeekBar按鈕的Y中心在RangeSeekBar中的相對位置 * @param heightSize RangeSeekBar期望SeekBar所擁有的高度 */ void onSizeChanged(int centerX, int centerY, int heightSize) { /** * 屬性 left right top bottom 描述了SeekBar按鈕的位置 * widthSize = heightSize * 0.8f 可見按鈕實際區域是個矩形而非正方形 * 圓圈按鈕為什麼要占有矩形區域?因為按鈕陰影效果。不要陰影不行嗎?我就不 * 那麼 onMeasure 那邊說好的2倍寬度?我就不 */ widthSize = (int) (heightSize * 0.8f); left = centerX - widthSize / 2; right = centerX + widthSize / 2; top = centerY - heightSize / 2; bottom = centerY + heightSize / 2; bmp = Bitmap.createBitmap(widthSize, heightSize, Bitmap.Config.ARGB_8888); int bmpCenterX = bmp.getWidth() / 2; int bmpCenterY = bmp.getHeight() / 2; int bmpRadius = (int) (widthSize * 0.5f); Canvas defaultCanvas = new Canvas(bmp); Paint defaultPaint = new Paint(Paint.ANTI_ALIAS_FLAG); // 繪制Shadow defaultPaint.setStyle(Paint.Style.FILL); int barShadowRadius = (int) (bmpRadius * 0.95f); defaultCanvas.save(); defaultCanvas.translate(0, bmpRadius * 0.25f); RadialGradient shadowGradient = new RadialGradient(bmpCenterX, bmpCenterY, barShadowRadius, Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP); defaultPaint.setShader(shadowGradient); defaultCanvas.drawCircle(bmpCenterX, bmpCenterY, barShadowRadius, defaultPaint); defaultPaint.setShader(null); defaultCanvas.restore(); // 繪制Body defaultPaint.setStyle(Paint.Style.FILL); defaultPaint.setColor(0xFFFFFFFF); defaultCanvas.drawCircle(bmpCenterX, bmpCenterY, bmpRadius, defaultPaint); // 繪制Border defaultPaint.setStyle(Paint.Style.STROKE); defaultPaint.setColor(0xFFD7D7D7); defaultCanvas.drawCircle(bmpCenterX, bmpCenterY, bmpRadius, defaultPaint); } void draw(Canvas canvas) { canvas.drawBitmap(bmp, left, top, null); } }
public class RangeSeekBar extends View { private SeekBar seekBar = new SeekBar(); private class SeekBar { ... } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); int seekBarRadius = h / 2; ... // 在RangeSeekBar確定尺寸時確定SeekBar按鈕尺寸 seekBar.onSizeChanged(seekBarRadius, seekBarRadius, h); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); ... // 在RangeSeekBar被繪制時繪制SeekBar按鈕 seekBar.draw(canvas); } }
onTouchEvent 觸摸監聽 讓SeekBar按鈕動起來
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: boolean touchResult = false; // 進行檢測,手指手指是否落在當前SeekBar上。即聲明SeekBar時使用left、top、right、bottom屬性所描述區域的內部 if (seekbar.collide(event)) { touchResult = true; } return touchResult; case MotionEvent.ACTION_MOVE: float percent; float x = event.getX(); if (x <= lineLeft) { percent = 0; } else if (x >= lineRight){ percent = 1; } else { percent = (x - lineLeft) * 1f / (lineWidth); } // SeekBar按鈕根據當前手指在拖動條上的滑動而滑動 seekbar.slide(percent); invalidate(); break; } return super.onTouchEvent(event); }
private class SeekBar { int lineWidth; // 拖動條寬度 可在onSizeChanged時刻獲得 float currPercent; int left, right, top, bottom; boolean collide(MotionEvent event) { float x = event.getX(); float y = event.getY(); int offset = (int) (lineWidth * currPercent); return x > left + offset && x < right + offset && y > top && y < bottom; } void slide(float percent) { if (percent < 0) percent = 0; else if (percent > 1) percent = 1; currPercent = percent; } void draw(Canvas canvas) { int offset = (int) (lineWidth * currPercent); canvas.save(); canvas.translate(offset, 0); canvas.drawBitmap(bmp, left, top, null); canvas.restore(); } }
更好的視覺體驗
到目前位置,SeekBar被按壓時顯得死氣沉沉,接下來為其添加強烈的視覺反饋。
那麼之前通過onSizeChanged預設按鈕的偷懶手段就GG了,因為SeekBar的UI效果需要隨觸摸狀態的變化而變化。
首先在onTouchEvent中拿到這個變化
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_MOVE: seekBar.material = seekBar.material >= 1 ? 1 : seekBar.material + 0.1f; ... invalidate(); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: seekBar.materialRestore(); break; } return super.onTouchEvent(event); }
private class SeekBar { float material = 0; ValueAnimator anim; final TypeEvaluatorte = new TypeEvaluator () { @Override public Integer evaluate(float fraction, Integer startValue, Integer endValue) { int alpha = (int) (Color.alpha(startValue) + fraction * (Color.alpha(endValue) - Color.alpha(startValue))); int red = (int) (Color.red(startValue) + fraction * (Color.red(endValue) - Color.red(startValue))); int green = (int) (Color.green(startValue) + fraction * (Color.green(endValue) - Color.green(startValue))); int blue = (int) (Color.blue(startValue) + fraction * (Color.blue(endValue) - Color.blue(startValue))); return Color.argb(alpha, red, green, blue); } }; void draw(Canvas canvas) { int offset = (int) (lineWidth * currPercent); canvas.save(); canvas.translate(left, 0); canvas.translate(offset, 0); drawDefault(canvas); canvas.restore(); } private void drawDefault(Canvas canvas) { int centerX = widthSize / 2; int centerY = heightSize / 2; int radius = (int) (widthSize * 0.5f); // draw shadow defaultPaint.setStyle(Paint.Style.FILL); canvas.save(); canvas.translate(0, radius * 0.25f); canvas.scale(1 + (0.1f * material), 1 + (0.1f * material), centerX, centerY); defaultPaint.setShader(shadowGradient); canvas.drawCircle(centerX, centerY, radius, defaultPaint); defaultPaint.setShader(null); canvas.restore(); // draw body defaultPaint.setStyle(Paint.Style.FILL); defaultPaint.setColor(te.evaluate(material, 0xFFFFFFFF, 0xFFE7E7E7)); canvas.drawCircle(centerX, centerY, radius, defaultPaint); // draw border defaultPaint.setStyle(Paint.Style.STROKE); defaultPaint.setColor(0xFFD7D7D7); canvas.drawCircle(centerX, centerY, radius, defaultPaint); } private void materialRestore() { if (anim != null) anim.cancel(); anim = ValueAnimator.ofFloat(material, 0); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { material = (float) animation.getAnimatedValue(); invalidate(); } }); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { material = 0; invalidate(); } }); anim.start(); } }
Range
Range的意思就是范圍,但是就算知道這些似乎並沒有什麼卵用 _(:3 」∠)_
so為了了解其中規律,本寶寶使勁摸索。最終發現
如果分開來看它們都擁有自己的固定滑動區間,右邊的SeekBar按鈕就是左邊SeekBar按鈕向右平移了個SeekBar按鈕寬度而已。
public class RangeSeekBar extends View { private SeekBar leftSB = new SeekBar(); private SeekBar rightSB = new SeekBar(); /** * 用來記錄當前用戶觸摸的到底是哪個SB */ private SeekBar currTouch; @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); ... // rightSB就如同分析的一樣,緊緊貼在leftSB的右邊而已 rightSB.left += leftSB.widthSize; rightSB.right += leftSB.widthSize; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); ... leftSB.draw(canvas); rightSB.draw(canvas); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: boolean touchResult = false; /** * 為什麼不先檢測leftSB而先檢測rightSB?為什麼? (●'?'●) */ if (rightSB.collide(event)) { currTouch = rightSB; touchResult = true; } else if (leftSB.collide(event)) { currTouch = leftSB; touchResult = true; } return touchResult; case MotionEvent.ACTION_MOVE: float percent; float x = event.getX(); if (currTouch == leftSB) { if (x < lineLeft) { percent = 0; } else { percent = (x - lineLeft) * 1f / (lineWidth - rightSB.widthSize); } if (percent > rightSB.currPercent) { percent = rightSB.currPercent; } leftSB.slide(percent); } else if (currTouch == rightSB) { if (x > lineRight) { percent = 1; } else { percent = (x - lineLeft - leftSB.widthSize) * 1f / (lineWidth - leftSB.widthSize); } if (percent < leftSB.currPercent) { percent = leftSB.currPercent; } rightSB.slide(percent); } invalidate(); break; } return super.onTouchEvent(event); } }
比如現在2個按鈕直接就保留了一個距離,當然也可以保留n個
支持刻度模式
當然支持刻度的同時也支持預留范圍
支持自定義UI按鈕樣式背景顏色
似乎少了按壓狀態變化
一個朋友今天有這麼個需求(下圖),我覺得那自定義View來做還是很適合的,就做了下,順便和大家分享下,對於自定義View多練沒壞處麼。如果你看了前兩篇,那麼
隨著公司新業務的起步由於原有APP_A的包已經很大了,所以上邊要求另外開發一款APP_B,要求是APP_A和APP_B賬號通用且兩個APP可以相互打開。賬號通用也就是說在
引言上一篇講解了TabLayout,接下來我們繼續學習Google I/O 2015 推出的 Android Design Support Library的其他成員,這一
本文實例講述了Android單選按鈕對話框用法。分享給大家供大家參考。具體如下:main.xml布局文件<?xml version=1.0 encoding