android的自带时间选择控件,是一个让用户既能输入的又能选择的样子。这本来没有太大的问题了。 但是,坑爹的android是开源的。自带的时间控件在某些机型上,早已经是面目全非了,在用以一个普通用户来说,苹果的时间滚轮的控件就是爽点。 要写滚轴控件,无非是要用好,写好wheelview这个类,对于wheelview这个类,我这里要做详细的分析。在iOS里面有一种控件------滚筒控件(Wheel View),这通常用于设置时间/日期,非常方便,但Android SDK并没有提供类似的控件。这里介绍一下如何Android实现WheelView。

我们从网上找到了一个开源的代码,它也实现了这样的效果,而且效果也不错,大家可以用SVN来checkout:

http://android-wheel.googlecode.com/svn/trunk

它这个Demo最本质是自己写布局,好像是利用一个LinearLayout来布局child,然后调用LinearLayout.draw(canvas)方法,把child绘制在指定的can1vas上面。它同时还提供了类似AdapterView的访问方式,用户可以设置Adapter来提供数据。我在这里主要不是讲解这个Demo的结构,如果大家感兴趣,可以自己下载代码研究。

我们进入WheelView类,看一下他是如何构建,首先,WheelView继承了View类。代码的22行到45行是导入的所需要的类。从54行到135行是声明一些变量和类:

/** 滚动的间隔 */
private static final int SCROLLING_DURATION = 400; /** 最小滚动的距离*/
private static final int MIN_DELTA_FOR_SCROLLING = 1; /** 当前颜色*/
private static final int VALUE_TEXT_COLOR = 0xF0000000; /** 文本的颜色*/
private static final int ITEMS_TEXT_COLOR = 0xFF000000; /** 顶部和底部的颜色 */
private static final int[] SHADOWS_COLORS = new int[] { 0xFF111111,
0x00AAAAAA, 0x00AAAAAA }; /** 附加的高度*/
private static final int ADDITIONAL_ITEM_HEIGHT = 15; /** 文字的大小*/
private static final int TEXT_SIZE = 24; /** 顶部和底部的距离*/
private static final int ITEM_OFFSET = TEXT_SIZE / 5; /** 附加的宽度 */
private static final int ADDITIONAL_ITEMS_SPACE = 10; /** 文本的距离*/
private static final int LABEL_OFFSET = 8; /** 左右的距离 */
private static final int PADDING = 10; /** 默认显示的项目*/
private static final int DEF_VISIBLE_ITEMS = 5; //滚轮的值
private WheelAdapter adapter = null;
//所选择的当前的项目
private int currentItem = 0; // 相应的宽度
private int itemsWidth = 0;
private int labelWidth = 0; //显示的项目数
private int visibleItems = DEF_VISIBLE_ITEMS; // 项目的高度
private int itemHeight = 0; // 条目的笔刷
private TextPaint itemsPaint;
private TextPaint valuePaint; // 相应的布局控件
private StaticLayout itemsLayout;
private StaticLayout labelLayout;
private StaticLayout valueLayout; // 标签和文本
private String label;
private Drawable centerDrawable; // 阴影的材质
private GradientDrawable topShadow;
private GradientDrawable bottomShadow; // Scrolling
private boolean isScrollingPerformed;
private int scrollingOffset; // 滚轮动画的坚挺着
private GestureDetector gestureDetector;
private Scroller scroller;
private int lastScrollY; // 是不是循环
boolean isCyclic = false; // 滚动变化的监听着
private List<OnWheelChangedListener> changingListeners = new LinkedList<OnWheelChangedListener>();
private List<OnWheelScrollListener> scrollingListeners = new LinkedList<OnWheelScrollListener>();

在这里面,使用到了StaticLayout,在开发文档中找一下这个类:

StaticLayout is a Layout for text that will not be edited after it is laid out. Use DynamicLayout for text that may change.  
  
This is used by widgets to control text layout. You should not need to use this class directly unless you are implementing your own widget or custom display object, or would be tempted to call Canvas.drawText
staticLayout被创建以后就不能被修改了,通常被用于控制文本组件布局。

