本文已授權(quán)微信公眾號:鴻洋(hongyangAndroid)在微信公眾號平臺原創(chuàng)首發(fā)
前言
隔一段時間工作不忙的時候就想溫習(xí)一下view相關(guān)的知識毁葱,比起學(xué)習(xí)其他東西,感覺做控件不會顯的枯燥粘昨,日復(fù)一日做著重復(fù)的工作,維護(hù)著項目窜锯,總想在里面添加一些新的東西张肾,比如新的界面開始用kotlin,用的第三方不是很滿意的控件锚扎,想想不是很難就自己來做吞瞪,閑來無聊就看看python入門,最近項目多了一個需要滑動選擇身高和運動時間的控件驾孔,在github上沒找到合適的芍秆,正好拋物線大神發(fā)起了一個自定義view的仿寫活動惯疙,一舉兩得,就有了該控件浪听。
封面圖
效果圖
2017/11/29 新添功能
使用
computeScrollTo(float)
2017/12/22 新添功能
增加了
scaleLimit
屬性用來設(shè)置相鄰2個刻度之間的數(shù)量屬性
支持設(shè)置的屬性
<attr name="scaleLimit" format="integer" /> <!--相鄰2個刻度之間的數(shù)量-->
<attr name="rulerHeight" format="dimension" /> <!--尺子的高度-->
<attr name="rulerToResultgap" format="dimension" /> <!--尺子距離結(jié)果的高度-->
<attr name="scaleGap" format="dimension" /> <!--刻度間距-->
<attr name="scaleCount" format="integer" /> <!--刻度數(shù)-->
<attr name="firstScale" format="float" /> <!--默認(rèn)選中的刻度-->
<attr name="maxScale" format="integer" /> <!--最大刻度-->
<attr name="minScale" format="integer" /> <!--最小刻度-->
<attr name="bgColor" format="color" /> <!--背景色-->
<attr name="smallScaleColor" format="color" /> <!--小刻度的顏色-->
<attr name="midScaleColor" format="color" /> <!--中刻度的顏色-->
<attr name="largeScaleColor" format="color" /> <!--大刻度的顏色-->
<attr name="scaleNumColor" format="color" /> <!--刻度數(shù)的顏色-->
<attr name="resultNumColor" format="color" /> <!--結(jié)果字體的顏色-->
<attr name="unit" format="string" /> <!--單位-->
<attr name="unitColor" format="color" /> <!--單位顏色-->
<attr name="smallScaleStroke" format="dimension" /> <!--小刻度的寬度-->
<attr name="midScaleStroke" format="dimension" /> <!--中刻度的寬度-->
<attr name="largeScaleStroke" format="dimension" /> <!--大刻度的寬度-->
<attr name="resultNumTextSize" format="dimension" /> <!--結(jié)果字體大小-->
<attr name="scaleNumTextSize" format="dimension" /> <!--刻度字體大小-->
<attr name="unitTextSize" format="dimension" /> <!--單位字體大小-->
<attr name="showScaleResult" format="boolean" /> <!--是否顯示結(jié)果值-->
<attr name="isBgRoundRect" format="boolean" /> <!--背景是否圓角-->
使用
compile 'com.github.superSp:RulerView:v1.5'
實現(xiàn)思路
- 初始化畫筆螟碎,以及其他需要的參數(shù)
- 重寫
onMeasuer()
確定尺子的大小 - 重寫
onDraw()
繪畫出靜態(tài)尺子,并且將一些滑動時需要改變的參數(shù)設(shè)置為變量迹栓,繪制時只繪制當(dāng)前屏幕可見區(qū)域掉分,滑動尺子時,只刷新當(dāng)前屏幕模擬滑動并不是真正的滑動 - 重寫
onTouchEvent()
處理滑動克伊,增加滑動速率監(jiān)聽VelocityTracker
以及慣性滑動以及抬起手指時指針落在刻度上面需要的屬性動畫ValueAnimator
實現(xiàn)過程
測量
控件的高度=尺子的高度+結(jié)果值的高度+尺子距離結(jié)果值的高度
控件的寬度=屏幕寬度或者固定寬度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int heightModule = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
switch (heightModule) {
case MeasureSpec.AT_MOST:
height = rulerHeight + (showScaleResult ? resultNumRect.height() : 0) + rulerToResultgap * 2 + getPaddingTop() + getPaddingBottom();
break;
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.EXACTLY:
height = heightSize + getPaddingTop() + getPaddingBottom();
break;
}
width = widthSize + getPaddingLeft() + getPaddingRight();
setMeasuredDimension(width, height);
}
繪制靜態(tài)尺子
- 繪制背景
drawRect()
private void drawBg(Canvas canvas) {
bgRect.set(0, 0, width, height);
if (isBgRoundRect) {
canvas.drawRoundRect(bgRect, 20, 20, bgPaint); //20->橢圓的用于圓形角x-radius
} else {
canvas.drawRect(bgRect, bgPaint);
}
}
- 繪制尺子
這一步是繪制控件中最為復(fù)雜的一步酥郭,需要考慮初始如何默認(rèn)選中初始刻度,手指抬起時候尺子需要滑動到的位置愿吹,計算當(dāng)前所處刻度等等不从。
繪制滑動類型的view時亮瓷,當(dāng)初的想法是一次性繪制出全部內(nèi)容兆览,之后使用
canvas.clipRect()
裁剪掉不可見區(qū)域仇让,但是如果內(nèi)容區(qū)域比較大昌妹,例如需要繪制1000個內(nèi)容,則沒滑動一次for循環(huán)需要執(zhí)行1000次疙渣,而且刻度越大時候循環(huán)越多晃危,占用內(nèi)存更大毛肋,會造成卡頓枫耳,因此換了另外一種思路乏矾,只繪制當(dāng)前屏幕可見區(qū)域內(nèi)容,這樣無論刻度有多大迁杨,暫用的內(nèi)存都很小钻心,滑動時,通過不斷刷新來模擬滑動铅协,做到以假亂真的效果捷沸。。狐史。
private void drawScaleAndNum(Canvas canvas) {
canvas.translate(0, (showScaleResult ? resultNumRect.height() : 0) + rulerToResultgap);//移動畫布到結(jié)果值的下面
int num1;//確定刻度位置
float num2;
if (firstScale != -1) { //第一次進(jìn)來的時候計算出默認(rèn)刻度對應(yīng)的假設(shè)滑動的距離moveX
moveX = getWhichScalMovex(firstScale); //如果設(shè)置了默認(rèn)滑動位置亿胸,計算出需要滑動的距離
lastMoveX = moveX;
firstScale = -1; //將結(jié)果置為-1,下次不再計算初始位置
}
num1 = -(int) (moveX / scaleGap); //滑動刻度的整數(shù)部分
num2 = (moveX % scaleGap); //滑動刻度的小數(shù)部分
canvas.save(); //保存當(dāng)前畫布
rulerRight = 0; //準(zhǔn)備開始繪制當(dāng)前屏幕,從最左面開始
if (isUp) { //這部分代碼主要是計算手指抬起時预皇,慣性滑動結(jié)束時,刻度需要停留的位置
num2 = ((moveX - width / 2 % scaleGap) % scaleGap); //計算滑動位置距離整點刻度的小數(shù)部分距離
if (num2 <= 0) {
num2 = scaleGap - Math.abs(num2);
}
leftScroll = (int) Math.abs(num2); //當(dāng)前滑動位置距離左邊整點刻度的距離
rightScroll = (int) (scaleGap - Math.abs(num2)); //當(dāng)前滑動位置距離右邊整點刻度的距離
float moveX2 = num2 <= scaleGap / 2 ? moveX - leftScroll : moveX + rightScroll; //最終計算出當(dāng)前位置到整點刻度位置需要滑動的距離
if (valueAnimator != null && !valueAnimator.isRunning()) { //手指抬起婉刀,并且當(dāng)前沒有慣性滑動在進(jìn)行吟温,創(chuàng)建一個慣性滑動
valueAnimator = ValueAnimator.ofFloat(moveX, moveX2);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
moveX = (float) animation.getAnimatedValue(); //不斷滑動去更新新的位置
lastMoveX = moveX;
invalidate();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() { //增加一個監(jiān)聽,用來返回給使用者滑動結(jié)束后的最終結(jié)果刻度值
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
//這里是滑動結(jié)束時候回調(diào)給使用者的結(jié)果值
if (onChooseResulterListener != null) {
onChooseResulterListener.onEndResult(resultText);
}
}
});
valueAnimator.setDuration(300);
valueAnimator.start();
isUp = false;
}
num1 = (int) -(moveX / scaleGap); //重新計算當(dāng)前滑動位置的整數(shù)以及小數(shù)位置
num2 = (moveX % scaleGap);
}
canvas.translate(num2, 0); //不加該偏移的話突颊,滑動時刻度不會落在0~1之間只會落在整數(shù)上面,其實這個都能設(shè)置一種模式了鲁豪,畢竟初衷就是指針不會落在小數(shù)上面
//這里是滑動時候不斷回調(diào)給使用者的結(jié)果值
resultText = String.valueOf(new WeakReference<>(new BigDecimal((width / 2 - moveX) / (scaleGap * scaleCount))).get().setScale(1, BigDecimal.ROUND_HALF_UP).floatValue() + minScale);
if (onChooseResulterListener != null) {
onChooseResulterListener.onScrollResult(resultText); //接口不斷回調(diào)給使用者結(jié)果值
}
//繪制當(dāng)前屏幕可見刻度,不需要裁剪屏幕,while循環(huán)只會執(zhí)行·屏幕寬度/刻度寬度·次,大部分的繪制都是if(curDis<width)這樣子內(nèi)存暫用相對來說會比較高潘悼。。
while (rulerRight < width) {
if (num1 % scaleCount == 0) { //繪制整點刻度以及文字
if ((moveX >= 0 && rulerRight < moveX - scaleGap) || width / 2 - rulerRight <= getWhichScalMovex(maxScale + 1) - moveX) {
//當(dāng)滑動出范圍的話爬橡,不繪制治唤,去除左右邊界
} else {
//繪制刻度,繪制刻度數(shù)字
canvas.drawLine(0, 0, 0, midScaleHeight, midScalePaint);
scaleNumPaint.getTextBounds(num1 / scaleGap + minScale + "", 0, (num1 / scaleGap + minScale + "").length(), scaleNumRect);
canvas.drawText(num1 / scaleCount + minScale + "", -scaleNumRect.width() / 2, lagScaleHeight +
(rulerHeight - lagScaleHeight) / 2 + scaleNumRect.height(), scaleNumPaint);
}
} else { //繪制小數(shù)刻度
if ((moveX >= 0 && rulerRight < moveX) || width / 2 - rulerRight < getWhichScalMovex(maxScale) - moveX) {
//當(dāng)滑動出范圍的話糙申,不繪制宾添,去除左右邊界
} else {
//繪制小數(shù)刻度
canvas.drawLine(0, 0, 0, smallScaleHeight, smallScalePaint);
}
}
++num1; //刻度加1
rulerRight += scaleGap; //繪制屏幕的距離在原有基礎(chǔ)上+1個刻度間距
canvas.translate(scaleGap, 0); //移動畫布到下一個刻度
}
canvas.restore();
//繪制屏幕中間用來選中刻度的最大刻度
canvas.drawLine(width / 2, 0, width / 2, lagScaleHeight, lagScalePaint);
}
繪制結(jié)果
//繪制上面的結(jié)果 結(jié)果值+單位
private void drawResultText(Canvas canvas, String resultText) {
if (!showScaleResult) { //判斷用戶是否設(shè)置需要顯示當(dāng)前刻度值,如果否則取消繪制
return;
}
canvas.translate(0, -resultNumRect.height() - rulerToResultgap / 2); //移動畫布到正確的位置來繪制結(jié)果值
resultNumPaint.getTextBounds(resultText, 0, resultText.length(), resultNumRect);
canvas.drawText(resultText, width / 2 - resultNumRect.width() / 2, resultNumRect.height(), //繪制當(dāng)前刻度結(jié)果值
resultNumPaint);
resultNumRight = width / 2 + resultNumRect.width() / 2 + 10;
canvas.drawText(unit, resultNumRight, kgRect.height() + 2, kgPaint); //在當(dāng)前刻度結(jié)果值的又面10px的位置繪制單位
}
處理滑動
主要是記錄moveX柜裸,以及添加velocityTracker
速度監(jiān)聽器缕陕,以及處理慣性滑動
@Override
public boolean onTouchEvent(MotionEvent event) {
currentX = event.getX();
isUp = false;
velocityTracker.computeCurrentVelocity(500);
velocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//按下時如果屬性動畫還沒執(zhí)行完,就終止,記錄下當(dāng)前按下點的位置
if (valueAnimator != null && valueAnimator.isRunning()) {
valueAnimator.end();
valueAnimator.cancel();
}
downX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
//滑動時候,通過假設(shè)的滑動距離,做超出左邊界以及右邊界的限制。
moveX = currentX - downX + lastMoveX;
if (moveX >= width / 2) {
moveX = width / 2;
} else if (moveX <= getWhichScalMovex(maxScale)) {
moveX = getWhichScalMovex(maxScale);
}
break;
case MotionEvent.ACTION_UP:
//手指抬起時候制造慣性滑動
lastMoveX = moveX;
xVelocity = (int) velocityTracker.getXVelocity();
autoVelocityScroll(xVelocity);
velocityTracker.clear();
break;
}
invalidate();
return true;
}
處理慣性滑動的代碼
這里就是調(diào)節(jié)了疙挺,根據(jù)得到的速率調(diào)節(jié)出比較舒服的滑動扛邑。。铐然。
private void autoVelocityScroll(int xVelocity) {
//慣性滑動的代碼,速率和滑動距離,以及滑動時間需要控制的很好,應(yīng)該網(wǎng)上已經(jīng)有關(guān)于這方面的算法了吧蔬崩。。這里是經(jīng)過N次測試調(diào)節(jié)出來的慣性滑動
if (Math.abs(xVelocity) < 50) {
isUp = true;
return;
}
if (valueAnimator.isRunning()) {
return;
}
valueAnimator = ValueAnimator.ofInt(0, xVelocity / 20).setDuration(Math.abs(xVelocity / 10));
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
moveX += (int) animation.getAnimatedValue();
if (moveX >= width / 2) {
moveX = width / 2;
} else if (moveX <= getWhichScalMovex(maxScale)) {
moveX = getWhichScalMovex(maxScale);
}
lastMoveX = moveX;
invalidate();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
isUp = true;
invalidate();
}
});
valueAnimator.start();
}
供外部使用的獲取結(jié)果值的接口
public interface OnChooseResulterListener {
void onEndResult(String result); //結(jié)束滑動時候返回的結(jié)果
void onScrollResult(String result); //滑動時不斷產(chǎn)生的結(jié)果
}
最后再貼一下使用以及地址
compile 'com.github.superSp:RulerView:v1.5'