自定义一个滑动选择器首先需要自定义一个Java类,
在这里将其命名为TestScroller,让其继承View,实现所有的构造函数,如下图
public class TestScroller extends View{
public TestScroller(Context context) {
super(context);
}
public TestScroller(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public TestScroller(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TestScroller(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
要实现滑动选择,需要重写两个方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
onDraw()会在View需要绘制的时候执行,例如TextView的字都是在这里绘制的。View显示和移动都是onDraw()的结果,只是由于绘制速度太快,才会给人移动的感觉。执行 invalid()后,onDraw()就会立即执行一次。我们可以通过调用 invalid(),来手动重绘。
onTouchEvent()方法将会在你的手指触碰到控件的时候执行,然后我们调用onDraw(),实现View的滑动。
首先我们先完成开头图片的数字,当然,是不能滑动的。
Canvas是画布,既然是画布,那肯定还需要一个画笔Paint。有了画布和画笔之后,我们才能开始“作画”。
首先我们需要初始化好我们需要的东西:
private void init(){
mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);//是使位图抗锯齿的标志
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTextSize(mTextSize);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mPaint.setColor(getContext().getColor(R.color.black));
}
}
然后把init()放到每一个构造函数里。
然后我们还需要获取View的宽高,因为我们要据此确定绘制的位置。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth=getMeasuredWidth();
mHeight=getMeasuredHeight();
}
然后我们就可以在onDraw()里绘制数字
@Override
protected void onDraw(Canvas canvas) {
for (int i=0;i<=10;i++){
canvas.drawText(String.valueOf(i),mWidth/2,mTextSize+i*mDistance,mPaint);
}
}
drawText四个参数分别为
1.绘制的字符
2.字符的X坐标
3.字符的baseline
4.画笔
mDistance则是每个字符之间的上下距离,最后的结果就是
接下来我们就开始实现滑动功能。这次我们需要在onTouchEvent里进行操作。
函数的返回值如果为true则表示,这次触摸事件由该控件处理了,就不会传递给父布局处理。
参数MotionEven则包含各种关于触摸点的信息,在这里我们只需要了解以下几个方法。
1.getActionMasked()。该函数主要有以下三个返回值
·MotionEvent.ACTION_DOWN 按下
·MotionEvent.ACTION_MOVE 移动
·MotionEvent.ACTION_DOWN 抬起
2.getRawX,getRawY。可以获得触摸点相对于屏幕的x,y坐标
然后我们就可以开始了。
首先定义两个属性。
·mMoveLength
手指滑动的距离
·mLastY
手指上一个所在的Y坐标,如果手指从Y坐标 5 移到了 10,那么当手指位置在10的时候,此时mLastY==5;
然后我们在onTouchEvent中进行switch判断
1.ACTION_DOWN,按下手指
mLastY=event.getRawY(),记录下第一次落点的Y坐标r
2.ACTION_MOVE,移动手指
mMoveLength+=event.getRawY()-mLastY,记录下移动的距离,同时通过调用invalid(),进行重绘。在onDraw()方法里,我们修改为
for (int i=0;i<=10;i++){
canvas.drawText(String.valueOf(i),mWidth/2,mTextSize+i*mDistance+mMoveLength,mPaint);
}
在第三个参数内,加入了mMoveLength。
3.ACTION_UP,抬起手指
这里暂时还不需要操作,后面才需要用到。
最后的效果如图
目前为止的代码如下
public class TestScroller extends View{
private Paint mPaint;
private int mWidth;
private int mHeight;
private float mTextSize=60;
private int mDistance=200;
private float mMoveLength;
private float mLastY;
public TestScroller(Context context) {
super(context);
init();
}
public TestScroller(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public TestScroller(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TestScroller(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth=getMeasuredWidth();
mHeight=getMeasuredHeight();
}
@Override
protected void onDraw(Canvas canvas) {
for (int i=0;i<=10;i++){
canvas.drawText(String.valueOf(i),mWidth/2,mTextSize+i*mDistance+mMoveLength,mPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()){
case MotionEvent.ACTION_DOWN:
mLastY=event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
mMoveLength+=event.getRawY()-mLastY;
mLastY=event.getRawY();
invalidate();
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
private void init(){
mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTextSize(mTextSize);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mPaint.setColor(getContext().getColor(R.color.black));
}
}
}
然后我们就需要实现自动归位,首先我们需要添加一个圆,如果数字进入圆后,我们松开手指,那么该数字就会自动进入圆心位置。
首先先让我们画出一个圆,这需要用到canvas.drawCircle(),四个参数分别为
圆心的X,Y坐标,半径和画笔,在onDraw里添加
mPaint.setStyle(Paint.Style.STROKE);//中空的线
canvas.drawCircle(mWidth/2,mTextSize+4*mDistance,mDistance/2,mPaint);
位置半径可以自行确定
结果如下:
自动归位的原来很简单,首先是要判断哪个数字离圆心更近,然后通过Timer,每10ms执行一次重新绘制,从而达到自动回到圆心的效果,而这都需要在上面的onTouchEvent中的ACTION_UP条件下执行。
首先我们将数字都存到一个mData集合中,在init()方法中初始化数据。添加一个int属性mSelected,代表圆心数字在集合中的位置,在init()中初始化为3。
然后我们需要将绘制数字分成两个方法 drawData()和drawOtherData()。drawData()绘制圆心的数字,,drawOtherData()绘制上下的数字。为了便于直观的感受,圆心的数字将会更大一些。
@Override
protected void onDraw(Canvas canvas) {
drawData(canvas);
drawOtherData(canvas,mSelected,DRAW_UP);
drawOtherData(canvas,mSelected,DRAW_DOWN);
mPaint.setStyle(Paint.Style.STROKE);//中空的线
canvas.drawCircle(mWidth/2,mTextSize+3*mDistance,mDistance/2,mPaint);
}
private void drawData(Canvas canvas){
mPaint.setTextSize(mCircleTextSize);
canvas.drawText(String.valueOf(mData.get(mSelected)), mWidth/2,
mTextSize+3*mDistance+mMoveLength,mPaint);
}
private void drawOtherData(Canvas canvas,int mSelectedPosition,int type){
mPaint.setTextSize(mTextSize);
if (type==1){
for (int i=mSelectedPosition+1;i=0;i--){
canvas.drawText(String.valueOf(mData.get(i)), mWidth/2,
mTextSize+(3+i-mSelectedPosition)*mDistance+mMoveLength,mPaint);
}
}
}
由于圆是固定的,所以drawData里的Y坐标也是相对固定的,只是会随着mMoveLength上下波动而已,这里的mMoveLength有所不同,具体有什么不同,看下面就知道了。
然后就是数字是否进入圆的判断。每个数字之间的距离都是mDistance,而圆的半径为mDistance/2。所以,如果当移动的位置mMoveLength超过mDistance就可以算是进入圆心,mSelected也就可以更新了。但是mMoveLength记录的是总的移动距离,只要移动的太远了也不行,所以mMoveLength的计算需要更改一下。
假设点之间的距离是100,开始selected的点2在圆心位置,然后所有的点一起向上移动70,mMoveLength=-70。这时候点3应该就是selected的点,所以这时候我们也可以认为是所有的点向下移动了30,所以mMoveLength=30。然后我们在代码中实现这段想法。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()){
case MotionEvent.ACTION_DOWN:
mLastY=event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
mMoveLength+=event.getRawY()-mLastY;
if (mMoveLength>=mDistance/2){//手指往下滑
if (mSelected>0){
mSelected--;
mMoveLength=mMoveLength-mDistance;
}
}else if (mMoveLength<=-mDistance/2){//手指往上滑
if (mSelected=mDistance/2){
mMoveLength=mDistance/2;
}//已经滑动底部,并且即将出圆,加上textSize是因为坐标从数字的左上角计算
else if (mSelected==mData.size()-1&&mMoveLength<=-mDistance/2+mTextSize){
mMoveLength=-mDistance/2+mTextSize;
}
mLastY=event.getRawY();
invalidate();
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
同时考虑到不能让数字画出范围,所以还加了判断条件。
效果如下:
至此,全部的代码如下:
public class TestScroller extends View{
private Paint mPaint;
private int mWidth;
private int mHeight;
private float mTextSize=60;
private float mCircleTextSize=80;
private int mDistance=200;
private float mMoveLength;
private float mLastY;
private ListmData;
private int mSelected;
private final int DRAW_UP=1;
private final int DRAW_DOWN=-1;
public TestScroller(Context context) {
super(context);
init();
}
public TestScroller(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public TestScroller(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TestScroller(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth=getMeasuredWidth();
mHeight=getMeasuredHeight();
}
@Override
protected void onDraw(Canvas canvas) {
drawData(canvas);
drawOtherData(canvas,mSelected,DRAW_UP);
drawOtherData(canvas,mSelected,DRAW_DOWN);
mPaint.setStyle(Paint.Style.STROKE);//中空的线
canvas.drawCircle(mWidth/2,mTextSize+3*mDistance,mDistance/2,mPaint);
}
private void drawData(Canvas canvas){
mPaint.setTextSize(mCircleTextSize);
canvas.drawText(String.valueOf(mData.get(mSelected)), mWidth/2,
mTextSize+3*mDistance+mMoveLength,mPaint);
}
private void drawOtherData(Canvas canvas,int mSelectedPosition,int type){
mPaint.setTextSize(mTextSize);
if (type==1){
for (int i=mSelectedPosition+1;i=0;i--){
canvas.drawText(String.valueOf(mData.get(i)), mWidth/2,
mTextSize+(3+i-mSelectedPosition)*mDistance+mMoveLength,mPaint);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()){
case MotionEvent.ACTION_DOWN:
mLastY=event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
mMoveLength+=event.getRawY()-mLastY;
if (mMoveLength>=mDistance/2){//手指往下滑
if (mSelected>0){
mSelected--;
mMoveLength=mMoveLength-mDistance;
}
}else if (mMoveLength<=-mDistance/2){//手指往上滑
if (mSelected=mDistance/2){
mMoveLength=mDistance/2;
}//已经滑动底部,并且即将出圆,加上textSize是因为坐标从数字的左上角计算
else if (mSelected==mData.size()-1&&mMoveLength<=-mDistance/2+mTextSize){
mMoveLength=-mDistance/2+mTextSize;
}
mLastY=event.getRawY();
invalidate();
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
private void init(){
mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTextSize(mTextSize);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mPaint.setColor(getContext().getColor(R.color.black));
}
mData=new ArrayList<>();
for (int i=0;i<=10;i++){
mData.add(i);
}
mSelected=3;
}
}
现在还剩下最后一个自动复位的功能,前面已经说过了,这个功能是依靠Timer的schedule()方法实现的,该方法可以间隔一定时间重复执行操作。我们则需要通过这个方法,将mMoveLength还原为0就可以实现我们所需要的功能了。
在这里,你需要用到三个类:Timer、TimerTask和Handle。
简单地说就是,Timer可以重复执行TimerTask里的操作,而TimerTask里的操作就是Handle要做的事。
代码如下
private float mSpead=10;
private MyTimerTask mTimerTask;
private Handler mHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
if (Math.abs(mMoveLength)
private class MyTimerTask extends TimerTask{
Handler handler;
public MyTimerTask(Handler handler) {
this.handler = handler;
}
@Override
public void run() {
handler.sendMessage(handler.obtainMessage());
}
}
case MotionEvent.ACTION_UP:
mTimerTask=new MyTimerTask(mHandler);
Timer timer=new Timer();
timer.schedule(mTimerTask,0,10);
break;
首先看第一部分代码,我们需要先确定一个自动滑动的速度,也就是每次移动的距离,太大了会给人突兀的感觉,太小了也就滑的太慢了。Handler初始化,第一个if判断是否滑动结束,如果滑动结束,那么mTimerTask如果不为空,就应该取消,一面继续执行。当然,这里肯定是不为空的。
Math.abs(mMoveLength)/mMoveLength
这个应该不用多说了吧,因为mMoveLength不能确定正负,而这样就一定可以得到相反的符号,然后在乘以mSpead,就可以是mMoveLength靠近0。最后invalid()重绘刷新。
第二部分代码就是自定义的MyTimerTask类,重写了run()方法,使其执行handler的方法。
第三部分代码是在onTouchEvent方法中,手指抬起的情况下执行的。timer.schedule(),三个参数分别为:要执行的TimerTask、推迟指定时间后执行以及执行的间隔,这里是10ms。
最后的效果如下图
最后我们还有一个小地方需要修改,第一部分代码会提示Handler类应该是静态的,这样可能会发生内存泄露,解决的方法很简单,只需要使用弱引用就行了。
private MyHandler mHandler=new MyHandler(this);
private static class MyHandler extends Handler{
WeakReferenceview;
public MyHandler(TestScroller view) {
this.view = new WeakReference<>(view);
}
@Override
public void handleMessage(Message msg) {
if (view==null)
return;
if (Math.abs(view.get().mMoveLength)
大功告成!