还使用到了Drawable、Text'Paint、GradientDrawable、GestureDetector、Scroller类,在开发文档中,GradientDrawable的概述:

A Drawable with a color gradient for buttons, backgrounds, etc.  
  
It can be defined in an XML file with the <shape> element. For more information, see the guide to Drawable Resources

就是说这个类可以为按钮或者背景等提供渐变颜色的绘制。

TextPaint的概述:

TextPaint is an extension of Paint that leaves room for some extra data used during text measuring and drawing.

TextPaint是Paint类的一个扩展,主要是用于文本在绘制的过程中为附件的数据留出空间。

GestureDetector:手势检测,看下开发文档中关于该类的概述:

Detects various gestures and events using the supplied MotionEvents. The GestureDetector.OnGestureListener callback will notify users when a particular motion event has occurred. This class should only be used with MotionEvents reported via touch (don't use for trackball events).

为各种手势和事件提供MotionEvents。当一个具体的事件发生时会调用回调函数GestureDetector.OnGestureListener。这个类应该只适用于MotionEvents通过触摸触发的事件(不要使用追踪事件)。

140行到156行是构造方法,175到183行是set和getAdapter。在193行,setInterpolator()方法,设置interPolator这个动画接口,我们看下这个接口的概述:

An interpolator defines the rate of change of an animation. This allows the basic animation effects (alpha, scale, translate, rotate) to be accelerated, decelerated, repeated, etc.

定义了一种基于变率的一个动画。这使得基本的动画效果(alpha, scale, translate, rotate)是加速,减慢,重复等。这个方法在随机数这个例子中被使用。

203行到213行设置显示的item条数。在setVisibleItems()方法里面调用了View的invalidate()方法,看下文档中对该方法的介绍:

Invalidate the whole view. If the view is visible, onDraw(Android.graphics.Canvas) will be called at some point in the future. This must be called from a UI thread. To call from a non-UI thread, call postInvalidate().

使全部视图失效,如果View视图是可见的,会在UI线程里面从新调用onDraw()方法。

223行到233行是设置Label,既后面图片中的hours

245行到296行是设置监听,在上面已经简单的说了一下,这里不在累述。

307行到349行是设置正被选中item,就是在那个阴影条框下的那个部分,比较简单。里面主要调用了scroll这个方法:

相应的源代码如下:

private void initResourcesIfNecessary() {  
        if (itemsPaint == null) {  
            itemsPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG  
                    | Paint.FAKE_BOLD_TEXT_FLAG);  
            //itemsPaint.density = getResources().getDisplayMetrics().density;   
            itemsPaint.setTextSize(TEXT_SIZE);  
        }  
  
        if (valuePaint == null) {  
            valuePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG  
                    | Paint.FAKE_BOLD_TEXT_FLAG | Paint.DITHER_FLAG);  
            //valuePaint.density = getResources().getDisplayMetrics().density;   
            valuePaint.setTextSize(TEXT_SIZE);  
            valuePaint.setShadowLayer(0.1f, 0, 0.1f, 0xFFC0C0C0);  
        }  
  
        if (centerDrawable == null) {  
            centerDrawable = getContext().getResources().getDrawable(R.drawable.wheel_val);  
        }  
  
        if (topShadow == null) {  
            topShadow = new GradientDrawable(Orientation.TOP_BOTTOM, SHADOWS_COLORS);  
        }  
  
        if (bottomShadow == null) {  
            bottomShadow = new GradientDrawable(Orientation.BOTTOM_TOP, SHADOWS_COLORS);  
        }  
  
        setBackgroundResource(R.drawable.wheel_bg);  
    }

这个方法就是初始化在532行calculateLayoutWidth()方法中调用了这个方法,同时调用了487行的getMaxTextLength()这个方法。

471行getTextItem(int index)通过一个索引获取该item的文本。

这是第一部分,没有多少有太多意思的地方,重点的地方在以后532行到940行的内容,

首先,先把代码贴出来:

