仿iOS滚轮选择控件
最近项目中需要用到类似iOS的滚轮选择控件,在网上找了些资料,自己写了个自定义控件,为了方便自己以后查看就有了这篇博客,当然了,也是为了跟大家分享一下,有什么问题也请在下面给我留言,好了废话不多说了,先上一下效果图。
效果图1:
带确定、取消头的效果图:
第二种效果是基于第一种效果的,所以接下来分析思路以及介绍源码我都会只说第一种,不过最后我也会分享源码链接里面有包括第二种的所有源码,有兴趣可以下载下来试试。
我用了两种方法来实现第一种效果,我接下来都会介绍,一种是直接用ondraw方法将所有数据都画出来,然后每次滑动根据滑动距离重绘界面来实现的,第二种是继承linearlayout,把内容textview都放到布局中,给他一个scroll用来实现滑动,最后的实现效果差不多,不过我觉得第一种比第二种滑动更流畅,但是第二种比第一种代码更好理解。
第一种继承View用ondraw方法画出界面:
思路:
1.指定你希望一屏显示的条目个数count;
2.根据count指定你的控件高度height;
3.根据height得到屏幕中心的y坐标,在中间画出上下两条选中线;
4.然后根据数据集合为数据集合中的所有数据设置初始y坐标,根据y坐标判断数据是否显示来决定是否把数据画到屏幕上,并且判断y坐标是否在上下两天选中线中间来决定是否使用选中字体来突出选中项;
5.每次滑动的时候改变数据的y坐标,重复4步骤。
思路就是这样,感觉很简单吧,接下来说说主要代码:
1.我们先来画出图上的两条横线(上下两条选中线):
/**可以显示的条目个数*/ private int itemCount = 7;
/**每个条目的高度*/ private int unitHeight = 50;
/**控件高度*/ private int Height;
Height = unitHeight * itemCount; topY = Height / 2 - unitHeight / 2;//上线的y坐标 bottomY = Height / 2 + unitHeight / 2;//下线的y坐标
/**画线*/ private void drawLine(Canvas canvas) { Paint linePaint = new Paint(Paint.ANTI_ALIAS_FLAG); linePaint.setColor(lineColor); canvas.drawLine(0, topY + lineHeight, Width, topY + lineHeight, linePaint); canvas.drawLine(0, bottomY - lineHeight, Width, bottomY - lineHeight, linePaint); }
2.画内容,把每个内容的y坐标都先设置成索引乘上单元格高度,然后判断y是否在控件高度内,决定是否显示,判断y是否在两条线中间,决定使用什么字体画出来:
refreshHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
invalidate();
}
};
先设置内容的初始y坐标
/**为数据列表添加数据*/ public void addData(ArrayList<String> list) { data = new ArrayList<>(); for (int i=0; i<list.size(); i++) { WheelItem item = new WheelItem(); item.setContent(list.get(i));//设置条目内容 item.setY(i * unitHeight);//设置条目初始y坐标 item.setId(i);//设置条目的索引 data.add(item); } refreshHandler.sendEmptyMessage(0); }
/**画数据*/ private void drawData(Canvas canvas) { for (int i=0; i<data.size(); i++) { data.get(i).drawText(canvas); } }
/**把文字画出来*/ private void drawText(Canvas canvas) { //Log.i("zhangdi","isSelected()-->itemId="+id+", height="+Height+", offsetY="+offsetY+", y="+y+", topY="+topY+", bottomY="+bottomY); if (!isVisible()) {//先判断y坐标是否在显示范围内,不在就直接返回了 return; } if (textPaint == null) {//初始化画笔 textPaint = new Paint(); textPaint.setAntiAlias(true); } if (isSelected()) {//判断y坐标是否在两条线的中间,决定是否使用选中字体 textPaint.setColor(selectedColor); textPaint.setTextSize(selectedSize); } else { textPaint.setColor(normalColor); //iOS的滚轮选择器有一种向内倾斜的感觉,但是我没找到实现办法,也懒得去找了,就让字体距离被选中的条目 //越远越小,让字体有一种自中间向两边逐渐变小的线性感觉,视觉上也会产生一点向内弯曲的感觉 if (y + offsetY > topY) { size = normalSize - 2 * translatDimensionUnit(TypedValue.COMPLEX_UNIT_SP, (y + offsetY - topY) / unitHeight, context); } else { size = normalSize - 2 * translatDimensionUnit(TypedValue.COMPLEX_UNIT_SP, (topY - y - offsetY) / unitHeight, context); } textPaint.setTextSize(size); } Rect bounds = new Rect(); textPaint.getTextBounds(content,0,content.length(),bounds);//获取内容边界值 int start = Width / 2 - bounds.width() / 2;//计算出内容的x坐标 int top = y + offsetY + unitHeight / 2 + bounds.height() / 2;//内容的y坐标 canvas.drawText(content,start,top,textPaint);//把文字写在正中间 }
3.滑动处理,按下的时候记录按下的时间和downY坐标,手指滑动的时候根据你滑动的距离设置数据的y偏移坐标,调用invalidate()方法重绘界面(这个方法会重复上边的两步),
手指松开的时候分为两种情况一种是正常松开,排除边界条件(已经选中0还向上滑或者已经选中最后一个还向下滑,直接还选中原先的项不作处理),重置数据的y和y的偏移坐标,并且判断选中项的y是否在两条线的正中间,如果不在进行微量调整;
另外一种情况就是短时间快速滑动松开,也就是按下的时间和松开的时间间隔很短但是移动距离很大,这时候就需要我们做滑动缓慢处理,这样才能实现一条一条滑动的效果,否则效果就可能是直接从第一条数据,然后出现最后一条数据,体验很差,处理方式就是分解滑动距离然后开启一个子线程,子线程每次会睡5毫秒然后设置数据y的偏移量移动一个分解单位,直到滑动到指定距离为止。
public boolean onTouchEvent(MotionEvent ev) { /*获取每次不论是按下,滑动,抬起的y坐标*/ int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN : downY = (int) ev.getY(); downTime = System.currentTimeMillis(); break; case MotionEvent.ACTION_MOVE: actionMove(y - downY); break; case MotionEvent.ACTION_UP: //短时间用力滑动的时候,会产生一个较大的偏移量,需要分解偏移量并延长时间,让空间产生缓慢滑动的效果 if (System.currentTimeMillis() - downTime < 200 && Math.abs(y - downY) > 100) { slowMove(y - downY); } else { actionUp(y - downY); } break; } return true; }
/**每次滑动的时候修改条目的offsetY值,然后重绘界面来实现滑动效果*/ private void actionMove(int i) { i = translatDimensionUnit(TypedValue.COMPLEX_UNIT_DIP,i,context); //Log.i("zhangdi","actionMove--> move="+i); for (WheelItem item : data) { item.setOffsetY(i); } //别忘了重绘界面 refreshHandler.sendEmptyMessage(0); }
/**将y轴偏移量分解,每隔5毫秒移动分解后的一个单位的距离*/ private synchronized void slowMove(final int move) { new Thread(new Runnable() { @Override public void run() { int height = Math.abs(move); int distance = 0; while (distance < height) { try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } actionMove(move > 0 ? distance : distance * (-1)); distance += 5;//移动单位为5,如果想移动快些就增加,移动慢性减少,但是移动太快会没有那种慢慢滚动的感觉 } //Log.i("zhangdi","move="+move+", distance="+distance); actionUp(move); } }).start(); }
/**每次松手后重置条目的y轴坐标以及偏移量,判断被选中的条目是否在屏幕中心,如果不在调整到中心*/ private void actionUp(int i) { i = translatDimensionUnit(TypedValue.COMPLEX_UNIT_DIP,i,context); /*排除边界条件,当第一个条目的y==topY时证明滑动到了最顶端,不允许再继续上滑 当最后一个条目的y==topY时证明滑动到了最底端,不允许再继续下滑 其他情况返回值为0*/ if (!isEdge(i)) { return; } //完成滑动后重置条目的y轴偏移量,并调整条目的y轴坐标 offsetYReset(i); for (WheelItem item: data) { if (item.isSelected()) { //Log.i("zhangdi","actionUp-->selectItemId="+item.getId()); if (listener != null) { listener.selected(item.getId(), item.getContent()); } int y = topY - item.getY();//计算出当前选中的条目与上线的y轴坐标差 if (y != 0) {//文本不是正好被滑动到正中间 actionMove(y);//根据y轴坐标差继续滑动一段距离 offsetYReset(y); } break; } } refreshHandler.sendEmptyMessage(0); }
上面就是第一种方法了,感觉滑动处理的时候会比较不好理解,不过仔细看看也就明白了。
第二种继承LinearLayout使用scroller来实现:
思路:
1.还是希望显示的条目个数count;
2.根据count设置控件高度height;
3.把所有的条目都addview,并且把选中项字体调整为选种字体;
4.手指滑动的时候根据滑动距离计算出选中项,改变选中项的字体,利用scrollBy来实现滑动;
5.手指松开的时候根据滑动距离计算出选中项,改变选中项的字体,利用scroller.startScroll+computeScroll来实现缓慢移动;
6.在控件的外层加一个FrameLayout与控件等高,然后在中间加两条横线代表选中线
这种方法的滑动都是用的现成的方法,所以代码感觉简单些,但是我写完的滑动效果没有第一种方法好。思路就是这些,下面看代码:
1.在屏幕上添加数据,把选中项字体改变,滑动到默认选项
/**为选择器设置数据*/ public void setData(ArrayList<String> list) { //每次需要先清空所有的数据 removeAllViews(); wheelItems = new ArrayList<>(); currentIndex = firstSelectedIndex; //遍历数组生成对应的文本控件并添加到屏幕上 for (int i=0; i<list.size(); i++) { TextView tv = new TextView(context); tv.setGravity(Gravity.CENTER); tv.setText(list.get(i)); tv.setLayoutParams(params); wheelItems.add(tv); setSelectedFont();//设置选中项的字体 smoothScrollBy((firstSelectedIndex - (itemCount/2))*unitHeight);//根据首选项的索引滑动到首选项的位置 addView(tv); } firstSelectedIndex -= (firstSelectedIndex - (itemCount/2));//计算出首选项与中间项的差值,以便后续计算选中项和滑动距离使用 invalidate(); }
2.滑动过程中如何计算出当前选中项
/**根据手指的滑动速度获取到当前选中的条目*/ private void getSelectedIndex(int scrollY){ tracker.computeCurrentVelocity(100);//设置时间单位为0.1秒 float yVelocity = tracker.getYVelocity();//1秒内延y轴运动了多少个像素 if (Math.abs(yVelocity) >= unitHeight) {//如果像素大于一个单位高度 //像素是大于0的则是向上滑动,否则是向下 int count = (int) (yVelocity / changUnit(TypedValue.COMPLEX_UNIT_PX, unitHeight) / 5); currentIndex = currentIndex - count; } else { if (scrollY > 0) {//scrollY大于0表示向itemCount/2的下方项滑动,否则是向上方项滑动 currentIndex = (scrollY + unitHeight/2)/unitHeight + firstSelectedIndex; } else { currentIndex = (scrollY - unitHeight/2)/unitHeight + firstSelectedIndex; } } currentIndex = Math.max(0, Math.min(currentIndex, wheelItems.size()-1)); setSelectedFont(); tracker.clear(); }
3.手指移动时的滑动方法:
/**手指滑动时使用,实时改变选中的条目*/ private void actionMove(int scrollY, int dexY) { getSelectedIndex(scrollY); scrollBy(0,-dexY); }
4.手指松开时调用的滑动方法:
/**手指松开后继续滑动一段距离*/ private void actionUp(final int scrollY) { getSelectedIndex(scrollY); int dy = (currentIndex - firstSelectedIndex) * unitHeight - scrollY; smoothScrollBy(dy); }
/**让控件在0.1秒内从当前位置滑动到指定位置,必须配合computeScroll使用*/ private void smoothScrollBy(int dy) {//dy是y轴移动距离 scroller.startScroll(0, getScrollY(), 0, dy, 100); postInvalidate(); } @Override public void computeScroll() { if (scroller.computeScrollOffset()) { scrollTo(scroller.getCurrX(), scroller.getCurrY()); postInvalidate(); } }
好了就是以上这些了,下面是demo源码下载链接,其中MyWheelView控件对应第一幅图,在MainActivity中调用的;HeaderWheelView对应第二幅图,在MainActivityHeader中调用;MyWheelView2是第二种实现方法,在MainActivity2中调用,使用方法就是修改清单配置文件中的启动activity来运行不同效果就行了。