大家好,这是我的第一篇文章。
其实写博客的想法在去年的这个时候就有了,但是一直没有实践,直到今天,我写了一份2017年规划(这规划是迟了一点),写博客就在规划之中。通过写博客记录自己的心得、练习写作能力and大家相互交流一哈。
好了,废话不多说。下面开始本篇的正题。先看效果图
效果比较简单,我没有画刻度值,我们主要讲的是其实现原理。
我们的突破点有:
* 只有蓝色矩形块之内的文字是白色的,一个文字可能有一半是白色
* 手指离开屏幕后,会以一定速度继续滑动
* 当滑出边界时,有回弹效果
* 如何通过滑动的距离获得刻度值
文字颜色的处理
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawLines(canvas); drawRect(canvas); drawTexts(canvas); }
onDraw方法中,drawLines是画两条横线,drawRect是画中间的蓝色矩形,这两个方法在这里就不多说了,比较简单。我们看以下drawTexts方法
private void drawTexts(Canvas canvas) { // 默认选中最小值,成员变量mTranslation维护滑动以及text显示位置 Rect bounds = new Rect(); float startX = getMeasuredWidth() * 1f / 2;//+ mTextSpace; for (int j = minValue; j <= maxValue; j++) { String str = j + ""; mPaints[0].getTextBounds(str, 0, str.length(), bounds); float width = bounds.width(); float height = bounds.height(); drawText(canvas, str, startX - width / 2 - mTranslation, startX + width / 2 - mTranslation, getMeasuredHeight() * 1f / 2 + height / 2); startX += mTextSpace; } }
在drawTexts中,临时变量bounds作用是计算文字的长度和高度,使用方法如下:
Rect bounds = new Rect(); mPaints[0].getTextBounds(str, 0, str.length(), bounds); int textWidth = bounds.width(); int textHeight = bounds.height();
因为默认选中最小值,所以for循环从最小值开始画,每画一次startX自加mTextSpace,mTextSpace是文字中心距。画文字调用私有方法private void drawText(Canvas canvas, String text, float textLeft, float textRight, float textBottom)
,在看drawText方法之前先看看如何实现一个文字两种颜色。
原理就是一个文字画两次,比如要画文字”擦”,我们想让”擦”的左半边是蓝色,右半边是红色,我们只需要先用蓝色的画笔画”擦”,并且通过调用canvas的clipRect方法让其只显示左边边,然后用红色画笔再画一遍,调用canvas的clipRect方法让其只显示右半边。
这里再贴一个简单例子:
String str = "我是谁,我来自哪里?"; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Rect bounds = new Rect(); mPaints[0].getTextBounds(str, 0, str.length(), bounds); drawLeft(canvas, 0, bounds.width() / 2); drawRight(canvas, bounds.width() / 2, getMeasuredWidth()); } private void drawRight(Canvas canvas, int startX, int endX) { drawText(canvas, Color.GREEN, startX, endY); } private void drawLeft(Canvas canvas, int startX, int endY) { drawText(canvas, Color.RED, startX, endY); } private void drawText(Canvas canvas, int color, int startX, int endX) { mPaints[0].setColor(color); canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(startX, 0, endX, getMeasuredHeight()); canvas.drawText(str, 0, getMeasuredHeight() - 10, mPaints[0]); canvas.restore(); }
运行结果如下:
一个文字两种颜色的实现讲完了,回到本篇的主题,看看drawText方法
private void drawText(Canvas canvas, String text, float textLeft, float textRight, float textBottom) { // 画带透明度的文字 if (textLeft >= mRect.right || textRight <= mRect.left) { mPaints[0].setColor(0x7dffffff); canvas.drawText(text, textLeft, textBottom, mPaints[0]); } else { if (textLeft < mRect.left && textRight > mRect.left) { canvas.save(Canvas.CLIP_SAVE_FLAG); mPaints[0].setColor(0x7dffffff); canvas.clipRect(textLeft, 0, mRect.left, getMeasuredHeight()); canvas.drawText(text, textLeft, textBottom, mPaints[0]); canvas.restore(); canvas.save(Canvas.CLIP_SAVE_FLAG); mPaints[0].setColor(0xffffffff); canvas.clipRect(mRect); canvas.drawText(text, textLeft, textBottom, mPaints[0]); canvas.restore(); } else if (textLeft < mRect.right && textRight > mRect.right) { canvas.save(Canvas.CLIP_SAVE_FLAG); mPaints[0].setColor(0xffffffff); canvas.clipRect(mRect); canvas.drawText(text, textLeft, textBottom, mPaints[0]); canvas.restore(); canvas.save(Canvas.CLIP_SAVE_FLAG); mPaints[0].setColor(0x7dffffff); canvas.clipRect(mRect.right, 0, textRight + 10, getMeasuredHeight()); canvas.drawText(text, textLeft, textBottom, mPaints[0]); canvas.restore(); } else { mPaints[0].setColor(0xffffffff); canvas.drawText(text, textLeft, textBottom, mPaints[0]); } } }
这里贴出了drawText的所有代码,代码比较长,但逻辑比较简单,先从第一个if看起
第一个if (textLeft >= mRect.right || textRight <= mRect.left){…}表示文字在矩形之外,mRect是矩形的区域。如果是在矩形之外,给画笔设置颜色为非纯白,画一次text。else表示文字在矩形之内,这里面又分为三种情况。第一,文字左半边在矩形之外,右半边在矩形之内(左外右内);第二,文字左半边在矩形之内,右半边在矩形之外(左内右外),第三,文字全部在矩形之内(内内)。
左外右内:if (textLeft < mRect.left && textRight > mRect.left) {…}
先画一遍暗色文字,裁剪区域为文字的左侧到矩形框的左侧,再画一遍亮色文字,裁剪区域为矩形框左侧到文字右侧;
左内右外:else if(textLeft < mRect.right && textRight > mRect.right){…}
先画一遍亮色文字,裁剪区域为文字左侧到矩形框右侧,再画一遍暗色文字,裁剪区域为矩形框右侧到文字右侧;
内内:else{…} 文字全部在矩形框内,只需要画一遍亮色文字
运行效果应该是这个样子的,但是还不能滑动
滑动距离和实际值的计算
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); initRect(); mValuePrePixel = 1f / mTextSpace; }
mValuePrePixel表示每个像素代表的值,在本例中,mTextSpace为文字中心距,而两个文字之前的实际值是1,所以mValuePrePixel = 1f / mTextSpace;那么计算当前值就是
mCurrValue = minValue + mTranslation * mValuePrePixel;
上面提到,mTranslation维护滑动。那么,在滑动过程中,当手指向左滑时,mTranslation = 手指滑过的距离,当手指向右滑时,mTranslation = - 手指滑过的距离,当前值通过上面的公式可以计算。
支持滑动和惯性滑动
// 手指按下时的x位置 private float down_x; // 记录手指按下时mTranslation的值 float tempTranslation = 0; public boolean onTouchEvent(MotionEvent event) { mGestureDetector.onTouchEvent(event); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: down_x = event.getX(); tempTranslation = mTranslation; break; case MotionEvent.ACTION_MOVE: float x = event.getX(); float temp = down_x - x; // 这个2表示最多可以让滑动超过2个值的长度 if (tempTranslation + temp > (maxValue - minValue + 2) / mValuePrePixel || tempTranslation + temp < + -2 / mValuePrePixel) { return false; } mTranslation = tempTranslation + temp; mCurrValue = minValue + mTranslation * mValuePrePixel; if (mListener != null) { mListener.onValueChangedOnscrolling((int(mCurrValue * 10) / 10f); } invalidate(); break; case MotionEvent.ACTION_UP: checkIsOverScan(); break; } return super.onTouchEvent(event); }
mGestureDetector.onTouchEvent(event);是接管滑动事件,处理惯性滑动,这个稍后再看,先看看onTouchEvent中都做了什么
1.一般滑动
在Down(手指按下的一瞬间)中,记录手指按下的x值和当前mTranslation值;
在Move(手指滑动)中,计算滑过的距离temp,通过temp计算mTranslation的变化以及mCurrValue的值。
在Up中,检查当手指抬起时,当前选中的值如果小于最小值,或者大于最大值,就应该让尺子回滚到最小值或者最大值。在checkIsOverScan()中实现。
2.惯性滑动
public SpeedInclineView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initPaints(); setFocusable(true); setLongClickable(true); mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // 手指向左滑是负的,手指向右滑是正的 if (Math.abs(velocityX) > 2000f) { if (velocityX > 0) { mVelocityX = 2000f; } else { mVelocityX = -2000f; } } else { mVelocityX = velocityX; } postDelayed(mScrollRunnable, 10); return true; } }); mGestureDetector.setIsLongpressEnabled(false); }
Runnable mScrollRunnable = new Runnable() { @Override public void run() { float len = mVelocityX / 100; if (mVelocityX > 0) { if (mVelocityX > 30) { mVelocityX -= 30f; } else { mVelocityX = 0; } } else { if (mVelocityX < -30f) { mVelocityX += 30f; } else { mVelocityX = 0; } } mTranslation -= len; mCurrValue -= len * mValuePrePixel; if (mListener != null) { mListener.onValueChangedOnscrolling((int) (mCurrValue * 10) / 10f); } invalidate(); if (mVelocityX != 0 && !mIsCheck && mCurrValue > minValue - 2 && mCurrValue < maxValue + 2) { postDelayed(mScrollRunnable, 10); } else { mVelocityX = 0; checkIsOverScan(); } } };
在构造方法中创建GestureDetector对象,重写onFling方法,当手指离开屏幕,并且在x轴或者y轴方向速度不为0时,回调onFling方法。在onFling中,我们让速度最大值为2000,在mScrollRunnable的run方法中,len = mVelocityX/100作为惯性滑过的像素,如果mVelocityX为正值,说明手指是向右滑动,每次给mVelocityX减30,mTranslation应该变小,相反,每次执行run方法mVelocityX加30,mTranslation应该增大, mValuePrePixel为每个像素代表的值,len * mValuePrePixel即为实际值的增量。最后再刷新View并且延迟10ms继续执行mScrollRunnable,直到不满足下面这个条件
if (mVelocityX != 0 && !mIsCheck && mCurrValue > minValue - 2 && mCurrValue < maxValue + 2)
mVelocityX 表示横向速度,mIsCheck表示checkIsOverScan()是否正在执行,mCurrValue > minValue - 2 和 mCurrValue < maxValue + 2表示当前值超过边界两个值了。
当不满足这个条件时,调用checkIsOverScan()方法,检查和处理超出边界两个实际值的情况(回滚)
超出边界后回滚
/** * 判断是否超过边界,如果超过需要做处理 */ private void checkIsOverScan() { // 需要对 超过最大值或者小于最小值做处理,让其回弹到最大值或者最小值 if (mTranslation > (maxValue - minValue) / mValuePrePixel) { mIsCheck = true; ValueAnimator anim = ValueAnimator.ofFloat(mTranslation, (maxValue - minValue) / mValuePrePixel); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mTranslation = (float) animation.getAnimatedValue(); invalidate(); } }); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (mListener != null) { mListener.onValueChangedOnScrollEnd(maxValue); } mIsCheck = false; } }); anim.setDuration(300); anim.start(); } else if (mTranslation < 0) { mIsCheck = true; ValueAnimator anim = ValueAnimator.ofFloat(mTranslation, 0); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mTranslation = (float) animation.getAnimatedValue(); invalidate(); } }); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (mListener != null) { mListener.onValueChangedOnScrollEnd(minValue); } mIsCheck = false; } }); anim.setDuration(300); anim.start(); } else { mIsCheck = false; if (mListener != null && mVelocityX == 0) { mListener.onValueChangedOnScrollEnd((int) (mCurrValue * 10) / 10f); } } }
这部分比较简单,第一个if表示mTranslation超过了mTranslation的最大值,开启属性动画,让mTranslation从当前值变化到最大实际值对应的mTranslation,第二个if表示mTranslation小于最小实际值对应的mTranslation,开启属性动画,让mTranslation从当前mTranslation变化到最小实际值对应的mTranslation,最后的else表示mTranslation没有超出边界,不做处理。
通过设置当前值,让尺子滚动到指定位置
比如设置的实际值为value,那么目标mTranslation的值应该为float dstTranslation = (value - minValue) * mTextSpace;
那么思路就很明确了,跟上面checkIsOverScan()方法中的原理一样。还是开启一个属性动画,让mTranslation从当前值变化到dstTranslation,下面贴出代码
public void setTranslation(float translation) { mTranslation = translation; invalidate(); } public float getTranslation() { return mTranslation; } /** * 设置当前值,让数字滚动到指定位置 * * @param value */ public void setValue(float value) { if (value < minValue || value > maxValue) { return; } float srcTranslation = mTranslation; float dstTranslation = (value - minValue) * mTextSpace; ObjectAnimator.ofFloat(this, "translation", srcTranslation, dstTranslation).setDuration(200).start(); }
可以看到这里我们使用ObjectAniator,使用ObjectAnimator代码简洁了许多,但是需要注意,使用ObjectAnimator需要提供对应的set属性方法,比如这里操作的属性是translation,那么必须提供public void setTranslation(float translation)
方法,如果我们的setValue方法向下面这样写,还需要提供get属性方法
public void setValue(float value) { if (value < minValue || value > maxValue) { return; } float dstTranslation = (value - minValue) * mTextSpace; ObjectAnimator.ofFloat(this, "translation",dstTranslation).setDuration(200).start(); }
这种写法会默认通过get方法获取当前mTranslation作为动画开始值。
这里建议无论哪种写法,最好同时提供get、set方法。
最后,再贴上回调
public interface OnValueChangeListener { /** * 滑动时value的变化 */ void onValueChangedOnMove(float value); /** * 手指抬起来时 value的值 * * @param value */ void onValueChangedOnUp(float value); } public void setOnValueChangeListener(OnValueChangeListener l) { mListener = l; }
通过给View设置OnValueChangeListener来监听值的变化。
使用
mTextView = (TextView) findViewById(R.id.text); mSpeedInclineView = (SpeedInclineView) findViewById(R.id.speed_incline_view); mSpeedInclineView.setOnValueChangeListener(new SpeedInclineView.OnValueChangeListener() { @Override public void onValueChangedOnScrolling(float value) { } @Override public void onValueChangedOnScrollEnd(float value) { mTextView.setText(value + ""); } });
回调中value的值保留小数点后一位,前面的代码中也能看得出来,使用很简单就不多说了。这里再多说一点,如何让View只选中整数呢?其实也很简单,看代码
mSpeedInclineView.setOnValueChangeListener(new SpeedInclineView.OnValueChangeListener() { @Override public void onValueChangedOnScrolling(float value) { } @Override public void onValueChangedOnScrollEnd(float value) { int inclineValue = (int) (value + 0.5f); mSpeedInclineView.setValue(inclineValue); mTextView.setText(inclineValue + ""); } });
int inclineValue = (int) (value + 0.5f)取当前值最接近的整数
mSpeedInclineView.setValue(inclineValue);让view滚动到该整数位
mTextView.setText(inclineValue + “”);设置TextView文字
好了,断断续续终于写完了,中途Chrome一直挂不知道什么原因,重启电脑还是不行,无奈只能切换到Safari了。百度代码太长如何让代码滚动,结果搜出来的结果都是如何让代码不滚动。
作者:kriouky