/**
* =进行文本的宽度和布局文件的计算
* @param widthSize the input layout width
* @param mode the layout mode
* @return the calculated control width
*/
private int calculateLayoutWidth(int widthSize, int mode) {
initResourcesIfNecessary(); int width = widthSize;
int maxLength = getMaxTextLength();
if (maxLength > 0) {
float textWidth = FloatMath.ceil(Layout.getDesiredWidth("0", itemsPaint));
itemsWidth = (int) (maxLength * textWidth);
} else {
itemsWidth = 0;
}
itemsWidth += ADDITIONAL_ITEMS_SPACE; // make it some more labelWidth = 0;
if (label != null && label.length() > 0) {
labelWidth = (int) FloatMath.ceil(Layout.getDesiredWidth(label, valuePaint));
} boolean recalculate = false;
if (mode == MeasureSpec.EXACTLY) {
width = widthSize;
recalculate = true;
} else {
width = itemsWidth + labelWidth + 2 * PADDING;
if (labelWidth > 0) {
width += LABEL_OFFSET;
} // Check against our minimum width
width = Math.max(width, getSuggestedMinimumWidth()); if (mode == MeasureSpec.AT_MOST && widthSize < width) {
width = widthSize;
recalculate = true;
}
} if (recalculate) {
// recalculate width
int pureWidth = width - LABEL_OFFSET - 2 * PADDING;
if (pureWidth <= 0) {
itemsWidth = labelWidth = 0;
}
if (labelWidth > 0) {
double newWidthItems = (double) itemsWidth * pureWidth
/ (itemsWidth + labelWidth);
itemsWidth = (int) newWidthItems;
labelWidth = pureWidth - itemsWidth;
} else {
itemsWidth = pureWidth + LABEL_OFFSET; // no label
}
} if (itemsWidth > 0) {
createLayouts(itemsWidth, labelWidth);
} return width;
} /**
* 创建相应的布局文件
* @param widthItems width of items layout
* @param widthLabel width of label layout
*/
private void createLayouts(int widthItems, int widthLabel) {
if (itemsLayout == null || itemsLayout.getWidth() > widthItems) {
itemsLayout = new StaticLayout(buildText(isScrollingPerformed), itemsPaint, widthItems,
widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER,
1, ADDITIONAL_ITEM_HEIGHT, false);
} else {
itemsLayout.increaseWidthTo(widthItems);
} if (!isScrollingPerformed && (valueLayout == null || valueLayout.getWidth() > widthItems)) {
String text = getAdapter() != null ? getAdapter().getItem(currentItem) : null;
valueLayout = new StaticLayout(text != null ? text : "",
valuePaint, widthItems, widthLabel > 0 ?
Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER,
1, ADDITIONAL_ITEM_HEIGHT, false);
} else if (isScrollingPerformed) {
valueLayout = null;
} else {
valueLayout.increaseWidthTo(widthItems);
} if (widthLabel > 0) {
if (labelLayout == null || labelLayout.getWidth() > widthLabel) {
labelLayout = new StaticLayout(label, valuePaint,
widthLabel, Layout.Alignment.ALIGN_NORMAL, 1,
ADDITIONAL_ITEM_HEIGHT, false);
} else {
labelLayout.increaseWidthTo(widthLabel);
}
}
} /**
*重写了次存改变的事件
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width = calculateLayoutWidth(widthSize, widthMode); int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = getDesiredHeight(itemsLayout); if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightSize);
}
} setMeasuredDimension(width, height);
} @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas); if (itemsLayout == null) {
if (itemsWidth == 0) {
calculateLayoutWidth(getWidth(), MeasureSpec.EXACTLY);
} else {
createLayouts(itemsWidth, labelWidth);
}
} if (itemsWidth > 0) {
canvas.save();
// Skip padding space and hide a part of top and bottom items
canvas.translate(PADDING, -ITEM_OFFSET);
drawItems(canvas);
drawValue(canvas);
canvas.restore();
} drawCenterRect(canvas);
drawShadows(canvas);
} /**
*画出顶部和底部的控件
* @param canvas the canvas for drawing
*/
private void drawShadows(Canvas canvas) {
topShadow.setBounds(0, 0, getWidth(), getHeight() / visibleItems);
topShadow.draw(canvas); bottomShadow.setBounds(0, getHeight() - getHeight() / visibleItems,
getWidth(), getHeight());
bottomShadow.draw(canvas);
} /**
* Draws value and label layout
* @param canvas the canvas for drawing
*/
private void drawValue(Canvas canvas) {
valuePaint.setColor(VALUE_TEXT_COLOR);
valuePaint.drawableState = getDrawableState(); Rect bounds = new Rect();
itemsLayout.getLineBounds(visibleItems / 2, bounds); // draw label
if (labelLayout != null) {
canvas.save();
canvas.translate(itemsLayout.getWidth() + LABEL_OFFSET, bounds.top);
labelLayout.draw(canvas);
canvas.restore();
} // draw current value
if (valueLayout != null) {
canvas.save();
canvas.translate(0, bounds.top + scrollingOffset);
valueLayout.draw(canvas);
canvas.restore();
}
} /**
* Draws items
* @param canvas the canvas for drawing
*/
private void drawItems(Canvas canvas) {
canvas.save(); int top = itemsLayout.getLineTop(1);
canvas.translate(0, - top + scrollingOffset); itemsPaint.setColor(ITEMS_TEXT_COLOR);
itemsPaint.drawableState = getDrawableState();
itemsLayout.draw(canvas); canvas.restore();
} /**
* Draws rect for current value
* @param canvas the canvas for drawing
*/
private void drawCenterRect(Canvas canvas) {
int center = getHeight() / 2;
int offset = getItemHeight() / 2;
centerDrawable.setBounds(0, center - offset, getWidth(), center + offset);
centerDrawable.draw(canvas);
} @Override
public boolean onTouchEvent(MotionEvent event) {
WheelAdapter adapter = getAdapter();
if (adapter == null) {
return true;
} if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {
justify();
}
return true;
} /**
* 滚动滚轮的方法
* @param delta the scrolling value
*/
private void doScroll(int delta) {
scrollingOffset += delta; int count = scrollingOffset / getItemHeight();
int pos = currentItem - count;
if (isCyclic && adapter.getItemsCount() > 0) {
// fix position by rotating
while (pos < 0) {
pos += adapter.getItemsCount();
}
pos %= adapter.getItemsCount();
} else if (isScrollingPerformed) {
//
if (pos < 0) {
count = currentItem;
pos = 0;
} else if (pos >= adapter.getItemsCount()) {
count = currentItem - adapter.getItemsCount() + 1;
pos = adapter.getItemsCount() - 1;
}
} else {
// fix position
pos = Math.max(pos, 0);
pos = Math.min(pos, adapter.getItemsCount() - 1);
} int offset = scrollingOffset;
if (pos != currentItem) {
setCurrentItem(pos, false);
} else {
invalidate();
} // update offset
scrollingOffset = offset - count * getItemHeight();
if (scrollingOffset > getHeight()) {
scrollingOffset = scrollingOffset % getHeight() + getHeight();
}
} // 手势的监听者
private SimpleOnGestureListener gestureListener = new SimpleOnGestureListener() {
public boolean onDown(MotionEvent e) {
if (isScrollingPerformed) {
scroller.forceFinished(true);
clearMessages();
return true;
}
return false;
} public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
startScrolling();
doScroll((int)-distanceY);
return true;
} public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
lastScrollY = currentItem * getItemHeight() + scrollingOffset;
int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();
int minY = isCyclic ? -maxY : 0;
scroller.fling(0, lastScrollY, 0, (int) -velocityY / 2, 0, 0, minY, maxY);
setNextMessage(MESSAGE_SCROLL);
return true;
}
}; // 消息的本事
private final int MESSAGE_SCROLL = 0;
private final int MESSAGE_JUSTIFY = 1; /**
* 请出消息队列的消息 防治下一条消息
*
* @param message the message to set
*/
private void setNextMessage(int message) {
clearMessages();
animationHandler.sendEmptyMessage(message);
} /**
* Clears messages from queue
*/
private void clearMessages() {
animationHandler.removeMessages(MESSAGE_SCROLL);
animationHandler.removeMessages(MESSAGE_JUSTIFY);
} //动画的handler
private Handler animationHandler = new Handler() {
public void handleMessage(Message msg) {
scroller.computeScrollOffset();
int currY = scroller.getCurrY();
int delta = lastScrollY - currY;
lastScrollY = currY;
if (delta != 0) {
doScroll(delta);
} // scrolling is not finished when it comes to final Y
// so, finish it manually
if (Math.abs(currY - scroller.getFinalY()) < MIN_DELTA_FOR_SCROLLING) {
currY = scroller.getFinalY();
scroller.forceFinished(true);
}
if (!scroller.isFinished()) {
animationHandler.sendEmptyMessage(msg.what);
} else if (msg.what == MESSAGE_SCROLL) {
justify();
} else {
finishScrolling();
}
}
}; /**
*调整滚轮的方法
*/
private void justify() {
if (adapter == null) {
return;
} lastScrollY = 0;
int offset = scrollingOffset;
int itemHeight = getItemHeight();
boolean needToIncrease = offset > 0 ? currentItem < adapter.getItemsCount() : currentItem > 0;
if ((isCyclic || needToIncrease) && Math.abs((float) offset) > (float) itemHeight / 2) {
if (offset < 0)
offset += itemHeight + MIN_DELTA_FOR_SCROLLING;
else
offset -= itemHeight + MIN_DELTA_FOR_SCROLLING;
}
if (Math.abs(offset) > MIN_DELTA_FOR_SCROLLING) {
scroller.startScroll(0, 0, 0, offset, SCROLLING_DURATION);
setNextMessage(MESSAGE_JUSTIFY);
} else {
finishScrolling();
}
} /**
* 开始滚动
*/
private void startScrolling() {
if (!isScrollingPerformed) {
isScrollingPerformed = true;
notifyScrollingListenersAboutStart();
}
} /**
* 结束滚动
*/
void finishScrolling() {
if (isScrollingPerformed) {
notifyScrollingListenersAboutEnd();
isScrollingPerformed = false;
}
invalidateLayouts();
invalidate();
} /**
* 滚动滚轮
* @param itemsToSkip items to scroll
* @param time scrolling duration
*/
public void scroll(int itemsToScroll, int time) {
scroller.forceFinished(true);
lastScrollY = scrollingOffset;
int offset = itemsToScroll * getItemHeight();
scroller.startScroll(0, lastScrollY, 0, offset - lastScrollY, time);
setNextMessage(MESSAGE_SCROLL);
startScrolling();
}

