Android 打造自己的滚动选择器ScrollSelector
来源:互联网 发布:软件测试费用标准 编辑:程序博客网 时间:2024/05/21 06:24
这是我在一个项目中做的日期选择器,用PopupWindow+自定义View(ScrollSelector)来实现的,其中最关键的是三个滚动选择器(年月日),是用我自定义的View:ScrollSelector来实现的。本来网上已经有别人做的类似的控件的了,不过我想要自己做一个。
上效果图
工程目录
我们要关注的就只有这三个文件
<span style="font-size:18px;">import android.app.Activity;import android.os.Bundle;import java.util.ArrayList;public class MainActivity extends Activity { private ScrollSelector scrollSelector; //滚动选择器 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //获取滚动选择器控件 scrollSelector = (ScrollSelector) findViewById(R.id.scrollSelector); //初始项列表 ArrayList<String> list = new ArrayList<>(); for (int i = 0; i < 20; i++){ list.add("第" + i + "项"); } //设置滚动选择器的项列表 scrollSelector.setItemContents(list); } }</span>
这里生成20个测试数据,传给ScrollSelector
<span style="font-size:18px;"><?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center"> <com.ffpy.demo.ScrollSelector android:id="@+id/scrollSelector" android:layout_width="200dp" android:layout_height="300dp" android:background="@android:color/darker_gray"/></LinearLayout></span>这里把ScrollSelector的背景色设置为灰色,与布局的背景色不同,是为了能够将控件和布局背景区分开来,方便测试
ScrollSelector.java
import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.Rect;import android.os.Handler;import android.os.Message;import android.util.AttributeSet;import android.util.Log;import android.view.GestureDetector;import android.view.MotionEvent;import android.view.View;import java.util.ArrayList;/** * Created by Administrator on 2016/9/7. * 滚动选择器 */public class ScrollSelector extends View { /** * 获取选中项 */ public int getSelectedIndex(){ return (int) (-offsetY + 0.5); } /** * 设置选中项 */ public void setSelectedIndex(int pos){ if (pos < 0 || pos >= contents.size()) return; offsetY = -pos + 1; } /** * 设置项列表的内容 */ public void setItemContents(ArrayList<String> list){ /*当前选中项为原本列表项的最后一项时,如果重新指定的列表项的比原本的列表项的小 则会让当前的选中项为空,所以需要重新指定选中项*/ if (getSelectedIndex() >= list.size()){ setSelectedIndex(list.size() - 1); } contents = list; invalidate(); } /** * 设置显示的项数 */ public void setShowItemNum(int num){ showItemNum = num; offsetY = (showItemNum - 1) / 2; //设置默认项 } /** * 设置分割线的颜色 */ public void setDividerColor(int dividerColor) { this.dividerColor = dividerColor; } /** * 设置选中状态字体的颜色 */ public void setTextSelectorColor(int textSelectorColor) { this.textSelectorColor = textSelectorColor; } /** * 设置正常状态字体的颜色 */ public void setTextNormalColor(int textNormalColor) { this.textNormalColor = textNormalColor; } private final int DIVIDER_WIDTH = 2; //分割线的宽度 private final int DEFAULT_TEXTSIZE = 50; //默认字体大小 private final int SLEEP_TIME = 1000 / 60; //动画的延时时间,每秒大约80帧 private final int WHAT_INVALIDATE = 0; //重新绘制 private int showItemNum = 3; //显示的项数 private int dividerY; //绘制分隔线的y坐标 private int itemHeight; //每一项所占的高度 private int dividerColor = 0xFF8A8A8A; //分割线的颜色 private int textSelectorColor = 0xFFFF0000; //选中状态文字的颜色 private int textNormalColor = 0xFF000000; //正常状态文字的颜色 private int marqueeX; //跑马灯的x坐标偏移 private int marqueeWidth; //跑马灯的宽度 private int borderWhenDown; //按下时的边界状态 private float offsetY; //项偏移的y坐标 private boolean isPress; //手指是否是按下状态 private boolean isFirst = true; //是否是首次绘制 private boolean isSkiping; //是否正在执行跳转 private boolean isStopSkiping; //是否要停止跳转 private boolean isHoming; //是否正在执行归位 private ArrayList<String> contents; //项的内容 private Paint mPaint; //画笔 private GestureDetector mDetector; //手势 private Handler mHandler; //异步处理 private RollThread rollThread; //滚动线程 private MarqueeThread marqueeThread; //跑马灯线程 public ScrollSelector(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); //实例化画笔 mPaint.setTextSize(DEFAULT_TEXTSIZE); //设置字体大小 mPaint.setStrokeWidth(DIVIDER_WIDTH); //设置线条的宽度 mDetector = new GestureDetector(context, new MyGestureListener()); //实例化手势 contents = new ArrayList<>(); //初始化列表项的内容,防止出现空指针错误 mHandler = new Handler(){ //实例化Handler @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what){ //重新绘制 case WHAT_INVALIDATE: invalidate(); break; } } }; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); //计算每一项的高度 itemHeight = h / showItemNum; //计算分割线的y坐标 dividerY = itemHeight * ((showItemNum - 1) / 2); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //绘制分割线 mPaint.setColor(dividerColor); canvas.drawLine(0, dividerY, getWidth(), dividerY, mPaint); canvas.drawLine(0, dividerY + itemHeight, getWidth(), dividerY + itemHeight, mPaint); //边界限制 borderLimit(); //绘制项 for (int i = 0; i < showItemNum + 1; i++){ //获取要绘制的项的序号 int index = (int) -offsetY + i - (showItemNum - 1) / 2; if (index >= contents.size()) break; if (index < 0) continue; //获取字符串的宽高 String item = contents.get(index); Rect bound = new Rect(); mPaint.getTextBounds(item, 0, item.length(), bound); //绘制字符串 int x = bound.width() > getWidth() ? 0 :(getWidth() - bound.width()) / 2; //绘制文本的x坐标 int y = (int) (itemHeight * i + (offsetY - (int) offsetY) * itemHeight); //绘制文本的y坐标 y += (itemHeight + bound.height()) / 2; //绘制文本的基线偏移量 if (getSelectedIndex() == index) { mPaint.setColor(textSelectorColor); //选中状态的字体颜色 //判断是否需要跑马灯 if (bound.width() > getWidth()) { marqueeWidth = bound.width(); x = marqueeX; if (isFirst){ marquee(); } }else{ marqueeWidth = 0; } }else { mPaint.setColor(textNormalColor); //正常状态的字体颜色 } canvas.drawText(item, x, y, mPaint); if (isFirst) isFirst = false; } } @Override public boolean onTouchEvent(MotionEvent event) { //手指按下 if (event.getAction() == MotionEvent.ACTION_DOWN){ isPress = true; borderWhenDown = borderLimit(); marqueeX = 0; if (isSkiping) isStopSkiping = true; } //手指抬起 if (event.getAction() == MotionEvent.ACTION_UP){ isPress = false; if (!isSkiping) { homing(); } marquee(); } //手势判断 mDetector.onTouchEvent(event); return true; } /** * 归位 */ private void homing(){ if (isHoming) return; isHoming = true; new HomingThread().start(); } /** * 滚动 */ private void roll(float speed){ if (rollThread != null && rollThread.isAlive()) return; rollThread = new RollThread(speed); rollThread.start(); } /** * 跳转 * @param dir true为跳转到顶部,false为跳转到底部 */ private void skip(boolean dir){ if (isSkiping) return; isSkiping = true; Log.e("tag", "skip"); new SkipThread(dir).start(); } /** * 跑马灯显示 */ private void marquee(){ if (marqueeThread != null && marqueeThread.isAlive()) return; marqueeThread = new MarqueeThread(); marqueeThread.start(); } /** * 边界限制 * @return -1为在顶部,1为在底部,0为不在边界 */ private int borderLimit(){ if (offsetY >= 0) { //顶部边界 offsetY = 0; return -1; } else if (offsetY <= -contents.size() + 1){ //底部边界 offsetY = -contents.size() + 1; return 1; } return 0; } /** * 手势事件 */ private class MyGestureListener extends GestureDetector.SimpleOnGestureListener{ /** * 滑动 */ @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { int border = borderLimit(); if (borderWhenDown == -1 && border == -1 && distanceY < 0) { //跳转到底部 skip(false); } else if (borderWhenDown == 1 && border == 1 && distanceY > 0) { //跳转到顶部 skip(true); }else if (!isSkiping) { offsetY -= distanceY / itemHeight; //偏移量 invalidate(); } return false; } /** * 滚动 */ @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { float speed = velocityY / itemHeight / 20; if (Math.abs(speed) > 0.5) { roll(speed); } return super.onFling(e1, e2, velocityX, velocityY); } } /** * 自动返回中间位置的(归位)线程 */ private class HomingThread extends Thread{ private final float MOVE_DISTANCE = 0.05f; //每一帧的移动距离(itemHeight的比例) @Override public void run() { super.run(); float dy = 0; while(!isPress){ //手指按下就停止归位 //取小数部分 float decimal = Math.abs(offsetY - (int) offsetY); //大概达到中间位置 if (decimal > -MOVE_DISTANCE * 1.1 && decimal < MOVE_DISTANCE * 1.1) break; //移动量 dy = decimal < 0.5 ? MOVE_DISTANCE : -MOVE_DISTANCE; //防止超过位置 if ((int) offsetY != (int) (offsetY + dy)) break; offsetY += dy; try{ Thread.sleep(SLEEP_TIME); }catch (InterruptedException e){ e.printStackTrace(); } //重新绘制 mHandler.sendEmptyMessage(WHAT_INVALIDATE); } //取整 if (!isPress) { offsetY = (int) offsetY; if (dy < 0) { //误差校正 offsetY--; } mHandler.sendEmptyMessage(WHAT_INVALIDATE); } isHoming = false; } } /** * 滚动的线程 */ private class RollThread extends Thread{ private final float DAMPING = 0.1f; //速度的衰减,即每一帧之后的衰减量 private float speed; //滚动的速度,即每一帧移动的距离 public RollThread(float speed){ this.speed = speed; } @Override public void run() { super.run(); boolean dir = speed > 0; //滚动方向,true为向上,false为向下 while (!isPress){ offsetY += speed; //显示越界 if (borderLimit() != 0) { mHandler.sendEmptyMessage(WHAT_INVALIDATE); break; } //速度衰减 speed += (dir ? -DAMPING : DAMPING); //速度越界 if ((dir && speed < 0) || (!dir && speed > 0)) break; try { Thread.sleep(SLEEP_TIME); }catch (InterruptedException e){ e.printStackTrace(); } //重新绘制 mHandler.sendEmptyMessage(WHAT_INVALIDATE); } //滚动完后归位 if (!isPress) { homing(); } } } /** * 顶部和底部的跳转 */ private class SkipThread extends Thread{ private final float SKIP_TIME = 1000; //跳转时间,1秒 private boolean dir; //true为跳转到顶部,false为跳转到底部 public SkipThread(boolean dir){ this.dir = dir; } @Override public void run() { super.run(); float framesNum = SKIP_TIME / SLEEP_TIME; //总帧数 float speed = (contents.size()) / framesNum; //每帧移动的距离 if (!dir) speed *= -1; while (!isStopSkiping && getSelectedIndex() != (dir ? 0 : contents.size() - 1)){ offsetY += speed; try { Thread.sleep(SLEEP_TIME); }catch (InterruptedException e){ e.printStackTrace(); } mHandler.sendEmptyMessage(WHAT_INVALIDATE); } if (!isPress){ homing(); } isSkiping = false; isStopSkiping = false; } } /** * 过长文字跑马灯显示的线程 */ private class MarqueeThread extends Thread { private final int moveDistance = 3; //每一帧的移动距离 @Override public void run() { super.run(); while (!isPress && marqueeWidth != 0){ marqueeX -= moveDistance; if (marqueeX < -marqueeWidth) marqueeX = getWidth(); try { Thread.sleep(SLEEP_TIME); }catch (InterruptedException e){ e.printStackTrace(); } mHandler.sendEmptyMessage(WHAT_INVALIDATE); } } }}
下面给出一些关键的计算过程
分割线位置的计算
在onSizeChanged方法中
//计算分割线的y坐标dividerY = itemHeight * ((showItemNum - 1) / 2);</span>
分割线有两条,一条在上面一条在下面,很容易就知道分割线的x坐标为从0到getWidth(),关键是分割线y坐标的计算。
用dividerY来存储上面那条分割线的y坐标,下面那条分割线的y坐标就是dividerY + itemHeight,所以只需要计算出dividerY的值就可以了
可以看的出,dividerY = itemHeight * ((showItemNum - 1) / 2)
要绘制的列表项的个数
在onDraw方法中,看这一句
for (int i = 0; i < showItemNum + 1; i++){
我们要显示shwoItemNum个项,但是我们要绘制showItemNum+1项,为什么呢?看图
以showItemItem=3时为例:
这是平常状态,要显示3项
这是拖动状态,要显示4项
那为什么不把这两种状态分开来呢?因为没必要,拖动状态则要频繁的绘制,而平常状态只要绘制一次就可以了,多出的那一项因为超出View的高度所以并不会显示出来,也不会增加多少负担。而且将它们分开处理还会增加代码的复杂性,更容易出错。
拖动绘制的计算
这里用到了手势,如果对手势不了解的可以看这篇文章http://www.runoob.com/w3cnote/android-tutorial-gestures.html
在onScroll方法中
offsetY -= distanceY / itemHeight; //偏移量</span>distanceY是手指在屏幕上滑动的像素,取它相对于itemHeight的比例,加到offsetY。这里用“-=”是当手指向上滑动时,distanceY的值是正数,而绘制的项y坐标是向上偏移的,所以offsetY的值是变小的。
offsetY的整数部分就是当前的选中项,小数部分就是绘制的y坐标偏移(相对于itemHeight)
- Android 打造自己的滚动选择器ScrollSelector
- <Android> 打造自己的进度条
- 自定义日历,随心所欲的打造自己的日历选择器
- Android打造全方位滚动的ListView
- android 自定义含有滚动选择器的对话框
- android图片滚动选择器的实现
- Android自定义滚动选择器
- Android 滚动选择器PikerView
- Android 滚动时间选择器
- [Android基础知识]打造自己的动画效果
- 打造自己的 Linux下Android环境
- 打造自己的chrome for android
- 打造自己的chrome for Android
- Android开发 打造自己的Annotation框架
- 打造自己的Android-Universal-Image-Loader
- Android基础 完美打造自己的apk
- Android Studio打造自己的Live Templates
- Android 打造自己的Application类
- 逆序对
- 轻量级ORM框架——第一篇:Dapper快速学习
- Java面向对象-构造方法,this关键字
- Hibernate缓存
- 2016 大连网络赛 hdu 5876 ACM ICPC(补图求最短路)
- Android 打造自己的滚动选择器ScrollSelector
- Hibernate的出现和Hinbernate的简单模拟实现
- bzoj1078
- hdu 5875 ACM/ICPC Dalian Online 1008 Function
- 重载赋值函数与复制构造函数
- 13. Roman to Integer
- 技术博客地址
- 原生表单提交的方式
- HDU3339-In Action(最短路+01背包)