前幾天寫了一篇一步一步教你實現即刻點贊效果後,實現點贊效果主要是本身對自定義View的一些canvas繪製,縮放知識,位移的理解。而朋友說HenCoder還有給出薄荷健康滑動捲尺,小米運動記錄界面,Flipboard 紅板報的翻頁效果。這幾個例子對自定義View知識頗有表明性,都用到了不一樣的知識。而今天要實現的是薄荷健康滑動捲尺效果,主要是加深觸摸反饋,和在Android座標系中,獲取View不一樣環境下座標系的方法,也恰好鞏固滑動如scrllTo()和scrllBy()用法。php
剛看到上圖,就立刻想到了Android裏的Scroller,這個類是專門處理滾動的工具類,咱們平時在開發中直接使用Scroller的場景很少,可是咱們不少時候都會接觸到它,像ViewPager、ListView。在Android中任何一個控件都是能夠移動的,由於VIew類中有scrollTo()和scrollBy()方法。java
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout android:id="@+id/activity_main" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.uestc.horizontalrulerview.MainActivity">
<Button android:id="@+id/btn_one" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="BUTTONONE"/>
<Button android:id="@+id/btn_two" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/btn_one" android:text="BUTTONTWO"/>
</RelativeLayout>
複製代碼
MainActivity文件:android
public class MainActivity extends AppCompatActivity {
private Button btn_one;
private Button btn_two;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_one = (Button) findViewById(R.id.btn_one);
btn_two = (Button) findViewById(R.id.btn_two);
btn_one.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
btn_one.scrollTo(-50, -100);
}
});
btn_two.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
btn_two.scrollBy(-50, -100);
}
});
}
}
複製代碼
效果以下: git
發現button沒有移動,可是裏面的文本缺不見了,能夠猜想,應該是 移動本身佈局裏面的內容。如今在佈局文件給兩個button加上一個父佈局:<RelativeLayout android:id="@+id/activity_main" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.uestc.horizontalrulerview.MainActivity">
<LinearLayout android:id="@+id/ll_btn" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
<Button android:id="@+id/btn_one" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="BUTTONONE"/>
<Button android:id="@+id/btn_two" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="BUTTONTWO"/>
</LinearLayout>
</RelativeLayout>
複製代碼
MainActivity文件改成:github
btn_one.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ll_btn.scrollTo(-50,-100);
}
});
btn_two.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ll_btn.scrollBy(-50,-100);
}
});
複製代碼
改成對這個佈局進行滾動。 看看效果: canvas
BUTTONONE調用了scrollTo方法,BUTTONTWO調用了scrollBy方法,發現BUTTONONE調用了一次scrollTo方法後繼續調用會沒有效果,而BUTTONTWO調用了scrollBy方法後繼續調用還會繼續滾動。那麼下面能夠得出結論:public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
複製代碼
public void invalidate(int l, int t, int r, int b) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
}
複製代碼
這裏就能夠知道看到的矩形是l-scrollX,t-scrollY,r-scrollX,b-scrollY,這就是爲何scrollTo設置負值就是往正方向走,設置負值往反方向走,而且裏面加了判斷條件if(mScrollx != x || mScrollY != y),第一次調用這個方法時,x的值賦給了mScrollX,y的值賦給了mScrollY,而再後面調用這個方法由於x等於mScrollX,y等於mScrollY,所以不會執行進入條件內的代碼。源碼中scrollBy仍是調用了scrollTo方法:安全
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
複製代碼
參數是(mScrollX + x,mScrollY + y),這裏就能夠解釋,scrollTo方法只會讓View移動一次,它是對View初始方向來講,而scrollBy是對View的如今位置來講,因此能夠不斷移動。ide
佈局文件:函數
public class SlidingRuleView extends View {
//文字畫筆
private Paint paint;
//文字足夠長 超過屏幕顯示寬度 方便後面看滑動效果
private String currentNum = "1234sdddddddddd423dddddddd234dddddd234dddddd23423dddddddd234ddddd234ddddddd23423dddddd23ddd234ddddddd34334ddddddddddddddddddddddddddddddddsdddddddddddd";
//這個自定義View的高度
private int height;
public SlidingRuleView(Context context) {
this(context,null);
}
public SlidingRuleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public SlidingRuleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context){
//初始化畫筆 抗鋸齒
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
//畫筆的顏色 黑色
paint.setColor(Color.BLACK);
//設置填充樣式,只繪製圖形的輪廓
paint.setStyle(Paint.Style.STROKE);
//設置文本大小
paint.setTextSize(25f);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//MeasureSpec值由specMode和specSize共同組成,onMeasure兩個參數的做用根據specMode的不一樣,有所區別。
//當specMode爲EXACTLY時,子視圖的大小會根據specSize的大小來設置,對於佈局參數中的match_parent或者精確大小值
//當specMode爲AT_MOST時,這兩個參數只表示了子視圖當前可使用的最大空間大小,而子視圖的實際大小不必定是specSize。因此咱們自定義View時,重寫onMeasure方法主要是在AT_MOST模式時,爲子視圖設置一個默認的大小,對於佈局參數wrap_content。
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, SystemUtil.dp2px(getContext(),60));
} else {
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
//這裏獲取View的高度 方便後面繪製算一些座標
height = getMeasuredHeight();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//獲得文字的字體屬性和測量
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
//文字設置在View的中間
float y = height / 2 + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2;
//canvas繪製文本
canvas.drawText(currentNum, 0,y, paint);
}
}
複製代碼
下面重點講解onMeasure方法和繪製文本方法工具
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//MeasureSpec值由specMode和specSize共同組成,onMeasure兩個參數的做用根據specMode的不一樣,有所區別。
//當specMode爲EXACTLY時,子視圖的大小會根據specSize的大小來設置,對於佈局參數中的match_parent或者精確大小值
//當specMode爲AT_MOST時,這兩個參數只表示了子視圖當前可使用的最大空間大小,而子視圖的實際大小不必定是specSize。因此咱們自定義View時,重寫onMeasure方法主要是在AT_MOST模式時,爲子視圖設置一個默認的大小,對於佈局參數wrap_content。
if (heightSpecMode == MeasureSpec.AT_MOST) {
//這個方法肯定了當前View的大小
setMeasuredDimension(widthSpecSize, SystemUtil.dp2px(getContext(),60));
} else {
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
複製代碼
這裏是獲取specMode的模式和specSize大小,爲何肯定View的大小根據heightSpecMode呢,由於要實現的滑動捲尺只是橫向滑動,width設置精準值、wrap_content和match_parent都是能夠的,不須要處理,超過View顯示的區域到時候能夠經過滑動來顯示。實現這個效果高度通常設置wrap_content,當設置wrap_content時,最好設置一個固定高度,上面代碼設置60px,若是不進行處理的話。有可能佔滿父容器所給的高度,或者高度太小顯示不全。這裏稍微講下Measure.Mode測量模式:
UNSPECIFIED 父容器不對子View作任何限制,要多大給多大,通常用於系統內部,這裏就不用多考慮
EXACTLY 精準模式,通常View指定了具體的大小(dp/px)或者設置match_parent
AT_MOST 父容器制定了一個可用的大小,子View不能大於這個值,這個是在佈局設置wrap_content
最後還發現調用了setMeasuredDimension,這個方法主要是決定當前View的大小,onMeasure方法最後調用setMeasuredDimension方法保存測量的寬高值,固然寫在onMesure方法裏,也說明它會調用屢次,由於有的時候,一次測量,當父控件發現子控件的尺寸不符合要求就會從新測量。若是不調用這個方法,可能會產生不可預測的問題。 下面講下定位文本座標的方法:
//獲得文字的字體屬性和測量
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
//文字設置在View的中間
float y = height / 2 + (Math.abs(fontMetrics.ascent) + fontMetrics.descent) / 2;
//canvas繪製文本
canvas.drawText(currentNum, 0,y, paint);
複製代碼
x座標就不講了,這裏重點講一下y座標,這裏主要用到了FontMetrics這個類,官網解釋是:
這裏結合一張圖來說: 圖中有五條線結合官方文檔,自上而下來解釋:上面圖中紅色的圓點,那個點對於TextView來講就是基線的原點,如今問題是要肯定這個點對於這個View下的y座標,能夠看到這個點離整個View的中線下移一段距離,這段距離我是設定整個文本高度的一半,文本字體的高度能夠用Math.abs(ascent) + descent,那麼文本高度的一半也就是**(Math.abs(ascent) + descent)/ 2**。所以最終紅色原點對於整個View的y座標是float y = height / 2 + (Math.abs(fontMetrics.ascent) + fontMetrics.descent) / 2;
上面講了些基礎知識,下面講述最核心的就是滑動效果,在init方法裏建立滾動實例:
//建立滑動實例
mScroller = new Scroller(context);
複製代碼
由於滑動只是View裏面的TextView,要肯定最大的左右滑動邊界值,這裏先上一個圖,就是Android中View的座標和獲取一些距離方法:
ViewGroup就是平時一些LinearLayout,RelativeLayout佈局,View如TextView,ImageView這些控件。 View提供的獲取座標以及距離的方法:MotionEvent提供的方法:
在ondraw方法分別獲得View自身左邊距離父佈局左邊距離和View自身右邊到父佈局左邊的距離:
//獲得左右邊界
leftBorder = getLeft();
rightBorder = (int)paint.measureText(currentNum);
複製代碼
currentNum就是TextView顯示的文字內容,Paint.measureText就是測量文字的寬度。每一個View都有onTouchEvent方法,onTouchEvent有手指觸摸屏幕MotionEvent.ACTION_DOEM,MotionEvent.Action_MOVE方法,那就在這個方法實現滑動邏輯。
@Override
public boolean onTouchEvent(MotionEvent ev){
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//記錄初始觸摸屏幕下的座標
mXDown = ev.getRawX();
mLastMoveX = mXDown;
break;
case MotionEvent.ACTION_MOVE:
mCurrentMoveX = ev.getRawX();
//本次的滑動距離
int scrolledX = (int) (mLastMoveX - mCurrentMoveX);
//若是右滑時 內容左邊界超過初始化時候的左邊界 就仍是初始化時候的狀態
if(getScrollX() + scrolledX < leftBorder){
scrollTo(leftBorder,0);
}
//同理 若是左滑 這裏判斷右邊界
else if(getScrollX() + getWidth() + scrolledX > rightBorder){
scrollTo(rightBorder - getWidth(),0);
}else{
//在左右邊界中 自由滑動
scrollBy(scrolledX,0);
}
mLastMoveX = mCurrentMoveX;
break;
}
return true;
}
複製代碼
上面代碼主要最難理解的就是邊界檢測,下面是左邊界檢測代碼:
//若是右滑時 內容左邊界超過初始化時候的左邊界 就仍是初始化時候的狀態
if(getScrollX() + scrolledX < leftBorder){
scrollTo(leftBorder,0);
}
複製代碼
這裏用了getScrollX方法,這個方法是返回當前View視圖左上角X座標與View視圖初始位置左上角X座標的距離,注意,這是以屏幕座標爲參照點,View右移這個值由正變爲負數一直遞增。 這個其實列出一張圖理解:
結合上面代碼來看,一開始判斷右滑到達左邊界的時候,是經過滑動後TextView的左邊界在初始狀態時的左邊界右邊時,就是右滑達到最大值。由於這裏getScrollx取得值和咱們正常理解的值是反的:if(getScrollX() < leftBorder){
scrollTo(leftBorder,0);
}
複製代碼
這樣來判斷,這裏默認leftBorder是0,也就是父佈局的左邊界和內容左邊界一致重疊。但我發現效果會有抖動,應該是臨界值沒有判斷到位,而後加上移動距離。
getScrollX() + scrolledX < leftBorder
複製代碼
其實轉換爲天然語言就是,View的移動距離比當前View視圖左上角座標與View視圖初始位置x軸方向上的距離大,同理左滑時邊界檢測也是同樣。注意:在非左右邊界狀況下,要用scrollBy方法來移動,由於這個是對於當前View位置來講的,還有,onTouchEvent要返回return true。由於return false或者return super.onTouchEvent只會執行down方法,不會執行move和up方法,只有在true的時候,三個都會執行,具體什麼緣由自行查找事件分發和消耗。其實這裏不用重寫computerScroll方法,就是在其內部完成平滑移動,computeScroll在父控件執行drawChild時,會調用這個方法。,效果圖以下:
在attrs下添加屬性集合:
<declare-styleable name="SlidingRuleView">
<!--長刻度的長度-->
<attr name="longDegreeLine" format="dimension"/>
<!--//線條顏色-->
<attr name="lineDegreeColor" format="color" />
<!--頂部的直線距離View頂部距離-->
<attr name="topDegreeLine" format="dimension"/>
<!-- 刻度間隔-->
<attr name="lineDegreeSpace" format="dimension"/>
<!-- 刻度大數目 -->
<attr name="lineCount" format="integer"/>
</declare-styleable>
複製代碼
在構造函數讀取attrs文件下屬性:
public SlidingRuleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化一些參數
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable
.SlidingRuleView);
//刻度線的顏色
lineDegreeColor = typedArray.getColor(R.styleable.SlidingRuleView_lineDegreeColor, Color.LTGRAY);
//頂部的直線距離View頂部距離
topDegreeLine = typedArray.getDimension(R.styleable.SlidingRuleView_topDegreeLine, SystemUtil.dp2px(getContext(),45));
//刻度間隔
lineDegreeSpace = typedArray.getDimension(R.styleable.SlidingRuleView_lineDegreeSpace, SystemUtil.dp2px(getContext(),10));
//刻度大數目 默認30
lineCount = typedArray.getInt(R.styleable.SlidingRuleView_lineCount, 30);
init(context);
typedArray.recycle();
}
複製代碼
初始化方法init肯定頂部刻度線的右端:
private void init(Context context){
//初始化畫筆 抗鋸齒
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
//建立滑動實例
mScroller = new Scroller(context);
//第一步,獲取Android常量距離對象,這個類有UI中所使用到的標準常量,像超時,尺寸,距離
ViewConfiguration configuration = ViewConfiguration.get(context);
//獲取最小移動距離
mTouchMinDistance = configuration.getScaledTouchSlop();
//肯定刻頂部度長線右邊界 格數 * 之間的間隔 * 大數目(間隔)之間是有10小間隔的
rightBorder = lineDegreeSpace * lineCount * 10;
}
複製代碼
這裏解釋一下**rightBorder = lineDegreeSpace * lineCount * 10;**意思是刻度之間的間隔 * 大刻度數 * 每一個大刻度之間會有10個小刻度。 ondraw方法繪製:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//肯定頂部長線的左端
float x = leftBorder;
//肯定頂部長線
float y = topDegreeLine;
//設置畫筆顏色
paint.setColor(lineDegreeColor);
//設置刻度線寬度
paint.setStrokeWidth(3);
canvas.drawLine(x, y, rightBorder, y, paint);
}
複製代碼
這樣頂部刻度長線繪製完成:
在構造方法增長:
//長的刻度線條長度
longDegreeLine = typedArray.getDimension(R.styleable.SlidingRuleView_longDegreeLine, SystemUtil.dp2px(getContext(),35));
複製代碼
在onDraw方法裏添加:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//肯定頂部長線的左端
float x = leftBorder;
//肯定頂部長線
float y = topDegreeLine;
//設置畫筆顏色
paint.setColor(lineDegreeColor);
//設置刻度線寬度
paint.setStrokeWidth(3);
canvas.drawLine(x, y, rightBorder, y, paint);
//循環繪製
for(int i = 0;i <= lineCount * 10;i++){
//畫長刻度
if(i % 10 == 0){
paint.setColor(lineDegreeColor);
paint.setStrokeWidth(5);
canvas.drawLine(x, y, x, y + longDegreeLine, paint);
}
x += lineDegreeSpace;
}
}
複製代碼
循環繪製裏,我這邊是循環全部的刻度值,可是如今只繪製長刻度,所以i % 10 == 0的時候才繪製,由於繪製是從左往右的,每一個刻度值的間隔是用lineDegreeSpace表示,所以每循環一遍,X座標的值要對應增長x += lineDegreeSpace。 運行效果以下:
發現左右邊界的刻度線太靠邊了,加上左右間隔:<!--刻度尺左邊界記錄View左邊界的距離-->
<attr name="ruleLeftSpacing" format="dimension" />
<!--刻度尺右邊界記錄View右邊界的距離-->
<attr name="ruleRightSpacing" format="dimension" />
複製代碼
構造方法增長:
ruleLeftSpacing = typedArray.getDimension(R.styleable.SlidingRuleView_ruleLeftSpacing, SystemUtil.dp2px(getContext(),5));
ruleRightSpacing = typedArray.getDimension(R.styleable.SlidingRuleView_ruleRightSpacing, SystemUtil.dp2px(getContext(),5));
複製代碼
初始化init方法變成:
//增長左邊界距離
leftBorder = ruleLeftSpacing;
//肯定刻頂部度長線右邊界 格數 * 之間的間隔 * 大數目(間隔)之間是有10小間隔的
rightBorder = lineDegreeSpace * lineCount * 10+ ruleLeftSpacing + ruleRightSpacing;
複製代碼
ondraw繪製頂部長線變爲:
canvas.drawLine(x, y, rightBorder - ruleRightSpacing, y, paint);
複製代碼
onTouchEvent方法左右檢測須要加上左右邊距
@Override
public boolean onTouchEvent(MotionEvent ev){
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//記錄初始觸摸屏幕下的座標
mXDown = ev.getRawX();
mLastMoveX = mXDown;
break;
case MotionEvent.ACTION_MOVE:
mCurrentMoveX = ev.getRawX();
//本次的滑動距離
int scrolledX = (int) (mLastMoveX - mCurrentMoveX);
//若是右滑時 內容左邊界超過初始化時候的左邊界 就仍是初始化時候的狀態
if(getScrollX() + scrolledX < leftBorder){
scrollTo((int)(-leftBorder),0);
return true;
}
//同理 若是左滑 這裏判斷右邊界
else if(getScrollX() + getWidth() + scrolledX > rightBorder){
scrollTo((int)(rightBorder - getWidth() + ruleRightSpacing),0);
return true;
}else{
//左右邊界中 自由滑動
scrollBy(scrolledX,0);
}
//當中止滑動時,如今的滑動已經變成上次滑動
mLastMoveX = mCurrentMoveX;
break;
}
return true;
}
複製代碼
運行效果以下:
在attrs文件下添加數字的顏色和大小:
<!--//數字顏色-->
<attr name="numberColor" format="color" />
<!--//數字大小-->
<attr name="numberSize" format="dimension" />
複製代碼
在構造方法增長對attrrs屬性獲取:
//數字顏色
numberColor = typedArray.getColor(R.styleable.SlidingRuleView_numberColor, Color.BLACK);
//數字大小
numberSize = typedArray.getDimension(R.styleable.SlidingRuleView_numberSize, SystemUtil.dp2px(getContext(),15));
複製代碼
onDraw方法繪製數字:
//畫刻度值
String number = String.valueOf(i / 10);
//獲得文字寬度
float textWidth = paint.measureText(number);
//繪製顏色
paint.setColor(numberColor);
//繪製文字大小
paint.setTextSize(numberSize);
paint.setStrokeWidth(1);
canvas.drawText(number, x - textWidth / 2, y + longDegreeLine + SystemUtil.dp2px(getContext(),25), paint);
複製代碼
這裏主要講下,數字的座標,由於數字是在刻度線正下方的,因此x座標應該是刻度線的x座標減去自己自身寬度的一半,y座標應該是刻度線長度再加上部分距離,我這邊是加了25dp。 運行效果:
在attrs文件增長短刻度的長度:
<!--短刻度值的長度-->
<attr name="shortDegreeLine" format="dimension"/>
複製代碼
在構造函數獲取其屬性:
//短刻度值的長度
shortDegreeLine = typedArray.getDimension(R.styleable.SlidingRuleView_shortDegreeLine, SystemUtil.dp2px(getContext(),20));
複製代碼
在onDraw方法循環裏非i%10==0的狀況下繪製,這裏很好理解:
//循環繪製
for(int i = 0;i <= lineCount * 10;i++){
//畫長刻度
if(i % 10 == 0){
paint.setColor(lineDegreeColor);
paint.setStrokeWidth(5);
canvas.drawLine(x, y, x, y + longDegreeLine, paint);
//畫刻度值
String number = String.valueOf(i / 10);
//獲得文字寬度
float textWidth = paint.measureText(number);
//繪製顏色
paint.setColor(numberColor);
//繪製文字大小
paint.setTextSize(numberSize);
paint.setStrokeWidth(1);
canvas.drawText(number, x - textWidth / 2, y + longDegreeLine + SystemUtil.dp2px(getContext(),25), paint);
}else {
//畫短刻度
paint.setColor(lineDegreeColor);
paint.setStrokeWidth(3);
canvas.drawLine(x, y, x, y + shortDegreeLine, paint);
}
x += lineDegreeSpace;
}
複製代碼
運行效果圖以下:
綠色指針底部實際上是半圓的,能夠在drawable下創建shape文件,經過bitmap繪製,如:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="#CCCCCC"/>
<corners android:bottomLeftRadius="0px" android:bottomRightRadius="30dp" android:topLeftRadius="0px" android:topRightRadius="30dp"/>
</shape>
複製代碼
我這裏爲了方便,直接用直線來代替,在attrs文件下增長綠色指針顏色和其寬度:
<!--指針寬度-->
<attr name="greenPointWidth" format="dimension"/>
<!--指針顏色-->
<attr name="greenPointColor" format="color"/>
複製代碼
在構造方法添加獲取顏色,粗細:
//綠色指針粗細
greenPointWidth = typedArray.getDimension(R.styleable.SlidingRuleView_greenPointWidth, SystemUtil.dp2px(getContext(),4));
//綠色指針顏色
greenPointColor = typedArray.getColor(R.styleable.SlidingRuleView_greenPointColor, 0xFF4FBA75);
複製代碼
由於綠色指針永遠是在View的中間,在onMeasure方法獲取X座標:
//綠色指針的x座標
greenPointX =getMeasuredWidth() / 2;
複製代碼
在onDraw方法繪製指針:
//畫指針
paint.setColor(greenPointColor);
paint.setStrokeWidth(greenPointWidth);
canvas.drawLine(greenPointX + getScrollX(), y, greenPointX + getScrollX(), y + longDegreeLine + SystemUtil.dp2px(getContext(),3),
paint);
複製代碼
這裏x座標爲何要加上getScrollx(刻度尺的偏移量)呢,由於要保持指針在View的中間位置,不加上的話,指針會隨着刻度移動而移動。
在attrs文件添加刻度值的顏色和大小,同理在構造函數獲取屬性,這裏就很少講來,由於數字是保留一位的,我這裏用到了DecimalFormat來格式化數字:
//數字小數點一位
df = new DecimalFormat("0.0");
複製代碼
在onDraw繪製:
//繪製當前刻度值
//畫當前刻度值
paint.setColor(currentNumberColor);
//設置大小
paint.setTextSize(currentNumberSize);
//肯定數字的值。用移動多少來肯定
currentNum = df.format((greenPointX + getScrollX() - leftBorder) / (lineDegreeSpace * 10.0f));
//測量數字寬度
float textWidth = paint.measureText(currentNum);
canvas.drawText(currentNum, greenPointX - textWidth / 2 + getScrollX(), topDegreeLine - SystemUtil.dp2px(getContext(),15), paint);
複製代碼
這裏說一下肯定數值的方法:
//肯定數字的值。用移動多少來肯定
currentNum = df.format((greenPointX + getScrollX() - leftBorder) / (lineDegreeSpace * 10.0f));
複製代碼
greenPointX + getScrollX() - leftBorder這條公式是肯定指針到刻度尺最左邊的距離是多少,再除以大刻度(每一個大刻度有10個小刻度)的距離,就能夠得出指針所指的刻度。肯定數字的x座標和y座標就不作解釋,很容易理解,由於數字是在刻度尺上面,因此要減去一些距離。
繪製kg無非是在當前刻度值右邊,字體小一點,左右移動先不實現了:
//畫kg 大小是刻度值的3分之一
paint.setTextSize(currentNumberSize / 3);
canvas.drawText("kg", greenPointX + textWidth / 2 + getScrollX() + SystemUtil.dp2px(getContext(),3), topDegreeLine - SystemUtil.dp2px(getContext(),30), paint);
複製代碼
相比當前刻度值而言,離刻度尺的距離要比當前刻度值要大,我這裏減去30dp。 最終效果:
效果和薄荷健康很相似吧,可是這裏注意,由於刻度值只是設置一位小數,也就是綠色指針不能移到兩個小刻度之間,下面處理一下,在觸摸方法,增長up方法,也就是當手指擡起時,若是指針滑到兩個刻度值之間,就將綠色指針移動最近的刻度值。private void moveRecently(){
float distance = (greenPointX + getScrollX() - leftBorder) % lineDegreeSpace;
//指針的位置在小刻度中間位置日後(右)
if (distance >= lineDegreeSpace / 2) {
scrollBy((int) (lineDegreeSpace - distance), 0);
} else {
scrollBy((int) (-distance), 0);
}
}
複製代碼
注意這裏:
(greenPointX + getScrollX() - leftBorder) % lineDegreeSpace;
複製代碼
這裏是取餘操做,這裏是肯定指針在小刻度之間的具體位置,若是結果小於間隔的一半,那向後(右)最近的刻度移動,若是大於間隔的一半,那就向前(左)最近的刻度移動。到這裏發現,指針不能指向0或者最後的位置,由於綠色指針在View的中間,那麼左右邊界檢測須要改變,須要左邊界減去上View的寬度一半,右邊界須要加上View寬度的一半。
@Override
public boolean onTouchEvent(MotionEvent ev){
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//記錄初始觸摸屏幕下的座標
mXDown = ev.getRawX();
mLastMoveX = mXDown;
break;
case MotionEvent.ACTION_MOVE:
mCurrentMoveX = ev.getRawX();
//本次的滑動距離
int scrolledX = (int) (mLastMoveX - mCurrentMoveX);
//若是右滑時 內容左邊界超過初始化時候的左邊界 就仍是初始化時候的狀態
if(getScrollX() + scrolledX < leftBorder - getWidth() / 2){
scrollTo((int)(- getWidth() / 2 +leftBorder),0);
return true;
}
//同理 若是左滑 這裏判斷右邊界
else if(getScrollX() + getWidth() / 2 + scrolledX > rightBorder){
scrollTo((int)(rightBorder - getWidth() /2 - ruleRightSpacing),0);
return true;
}else{
//左右邊界中 自由滑動
scrollBy(scrolledX,0);
}
mLastMoveX = mCurrentMoveX;
break;
case MotionEvent.ACTION_UP:
moveRecently();
break;
}
return true;
}
private void moveRecently(){
float distance = (greenPointX + getScrollX() - leftBorder) % lineDegreeSpace;
//指針的位置在小刻度中間位置日後(右)
if (distance >= lineDegreeSpace / 2) {
scrollBy((int) (lineDegreeSpace - distance), 0);
} else {
scrollBy((int) (-distance), 0);
}
}
複製代碼
最終效果以下圖:
發現這裏沒加慣性滑動,滑動很艱難,下面添加速度追蹤器:
/** * 監控手勢速度類 */
private VelocityTracker mVelocityTracker;
//慣性最大最小速度
protected int mMaximumVelocity, mMinimumVelocity;
複製代碼
在初始化方法獲取最大滑動速度,最小滑動速度:
//添加速度追蹤器
mVelocityTracker = VelocityTracker.obtain();
//獲取最大速度
mMaximumVelocity = ViewConfiguration.get(context)
.getScaledMaximumFlingVelocity();
//獲取最小速度
mMinimumVelocity = ViewConfiguration.get(context)
.getScaledMinimumFlingVelocity();
複製代碼
在觸摸事件onTouchEvent初始化mVelocityTracker
mVelocityTracker.addMovement(ev);
複製代碼
@Override
public boolean onTouchEvent(MotionEvent ev){
mVelocityTracker.addMovement(ev);
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//記錄初始觸摸屏幕下的座標
mXDown = ev.getRawX();
mLastMoveX = mXDown;
break;
case MotionEvent.ACTION_MOVE:
mCurrentMoveX = ev.getRawX();
//本次的滑動距離
int scrolledX = (int) (mLastMoveX - mCurrentMoveX);
//左右邊界中 自由滑動
scrollBy(scrolledX,0);
mLastMoveX = mCurrentMoveX;
break;
case MotionEvent.ACTION_UP:
//處理鬆手後的Fling 獲取當前事件的速率,1毫秒運動了多少個像素的速率,1000表示一秒
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
//獲取橫向速率
int velocityX = (int) mVelocityTracker.getXVelocity();
//滑動速度大於最小速度 就滑動
if (Math.abs(velocityX) > mMinimumVelocity) {
fling(-velocityX);
}
//刻度之間檢測
moveRecently();
break;
case MotionEvent.ACTION_CANCEL:
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return true;
}
複製代碼
發現Move方法調用scrollBy方法,ACTION_UP增長了速度速率判斷邏輯,最後調用了fling方法:
private void fling(int vX) {
mScroller.fling(getScrollX(), 0, vX, 0,(int)(- rightBorder), (int)rightBorder, 0, 0);
}
複製代碼
當用戶手指快速劃過屏幕,手指快速離開屏幕時,系統會斷定用戶執行一個Fling手勢,視圖會快速滾動,而且在手指離開屏幕以後也會滾動必定時間。
/**
* Start scrolling based on a fling gesture. The distance travelled will
* depend on the initial velocity of the fling.
*
* @param startX Starting point of the scroll (X)
* @param startY Starting point of the scroll (Y)
* @param velocityX Initial velocity of the fling (X) measured in pixels per
* second.
* @param velocityY Initial velocity of the fling (Y) measured in pixels per
* second
* @param minX Minimum X value. The scroller will not scroll past this
* point.
* @param maxX Maximum X value. The scroller will not scroll past this
* point.
* @param minY Minimum Y value. The scroller will not scroll past this
* point.
* @param maxY Maximum Y value. The scroller will not scroll past this
* point.
*/
複製代碼
Scroller的fling函數就是基於手勢滑動,參數的意思:
增長computeScroll,這個方法在fling或者startScroll方法,調用invalidate方法後執行的函數,並在裏面增長刻度邊界的檢測,完成平滑移動:
@Override
public void computeScroll() {
// 第三步,重寫computeScroll()方法,並在其內部完成平滑滾動的邏輯
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//這是最後mScroller的最後一次滑動 進行刻度邊界檢測
if(!mScroller.computeScrollOffset()){
moveRecently();
}
}
}
複製代碼
最後重寫scrollTo方法,加刻度尺滑動左右邊界檢測:
//重寫滑動方法,設置到邊界的時候不滑,並顯示邊緣效果。滑動完輸出刻度。
@Override
public void scrollTo( int x, int y) {
//左邊界檢測
if (x < leftBorder - getWidth() / 2) {
x = (int)(- getWidth() / 2 +leftBorder);
}
//有邊界檢測
if (x + getWidth() / 2> rightBorder) {
x = (int)(rightBorder - getWidth() /2 - ruleRightSpacing);
}
if (x != getScrollX()) {
super.scrollTo(x, y);
}
}
複製代碼
最終運行效果以下:
每一次練習,小案例都是知識的鞏固和提高。 Demo連接