在629行到744行的代码是绘制图形,747行onTouchEvent()里面主要是调用了882行的justify()方法,用于调整画面

@Override
public boolean onTouchEvent(MotionEvent event) {
WheelAdapter adapter = getAdapter();
if (adapter == null) {
return true;
} if (!gestureDetector.onTouchEvent(event) && event.getAction() == MotionEvent.ACTION_UP) {
justify();
}
return true;
}
/**
* Justifies wheel
*/
private void justify() {
if (adapter == null) {
return;
} lastScrollY = 0;
int offset = scrollingOffset;
int itemHeight = getItemHeight();
boolean needToIncrease = offset > 0 ? currentItem < adapter.getItemsCount() : currentItem > 0;
if ((isCyclic || needToIncrease) && Math.abs((float) offset) > (float) itemHeight / 2) {
if (offset < 0)
offset += itemHeight + MIN_DELTA_FOR_SCROLLING;
else
offset -= itemHeight + MIN_DELTA_FOR_SCROLLING;
}
if (Math.abs(offset) > MIN_DELTA_FOR_SCROLLING) {
scroller.startScroll(0, 0, 0, offset, SCROLLING_DURATION);
setNextMessage(MESSAGE_JUSTIFY);
} else {
finishScrolling();
}
}

我们看下重写的系统回调函数onMeasure()(用于测量各个控件距离,父子控件空间大小等):

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width = calculateLayoutWidth(widthSize, widthMode); int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = getDesiredHeight(itemsLayout); if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightSize);
}
} setMeasuredDimension(width, height);
}
里面用到了532行calculateLayoutWidth()的方法,就是计算Layout的宽度,在calculateLayoutWidth()这个方法里面调用了 /**
* Creates layouts
* @param widthItems width of items layout
* @param widthLabel width of label layout
*/
private void createLayouts(int widthItems, int widthLabel) {
if (itemsLayout == null || itemsLayout.getWidth() > widthItems) {
itemsLayout = new StaticLayout(buildText(isScrollingPerformed), itemsPaint, widthItems,
widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER,
1, ADDITIONAL_ITEM_HEIGHT, false);
} else {
itemsLayout.increaseWidthTo(widthItems);
} if (!isScrollingPerformed && (valueLayout == null || valueLayout.getWidth() > widthItems)) {
String text = getAdapter() != null ? getAdapter().getItem(currentItem) : null;
valueLayout = new StaticLayout(text != null ? text : "",
valuePaint, widthItems, widthLabel > 0 ?
Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER,
1, ADDITIONAL_ITEM_HEIGHT, false);
} else if (isScrollingPerformed) {
valueLayout = null;
} else {
valueLayout.increaseWidthTo(widthItems);
} if (widthLabel > 0) {
if (labelLayout == null || labelLayout.getWidth() > widthLabel) {
labelLayout = new StaticLayout(label, valuePaint,
widthLabel, Layout.Alignment.ALIGN_NORMAL, 1,
ADDITIONAL_ITEM_HEIGHT, false);
} else {
labelLayout.increaseWidthTo(widthLabel);
}
}
}

