前几天写了一篇一步一步教你实现即刻点赞效果后,实现点赞效果主要是本身对自定义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
<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
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的坐标和获取一些距离方法:
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右移这个值由正变为负数一直递增。 这个其实列出一张图理解:
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。 最终效果:
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连接