/** Android Wheel Control.
*https://code.google.com/p/android-wheel/*
* Copyright 2010 Yuri Kanivets
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*http://www.apache.org/licenses/LICENSE-2.0*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.*/
packagecn.newcom.wheelview;/*** 自定义控件*/
importjava.util.LinkedList;importjava.util.List;importcn.newcom.pickdemo.R;importcn.newcom.pickdemo.R.drawable;importandroid.content.Context;importandroid.graphics.Canvas;importandroid.graphics.Paint;importandroid.graphics.Rect;importandroid.graphics.drawable.Drawable;importandroid.graphics.drawable.GradientDrawable;importandroid.graphics.drawable.GradientDrawable.Orientation;importandroid.os.Handler;importandroid.os.Message;importandroid.text.Layout;importandroid.text.StaticLayout;importandroid.text.TextPaint;importandroid.util.AttributeSet;importandroid.util.FloatMath;importandroid.view.GestureDetector;importandroid.view.GestureDetector.SimpleOnGestureListener;importandroid.view.MotionEvent;importandroid.view.View;importandroid.view.animation.Interpolator;importandroid.widget.Scroller;/*** Numeric wheel view.*/
public class WheelView extendsView {/**Scrolling duration*/
private static final int SCROLLING_DURATION = 400;/**Minimum delta for scrolling*/
private static final int MIN_DELTA_FOR_SCROLLING = 1;/**Current value & label text color*/
private static final int VALUE_TEXT_COLOR = 0xF0000000;/**Items text color*/
private static final int ITEMS_TEXT_COLOR = 0xFF000000;/**Top and bottom shadows colors*/
private static final int[] SHADOWS_COLORS = new int[] { 0xFF111111,0x00AAAAAA, 0x00AAAAAA};/**Additional items height (is added to standard text item height)*/
private static final int ADDITIONAL_ITEM_HEIGHT = 15;/**Text size*/
private static final int TEXT_SIZE = 24;/**Top and bottom items offset (to hide that)*/
private static final int ITEM_OFFSET = TEXT_SIZE / 5;/**Additional width for items layout*/
private static final int ADDITIONAL_ITEMS_SPACE = 10;/**Label offset*/
private static final int LABEL_OFFSET = 8;/**Left and right padding value*/
private static final int PADDING = 10;/**Default count of visible items*/
private static final int DEF_VISIBLE_ITEMS = 5;//Wheel Values
private WheelAdapter adapter = null;private int currentItem = 0;//Widths
private int itemsWidth = 0;private int labelWidth = 0;//Count of visible items
private int visibleItems =DEF_VISIBLE_ITEMS;//Item height
private int itemHeight = 0;//Text paints
privateTextPaint itemsPaint;privateTextPaint valuePaint;//Layouts
privateStaticLayout itemsLayout;privateStaticLayout labelLayout;privateStaticLayout valueLayout;//Label & background
privateString label;privateDrawable centerDrawable;//Shadows drawables
privateGradientDrawable topShadow;privateGradientDrawable bottomShadow;//Scrolling
private booleanisScrollingPerformed;private intscrollingOffset;//Scrolling animation
privateGestureDetector gestureDetector;privateScroller scroller;private intlastScrollY;//Cyclic
boolean isCyclic = false;//Listeners
private List changingListeners = new LinkedList();private List scrollingListeners = new LinkedList();/*** Constructor*/
public WheelView(Context context, AttributeSet attrs, intdefStyle) {super(context, attrs, defStyle);
initData(context);
}/*** Constructor*/
publicWheelView(Context context, AttributeSet attrs) {super(context, attrs);
initData(context);
}/*** Constructor*/
publicWheelView(Context context) {super(context);
initData(context);
}/*** Initializes class data
*@paramcontext the context*/
private voidinitData(Context context) {
gestureDetector= newGestureDetector(context, gestureListener);
gestureDetector.setIsLongpressEnabled(false);
scroller= newScroller(context);
}/*** Gets wheel adapter
*@returnthe adapter*/
publicWheelAdapter getAdapter() {returnadapter;
}/*** Sets wheel adapter
*@paramadapter the new wheel adapter*/
public voidsetAdapter(WheelAdapter adapter) {this.adapter =adapter;
invalidateLayouts();
invalidate();
}/*** Set the the specified scrolling interpolator
*@paraminterpolator the interpolator*/
public voidsetInterpolator(Interpolator interpolator) {
scroller.forceFinished(true);
scroller= newScroller(getContext(), interpolator);
}/*** Gets count of visible items
*
*@returnthe count of visible items*/
public intgetVisibleItems() {returnvisibleItems;
}/*** Sets count of visible items
*
*@paramcount
* the new count*/
public void setVisibleItems(intcount) {
visibleItems=count;
invalidate();
}/*** Gets label
*
*@returnthe label*/
publicString getLabel() {returnlabel;
}/*** Sets label
*
*@paramnewLabel
* the label to set*/
public voidsetLabel(String newLabel) {if (label == null || !label.equals(newLabel)) {
label=newLabel;
labelLayout= null;
invalidate();
}
}/*** Adds wheel changing listener
*@paramlistener the listener*/
public voidaddChangingListener(OnWheelChangedListener listener) {
changingListeners.add(listener);
}/*** Removes wheel changing listener
*@paramlistener the listener*/
public voidremoveChangingListener(OnWheelChangedListener listener) {
changingListeners.remove(listener);
}/*** Notifies changing listeners
*@paramoldValue the old wheel value
*@paramnewValue the new wheel value*/
protected void notifyChangingListeners(int oldValue, intnewValue) {for(OnWheelChangedListener listener : changingListeners) {
listener.onChanged(this, oldValue, newValue);
}
}/*** Adds wheel scrolling listener
*@paramlistener the listener*/
public voidaddScrollingListener(OnWheelScrollListener listener) {
scrollingListeners.add(listener);
}/*** Removes wheel scrolling listener
*@paramlistener the listener*/
public voidremoveScrollingListener(OnWheelScrollListener listener) {
scrollingListeners.remove(listener);
}/*** Notifies listeners about starting scrolling*/
protected voidnotifyScrollingListenersAboutStart() {for(OnWheelScrollListener listener : scrollingListeners) {
listener.onScrollingStarted(this);
}
}/*** Notifies listeners about ending scrolling*/
protected voidnotifyScrollingListenersAboutEnd() {for(OnWheelScrollListener listener : scrollingListeners) {
listener.onScrollingFinished(this);
}
}/*** Gets current value
*
*@returnthe current value*/
public intgetCurrentItem() {returncurrentItem;
}/*** Sets the current item. Does nothing when index is wrong.
*
*@paramindex the item index
*@paramanimated the animation flag*/
public void setCurrentItem(int index, booleananimated) {if (adapter == null || adapter.getItemsCount() == 0) {return; //throw?
}if (index < 0 || index >=adapter.getItemsCount()) {if(isCyclic) {while (index < 0) {
index+=adapter.getItemsCount();
}
index%=adapter.getItemsCount();
}else{return; //throw?
}
}if (index !=currentItem) {if(animated) {
scroll(index-currentItem, SCROLLING_DURATION);
}else{
invalidateLayouts();int old =currentItem;
currentItem=index;
notifyChangingListeners(old, currentItem);
invalidate();
}
}
}/*** Sets the current item w/o animation. Does nothing when index is wrong.
*
*@paramindex the item index*/
public void setCurrentItem(intindex) {
setCurrentItem(index,false);
}/*** Tests if wheel is cyclic. That means before the 1st item there is shown the last one
*@returntrue if wheel is cyclic*/
public booleanisCyclic() {returnisCyclic;
}/*** Set wheel cyclic flag
*@paramisCyclic the flag to set*/
public void setCyclic(booleanisCyclic) {this.isCyclic =isCyclic;
invalidate();
invalidateLayouts();
}/*** Invalidates layouts*/
private voidinvalidateLayouts() {
itemsLayout= null;
valueLayout= null;
scrollingOffset= 0;
}/*** Initializes resources*/
private voidinitResourcesIfNecessary() {if (itemsPaint == null) {
itemsPaint= newTextPaint(Paint.ANTI_ALIAS_FLAG|Paint.FAKE_BOLD_TEXT_FLAG);//itemsPaint.density = getResources().getDisplayMetrics().density;
itemsPaint.setTextSize(TEXT_SIZE);
}if (valuePaint == null) {
valuePaint= newTextPaint(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= newGradientDrawable(Orientation.TOP_BOTTOM, SHADOWS_COLORS);
}if (bottomShadow == null) {
bottomShadow= newGradientDrawable(Orientation.BOTTOM_TOP, SHADOWS_COLORS);
}
setBackgroundResource(R.drawable.wheel_bg);
}/*** Calculates desired height for layout
*
*@paramlayout
* the source layout
*@returnthe desired layout height*/
private intgetDesiredHeight(Layout layout) {if (layout == null) {return 0;
}int desired = getItemHeight() * visibleItems - ITEM_OFFSET * 2
-ADDITIONAL_ITEM_HEIGHT;//Check against our minimum height
desired =Math.max(desired, getSuggestedMinimumHeight());returndesired;
}/*** Returns text item by index
*@paramindex the item index
*@returnthe item or null*/
private String getTextItem(intindex) {if (adapter == null || adapter.getItemsCount() == 0) {return null;
}int count =adapter.getItemsCount();if ((index < 0 || index >= count) && !isCyclic) {return null;
}else{while (index < 0) {
index= count +index;
}
}
index%=count;returnadapter.getItem(index);
}/*** Builds text depending on current value
*
*@paramuseCurrentValue
*@returnthe text*/
private String buildText(booleanuseCurrentValue) {
StringBuilder itemsText= newStringBuilder();int addItems = visibleItems / 2 + 1;for (int i = currentItem - addItems; i <= currentItem + addItems; i++) {if (useCurrentValue || i !=currentItem) {
String text=getTextItem(i);if (text != null) {
itemsText.append(text);
}
}if (i < currentItem +addItems) {
itemsText.append("\n");
}
}returnitemsText.toString();
}/*** Returns the max item length that can be present
*@returnthe max length*/
private intgetMaxTextLength() {
WheelAdapter adapter=getAdapter();if (adapter == null) {return 0;
}int adapterLength =adapter.getMaximumLength();if (adapterLength > 0) {returnadapterLength;
}
String maxText= null;int addItems = visibleItems / 2;for (int i = Math.max(currentItem - addItems, 0);
i< Math.min(currentItem + visibleItems, adapter.getItemsCount()); i++) {
String text=adapter.getItem(i);if (text != null && (maxText == null || maxText.length()
maxText=text;
}
}return maxText != null ? maxText.length() : 0;
}/*** Returns height of wheel item
*@returnthe item height*/
private intgetItemHeight() {if (itemHeight != 0) {returnitemHeight;
}else if (itemsLayout != null && itemsLayout.getLineCount() > 2) {
itemHeight= itemsLayout.getLineTop(2) - itemsLayout.getLineTop(1);returnitemHeight;
}return getHeight() /visibleItems;
}/*** Calculates control width and creates text layouts
*@paramwidthSize the input layout width
*@parammode the layout mode
*@returnthe calculated control width*/
private int calculateLayoutWidth(int widthSize, intmode) {
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=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);
}returnwidth;
}/*** Creates layouts
*@paramwidthItems width of items layout
*@paramwidthLabel width of label layout*/
private void createLayouts(int widthItems, intwidthLabel) {if (itemsLayout == null || itemsLayout.getWidth() >widthItems) {
itemsLayout= newStaticLayout(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= newStaticLayout(label, valuePaint,
widthLabel, Layout.Alignment.ALIGN_NORMAL,1,
ADDITIONAL_ITEM_HEIGHT,false);
}else{
labelLayout.increaseWidthTo(widthLabel);
}
}
}
@Overrideprotected void onMeasure(int widthMeasureSpec, intheightMeasureSpec) {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);intheight;if (heightMode ==MeasureSpec.EXACTLY) {
height=heightSize;
}else{
height=getDesiredHeight(itemsLayout);if (heightMode ==MeasureSpec.AT_MOST) {
height=Math.min(height, heightSize);
}
}
setMeasuredDimension(width, height);
}
@Overrideprotected voidonDraw(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);
}/*** Draws shadows on top and bottom of control
*@paramcanvas the canvas for drawing*/
private voiddrawShadows(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
*@paramcanvas the canvas for drawing*/
private voiddrawValue(Canvas canvas) {
valuePaint.setColor(VALUE_TEXT_COLOR);
valuePaint.drawableState=getDrawableState();
Rect bounds= newRect();
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
*@paramcanvas the canvas for drawing*/
private voiddrawItems(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
*@paramcanvas the canvas for drawing*/
private voiddrawCenterRect(Canvas canvas) {int center = getHeight() / 2;int offset = getItemHeight() / 2;
centerDrawable.setBounds(0, center - offset, getWidth(), center +offset);
centerDrawable.draw(canvas);
}
@Overridepublic booleanonTouchEvent(MotionEvent event) {
WheelAdapter adapter=getAdapter();if (adapter == null) {return true;
}if (!gestureDetector.onTouchEvent(event) && event.getAction() ==MotionEvent.ACTION_UP) {
justify();
}return true;
}/*** Scrolls the wheel
*@paramdelta the scrolling value*/
private void doScroll(intdelta) {
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();
}
}//gesture listener
private SimpleOnGestureListener gestureListener = newSimpleOnGestureListener() {public booleanonDown(MotionEvent e) {if(isScrollingPerformed) {
scroller.forceFinished(true);
clearMessages();return true;
}return false;
}public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, floatdistanceY) {
startScrolling();
doScroll((int)-distanceY);return true;
}public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, floatvelocityY) {
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;
}
};//Messages
private final int MESSAGE_SCROLL = 0;private final int MESSAGE_JUSTIFY = 1;/*** Set next message to queue. Clears queue before.
*
*@parammessage the message to set*/
private void setNextMessage(intmessage) {
clearMessages();
animationHandler.sendEmptyMessage(message);
}/*** Clears messages from queue*/
private voidclearMessages() {
animationHandler.removeMessages(MESSAGE_SCROLL);
animationHandler.removeMessages(MESSAGE_JUSTIFY);
}//animation handler
private Handler animationHandler = newHandler() {public voidhandleMessage(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())
currY=scroller.getFinalY();
scroller.forceFinished(true);
}if (!scroller.isFinished()) {
animationHandler.sendEmptyMessage(msg.what);
}else if (msg.what ==MESSAGE_SCROLL) {
justify();
}else{
finishScrolling();
}
}
};/*** Justifies wheel*/
private voidjustify() {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;elseoffset-= 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();
}
}/*** Starts scrolling*/
private voidstartScrolling() {if (!isScrollingPerformed) {
isScrollingPerformed= true;
notifyScrollingListenersAboutStart();
}
}/*** Finishes scrolling*/
voidfinishScrolling() {if(isScrollingPerformed) {
notifyScrollingListenersAboutEnd();
isScrollingPerformed= false;
}
invalidateLayouts();
invalidate();
}/*** Scroll the wheel
*@paramitemsToSkip items to scroll
*@paramtime scrolling duration*/
public void scroll(int itemsToScroll, inttime) {
scroller.forceFinished(true);
lastScrollY=scrollingOffset;int offset = itemsToScroll *getItemHeight();
scroller.startScroll(0, lastScrollY, 0, offset -lastScrollY, time);
setNextMessage(MESSAGE_SCROLL);
startScrolling();
}
}