然后我们接着看onDraw()方法:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas); if (itemsLayout == null) {
if (itemsWidth == 0) {
calculateLayoutWidth(getWidth(), MeasureSpec.EXACTLY);
} else {
createLayouts(itemsWidth, labelWidth);
}
} if (itemsWidth > 0) {
canvas.save();
// Skip padding space and hide a part of top and bottom items
canvas.translate(PADDING, -ITEM_OFFSET);
drawItems(canvas);
drawValue(canvas);
canvas.restore();
} drawCenterRect(canvas);
drawShadows(canvas);
}

在onDraw方法中,也调用了CreateLayout()方法,然后在后面调用drawCenterRect()、drawItems()、drawValue()、绘制阴影drawShadows()两个方法:

**
* Draws shadows on top and bottom of control
* @param canvas the canvas for drawing
*/
private void drawShadows(Canvas canvas) {
topShadow.setBounds(0, 0, getWidth(), getHeight() / visibleItems);
topShadow.draw(canvas); bottomShadow.setBounds(0, getHeight() - getHeight() / visibleItems,
getWidth(), getHeight());
bottomShadow.draw(canvas);
} /**
* Draws value and label layout
* @param canvas the canvas for drawing
*/
private void drawValue(Canvas canvas) {
valuePaint.setColor(VALUE_TEXT_COLOR);
valuePaint.drawableState = getDrawableState(); Rect bounds = new Rect();
itemsLayout.getLineBounds(visibleItems / 2, bounds); // draw label
if (labelLayout != null) {
canvas.save();
canvas.translate(itemsLayout.getWidth() + LABEL_OFFSET, bounds.top);
labelLayout.draw(canvas);
canvas.restore();
} // draw current value
if (valueLayout != null) {
canvas.save();
canvas.translate(0, bounds.top + scrollingOffset);
valueLayout.draw(canvas);
canvas.restore();
}
} /**
* Draws items
* @param canvas the canvas for drawing
*/
private void drawItems(Canvas canvas) {
canvas.save(); int top = itemsLayout.getLineTop(1);
canvas.translate(0, - top + scrollingOffset); itemsPaint.setColor(ITEMS_TEXT_COLOR);
itemsPaint.drawableState = getDrawableState();
itemsLayout.draw(canvas); canvas.restore();
} /**
* Draws rect for current value
* @param canvas the canvas for drawing
*/
private void drawCenterRect(Canvas canvas) {
int center = getHeight() / 2;
int offset = getItemHeight() / 2;
centerDrawable.setBounds(0, center - offset, getWidth(), center + offset);
centerDrawable.draw(canvas);
}

