原創文章,轉載請註明出處,juejin.im/post/5bebed…android
某設計網常常會有不少優秀漂亮的交互設計做品,有一天,偶遇這樣的效果,動畫流暢,交互天然,因而埋頭本身解剖其中的元素,作了個開源控件,十來天有了一百來個star,以爲很受歡迎,今天專門寫這潦草幾筆,分享案發通過,但願對同行有所幫助。git
一、默認狀態:直線,首尾標註,默認值標註。github
二、手指按下:曲線動畫執行,標識小圓規則地放大,值的標註根據曲線波峯相對位置不變,向上同速移動,同時,標註背景漸變加深,動畫結束。canvas
三、手指拖動效果:曲線、小圓形、標註三者同時跟隨觸摸點移動,同時更新標註值。api
四、手指離開屏幕:曲線收回動畫執行,標識小圓規則縮小到默認狀態,選中的值跟隨波峯下沉到默認狀態,標註背景漸變消失,動畫結束。bash
拆解狀態三部分:默認狀態、觸摸過程當中、觸摸後狀態。其中默認狀態下指示器很小的圓形,距離水平曲線下方一個約定距離,當按下過程當中,圓最下方座標不變,圓直徑逐漸增大,圓頂部與曲線的距離不變,直到動畫結束。app
一、控件內元素:標尺、標註用的小圓、選中值,都可配置各自的顏色,ide
二、可配置值範圍,post
三、可配置默認值,測試
四、可實時監聽選中的值。
五、可顯示單位。
經過靜態截圖可知本控件主要元素爲觸摸觸發的曲線和其伸縮效果。讓咱們來簡單分析一下曲線部分的結構: //這裏展現曲線拆解圖 拆解後,觸摸部分爲六階貝塞爾曲線,五個基準點,四個控制點,咱們將它拆分紅兩個三階曲線便可。其中,首尾基準點的Y座標固定,X座標隨着觸摸位置相對移動,剩下的基準點X座標相對固定,Y座標根據動畫規律升降。再說控制點,爲保障默認狀態下,曲線部分爲水平,首尾兩個控制點的Y座標固定,X座標相對固定,中間兩個控制點Y座標和中間那個基準點一致,X相對中間基準點固定。 (經過上面的拆解,可讓曲線在默認狀態下是一條水平直線,而且在按下狀態下,與水平位置、波峯位置,能有比較天然的過分弧形,不至於那麼生硬。)
普通圓形,相對自身底部向上變大、向下縮小還原。
按下時的動畫採用普通的ValueAnimator,勻速LinearInterpolator。另外還有個選中值的背景變化,根據動畫進度改變畫筆Alpha值便可。 //這裏寫點動畫代碼囉。
泡杯茶,挽起袖子開擼!
繼承View:
public class BezierSeekBar extends View {
public BezierSeekBar(Context context) {
super(context);
init(context, null);
}
public BezierSeekBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public BezierSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public BezierSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
this.context = context;
}
}
複製代碼
先繪製出曲線效果:
//第一個三階曲線
bezierPath.moveTo(this.fingerX - circleRadiusMax * 2 * 3, (float) 2 * height / 3);
bezierPath.cubicTo(this.fingerX - circleRadiusMax * 2 * 2, (float) 2 * height / 3, this.fingerX - circleRadiusMax * 2 * 1, (float) 2 * height / 3 - bezierHeight, this.fingerX, (float) 2 * height / 3 - bezierHeight);
//第二個三階曲線
bezierPath.moveTo(this.fingerX, (float) 2 * height / 3 - bezierHeight);
bezierPath.cubicTo(this.fingerX + circleRadiusMax * 2, (float) 2 * height / 3 - bezierHeight, this.fingerX + circleRadiusMax * 2 * 2, (float) 2 * height / 3, this.fingerX + circleRadiusMax * 2 * 3, (float) 2 * height / 3);
複製代碼
改變其Y座標,讓曲線恢復默認狀態: 繪製完整線條:
//line1
bezierPath.reset();
bezierPath.moveTo(0, (float) 2 * height / 3);
bezierPath.lineTo(this.fingerX - circleRadiusMax * 2 * 3, (float) 2 * height / 3);
//bezier1
bezierPath.moveTo(this.fingerX - circleRadiusMax * 2 * 3, (float) 2 * height / 3);
bezierPath.cubicTo(this.fingerX - circleRadiusMax * 2 * 2, (float) 2 * height / 3, this.fingerX - circleRadiusMax * 2 * 1, (float) 2 * height / 3 - bezierHeight, this.fingerX, (float) 2 * height / 3 - bezierHeight);
//bezier2
bezierPath.moveTo(this.fingerX, (float) 2 * height / 3 - bezierHeight);
bezierPath.cubicTo(this.fingerX + circleRadiusMax * 2, (float) 2 * height / 3 - bezierHeight, this.fingerX + circleRadiusMax * 2 * 2, (float) 2 * height / 3, this.fingerX + circleRadiusMax * 2 * 3, (float) 2 * height / 3);
//line2
bezierPath.lineTo(width, (float) 2 * height / 3);
canvas.drawPath(bezierPath, bezierPaint);
複製代碼
添加Touch事件攔截,按下時顯示曲線:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
fingerX = event.getX();
if (fingerX < 0F) fingerX = 0F;
if (fingerX > width) fingerX = width;
//觸摸介質進入控件,開始動畫過渡
this.animatorFingerIn.start();
break;
case MotionEvent.ACTION_MOVE:
fingerX = event.getX();
if (fingerX < 0F) fingerX = 0F;
if (fingerX > width) fingerX = width;
postInvalidate();
break;
case MotionEvent.ACTION_UP:
//觸摸介質離開控件,執行動畫
this.animatorFingerOut.start();
break;
}
valueSelected = Integer.valueOf(decimalFormat.format(valueMin + (valueMax - valueMin) * fingerX / width));
if (selectedListener != null) {
selectedListener.onSelected(valueSelected);
}
return true;
}
複製代碼
添加動畫效果:
this.animatorFingerIn = ValueAnimator.ofFloat(0f, 1f);
this.animatorFingerIn.setDuration(200L);
this.animatorFingerIn.setInterpolator(new LinearInterpolator());
this.animatorFingerOut = ValueAnimator.ofFloat(1f, 0f);
this.animatorFingerOut.setDuration(200L);
this.animatorFingerOut.setInterpolator(new LinearInterpolator());
this.animatorFingerOut.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float progress = (float) animation.getAnimatedValue();
animInFinshed = (progress >= 0.15F);
txtSelectedBgPaint.setAlpha((int) (255 * (progress - 0.15F)));
if (progress >= 0.95F) {
textPaint.setColor(colorValueSelected);
} else {
textPaint.setColor(colorValue);
}
bezierHeight = circleRadiusMax * 1.5F * progress;
circleRadius = circleRadiusMin + (circleRadiusMax - circleRadiusMin) * progress;
spaceToLine = circleRadiusMin * 2 * (1F - progress);
postInvalidate();
}
});
複製代碼
繪製圓形指示器,根據動畫進度改變其大小:
canvas.drawCircle(this.fingerX, (float) 2 * height / 3 + spaceToLine + circleRadius, circleRadius, ballPaint);
複製代碼
添加其它輔助元素後,配置通用屬性,拋出公共方法:
<declare-styleable name="BezierSeekBar">
//曲線顏色
<attr name="bsBar_color_line" format="reference|color" />
//圓形指示器顏色
<attr name="bsBar_color_ball" format="reference|color" />
//閥值的文本顏色
<attr name="bsBar_color_value" format="reference|color" />
//選中值的文本顏色
<attr name="bsBar_color_value_selected" format="reference|color" />
//選中值的文本顏色背景
<attr name="bsBar_color_bg_selected" format="reference|color" />
//閥值最小
<attr name="bsBar_value_min" format="integer" />
//閥值最大
<attr name="bsBar_value_max" format="integer" />
//默認選中值
<attr name="bsBar_value_selected" format="integer" />
//單位
<attr name="bsBar_unit" format="reference|string" />
</declare-styleable>
複製代碼
private void initAttr(Context context, AttributeSet attrs) {
if (attrs != null) {
TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.BezierSeekBar);
this.colorBall = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_ball, Color.BLACK);
this.colorLine = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_line, Color.BLACK);
this.colorValue = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_value, Color.BLACK);
this.colorValueSelected = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_value_selected, Color.WHITE);
this.colorBgSelected = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_bg_selected, Color.BLACK);
this.valueMin = attributes.getInteger(R.styleable.BezierSeekBar_bsBar_value_min, 30);
this.valueMax = attributes.getInteger(R.styleable.BezierSeekBar_bsBar_value_max, 150);
this.valueSelected = attributes.getInteger(R.styleable.BezierSeekBar_bsBar_value_selected, 65);
this.unit = attributes.getString(R.styleable.BezierSeekBar_bsBar_unit) + "";
attributes.recycle();
}
}
複製代碼
最後,測試一下:
<tech.nicesky.bezierseekbar.BezierSeekBar android:id="@+id/bsBar_test" app:bsBar_color_ball="@android:color/white" app:bsBar_color_bg_selected="@android:color/white" app:bsBar_color_line="@android:color/white" app:bsBar_color_value="@android:color/white" app:bsBar_color_value_selected="#ef5350" app:bsBar_value_min="30" app:bsBar_value_max="120" app:bsBar_value_selected="65" app:bsBar_unit="kg" android:layout_width="match_parent" android:layout_height="wrap_content" />
複製代碼
完美 :) ! 附上demo APK:
本控件主要涉及到貝塞爾曲線的基礎理解和應用、動畫的基礎應用、自定義控件的常規流程,重點仍是熟練各類UI效果的分析拆解和思路整理。
歡迎Star,開源地址:
https://github.com/fairytale110/BezierSeekBar
複製代碼