主要就是通过canvas类进行图形的绘制。

最后,我们看下840行定义的手势监听:

// gesture listener
private SimpleOnGestureListener gestureListener = new SimpleOnGestureListener() {
public boolean onDown(MotionEvent e) {
if (isScrollingPerformed) {
scroller.forceFinished(true);
clearMessages();
return true;
}
return false;
} public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
startScrolling();
doScroll((int)-distanceY);
return true;
} public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
lastScrollY = currentItem * getItemHeight() + scrollingOffset;
int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemsCount() * getItemHeight();
int minY = isCyclic ? -maxY : 0;
scroller.fling(0, lastScrollY, 0, (int) -velocityY / 2, 0, 0, minY, maxY);
setNextMessage(MESSAGE_SCROLL);
return true;
}
};

里面主要调用的方法:clearMessages()、startScrolling()、doScroll()、setNextMessage(),先看下中间的两个方法开始滑动和滑动

/**
* Scrolls the wheel
* @param delta the scrolling value
*/
private void doScroll(int delta) {
scrollingOffset += delta;
int count = scrollingOffset / getItemHeight();
int pos = currentItem - count;
if (isCyclic && adapter.getItemsCount() > 0) {
// fix position by rotating
while (pos < 0) {
pos += adapter.getItemsCount();
}
pos %= adapter.getItemsCount();
} else if (isScrollingPerformed) {
//
if (pos < 0) {
count = currentItem;
pos = 0;
} else if (pos >= adapter.getItemsCount()) {
count = currentItem - adapter.getItemsCount() + 1;
pos = adapter.getItemsCount() - 1;
}
} else {
// fix position
pos = Math.max(pos, 0);
pos = Math.min(pos, adapter.getItemsCount() - 1);
} int offset = scrollingOffset;
if (pos != currentItem) {
setCurrentItem(pos, false);
} else {
invalidate();
} // update offset
scrollingOffset = offset - count * getItemHeight();
if (scrollingOffset > getHeight()) {
scrollingOffset = scrollingOffset % getHeight() + getHeight();
}
} /**
* Starts scrolling
*/
private void startScrolling() {
if (!isScrollingPerformed) {
isScrollingPerformed = true;
notifyScrollingListenersAboutStart();
}
}

在startScrolling方法里面有287行的notifyScrollingListenersAboutStart函数。

再看clearMessages()、setMessageNext()

private void setNextMessage(int message) {
clearMessages();
animationHandler.sendEmptyMessage(message);
} /**
* Clears messages from queue
*/
private void clearMessages() {
animationHandler.removeMessages(MESSAGE_SCROLL);
animationHandler.removeMessages(MESSAGE_JUSTIFY);
}

里面使用到了animationHandler,用来传递动画有段的操作:

animation handler
private Handler animationHandler = new Handler() {
public void handleMessage(Message msg) {
scroller.computeScrollOffset();
int currY = scroller.getCurrY();
int delta = lastScrollY - currY;
lastScrollY = currY;
if (delta != 0) {
doScroll(delta);
} // scrolling is not finished when it comes to final Y
// so, finish it manually
if (Math.abs(currY - scroller.getFinalY()) < MIN_DELTA_FOR_SCROLLING) {
currY = scroller.getFinalY();
scroller.forceFinished(true);
}
if (!scroller.isFinished()) {
animationHandler.sendEmptyMessage(msg.what);
} else if (msg.what == MESSAGE_SCROLL) {
justify();
} else {
finishScrolling();
}
}
};

里面调用了finishScrolling()

/**
* Finishes scrolling
*/
void finishScrolling() {
if (isScrollingPerformed) {
notifyScrollingListenersAboutEnd();
isScrollingPerformed = false;
}
invalidateLayouts();
invalidate();
}

over

这就是对wheelview的控件的分析,有了它,时间滚轮控件,so-easy。

android 时间控件概述的更多相关文章

  1. android 地址控件概述

    最近,公司做项目,需要一个地址控件,本来是想androidcopy开源的android的地址控件,但是了,找来找去.都没有找到一个真正满足我的需求的,普通的地址控件只是精确到市县区三级,但是我们的需求 ...

  2. Android --时间控件的使用

    1. mian.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns: ...

  3. DatePicker日期与时间控件

    DatePicker日期与时间控件 一.简介 二.方法 最日常的使用方法了 日期控件DatePicker 时间控件TimePicker 月份从0开始 三.代码实例 效果图: 代码: fry.Activ ...

  4. 一个Demo让你掌握Android所有控件

    原文:一个Demo让你掌握Android所有控件 本文是转载收藏,侵删,出处:"安卓巴士"      下面给出实现各个组件的源代码: 1.下拉框实现--Spinner packag ...

  5. Android 开源控件与常用开发框架开发工具类

    Android的加载动画AVLoadingIndicatorView 项目地址: https://github.com/81813780/AVLoadingIndicatorView 首先,在 bui ...

  6. android课程表控件、悬浮窗、Todo应用、MVP框架、Kotlin完整项目源码

    Android精选源码 Android游戏2048 MVP Kotlin项目(RxJava+Rerotfit+OkHttp+Glide) Android基于自定义Span的富文本编辑器 android ...

  7. Appium Android Toast控件

    Android Toast控件是Android系统级别的控件,不是App的控件,getPageSource是⽆法找到的. Toast介绍 1.背景 在安卓设备里面,使用各种手机应用程序的时候,需要先进 ...

  8. VB6.0中,DTPicker日期、时间控件不允许为空时,采用文本框与日期、时间控件相互替换赋值(解决方案)

    VB6.0中,日期.时间控件不允许为空时,采用文本框与日期.时间控件相互替换赋值,或许是一个不错的选择. 实现效果如下图: 文本框txtStopTime1 时间框DTStopTime1(DTPicke ...

  9. [转]一种简单的js时间控件

    使用方法: 粘贴代码到文本文档中,文档名称为datetime.js,然后在html文件中引用如下代码即可 <input name="shijian1" id="sh ...

随机推荐

  1. 深入理解ajax系列第八篇

    前面的话 在以前,网站的用户与后端交互的主要方式是通过HTML表单的使用.表单的引入在1993年,由于其简单性和易用性,直到电子商务出现之前一直保持着重要位置.理解表单提交,对于更深入地理解ajax是 ...

  2. [HDU6155]Subsequence Count(线段树+矩阵)

    DP式很容易得到,发现是线性递推形式,于是可以矩阵加速.又由于是区间形式,所以用线段树维护. https://www.cnblogs.com/Miracevin/p/9124511.html 关键在于 ...

  3. 【推导】【贪心】Codeforces Round #472 (rated, Div. 2, based on VK Cup 2018 Round 2) D. Riverside Curio

    题意:海平面每天高度会变化,一个人会在每天海平面的位置刻下一道痕迹(如果当前位置没有已经刻划过的痕迹),并且记录下当天比海平面高的痕迹有多少条,记为a[i].让你最小化每天比海平面低的痕迹条数之和. ...

  4. bzoj 1590: [Usaco2008 Dec]Secret Message 秘密信息

    1590: [Usaco2008 Dec]Secret Message 秘密信息 Description     贝茜正在领导奶牛们逃跑.为了联络,奶牛们互相发送秘密信息.     信息是二进制的,共 ...

  5. 最小生成树 Prim(普里姆)算法和Kruskal(克鲁斯特尔)算法

    Prim算法 1.概览 普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (gra ...

  6. 使用 Nexus 搭建私服仓库时我犯的一个小错误

    私服搭建好,啥都配置好了,纳闷的是 Repositories 中的 group 为何总是空值?我还反反复复删了又重建,结果还是一样,不经意间再看 Configuration 选项卡的内容,发现左右两个 ...

  7. bzoj 1209

    三维凸包裸题. 1.通过volume计算有向体积,判断点与面的位置关系. 2.噪声 /********************************************************* ...

  8. 拆分Cocos2dx渲染部分代码

    纹理实现 思想 这个是Cocos2dx的渲染部分的最基本的实现,被我拆分到mac上,但是并不是用的EGLContext,而是搭配glfw,还有soil第三方图形库. 实现 // // main.cpp ...

  9. SGU 405 Totalizator

    405. Totalizator Time limit per test: 0.25 second(s)Memory limit: 65536 kilobytes input: standardout ...

  10. Antd前端开发采坑记录

    背景 基于页面友好,界面整洁美观:基于Antd框架开发虾能平台 选型 基于Antd-admin工程架构,进行开发:基于Antd+React+Umj 采坑记录 按照Html方式天机onClick方法,每 ...