Android进阶篇-时间滑动控件

Android进阶篇-时间滑动控件

仿Iphone时间选择滑动控件:

WheelView.java:

/**
 * @author Administrator
 * 
 * 时间滑动滚轮
 */
public class WheelView extends View{
    /** 手势监听控件  **/
    private GestureDetector gestureDetector;
    /** 滑动管理类   **/
    private Scroller scroller;
    /** 适配器  **/
    private WheelAdapter adapter;
    
    private StaticLayout itemsLayout;
    private StaticLayout labelLayout;
    private StaticLayout valueLayout;
    
    /** 滑动偏移量   **/
    private int scrollingOffset;
    /** 当前的Item **/
    private int currentItem = 0;
    
    /** 默认的可见的Item个数   **/
    private static final int DEF_VISIBLE_ITEMS = 5;
    private int visibleItems = DEF_VISIBLE_ITEMS;
    
    /** 标签  **/
    private String label;
    /** 图片  **/
    private Drawable centerDrawable;
    /** 是否回收  **/
    private boolean isCyclic = false;
    
    private List<OnWheelChangedListener> changingListeners = new LinkedList<OnWheelChangedListener>();
    private List<OnWheelScrollListener> scrollingListeners = new LinkedList<OnWheelScrollListener>();
    
    /** 滑动的频率  **/
    private static final int SCROLLING_DURATION = 400;
    /** 文字大小  */
    private static final int TEXT_SIZE = 24;
    
    private TextPaint itemsPaint;
    private TextPaint valuePaint;
    
    private GradientDrawable topShadow;
    private GradientDrawable bottomShadow;
    private static final int[] SHADOWS_COLORS = new int[] { 0xFF111111,
        0x00AAAAAA, 0x00AAAAAA };
    
    private static final int ITEM_OFFSET = TEXT_SIZE / 5;
    private static final int ADDITIONAL_ITEM_HEIGHT = 15;
    private static final int LABEL_OFFSET = 8;
    private static final int ADDITIONAL_ITEMS_SPACE = 10;
    private static final int PADDING = 10;
    
    private int itemHeight = 0;
    private int itemsWidth = 0;
    private int labelWidth = 0;
    
    private int lastScrollY;
    
    // Messages
    private final int MESSAGE_SCROLL = 0;
    private final int MESSAGE_JUSTIFY = 1;
    
    private boolean isScrollingPerformed; 
    private static final int MIN_DELTA_FOR_SCROLLING = 1;
    
    private static final int ITEMS_TEXT_COLOR = 0xFF000000;
    private static final int VALUE_TEXT_COLOR = 0xF0000000;
    
    public WheelView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        // TODO Auto-generated constructor stub
        init();
    }

    public WheelView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // TODO Auto-generated constructor stub
        init();
    }

    public WheelView(Context context) {
        super(context);
        // TODO Auto-generated constructor stub
        init();
    }
    
    private void init() {
        gestureDetector = new GestureDetector(getContext(), gestureListener);
        gestureDetector.setIsLongpressEnabled(false);
        scroller = new Scroller(getContext());
    }
    
    public WheelAdapter getAdapter() {
        return adapter;
    }
    
    public void setAdapter(WheelAdapter adapter) {
        this.adapter = adapter;
        invalidateLayouts();
        invalidate();
    }
    
    /**
     * @param interpolator 动画插补器
     */
    public void setInterpolator(Interpolator interpolator){
        // 立即停止滑动
        scroller.forceFinished(true);
        scroller = new Scroller(getContext(),interpolator);
    }
    
    /**
     * 初始布局
     */
    private void invalidateLayouts() {
        itemsLayout = null;
        valueLayout = null;
        scrollingOffset = 0;
    }
    
    public int getVisiableItems() {
        return visibleItems;
    }
    
    public void setVisiableItems(int count) {
        visibleItems = count;
        invalidate();
    }
    
    public String getLabel() {
        return label;
    }
    
    public void setLabel(String newLabel) {
        if(label == null || !label.equals(newLabel)) {
            label = newLabel;
            labelLayout = null;
            invalidate();
         }
    }
    
    public void addChangingListener(OnWheelChangedListener listener){
        changingListeners.add(listener);
    }
    
    public void removeChangingListener(OnWheelChangedListener listener) {
        changingListeners.remove(listener);
    }

    /**
     * 更新监听
     * 
     * @param oldValue
     * @param newValue
     */
    protected void notifyChangingListeners(int oldValue, int newValue) {
        for (OnWheelChangedListener listener : changingListeners) {
            listener.onChanged(this, oldValue, newValue);
        }
    }

    public void addScrollingListener(OnWheelScrollListener listener) {
        scrollingListeners.add(listener);
    }
    
    public void removeScrollingListener(OnWheelScrollListener listener) {
        scrollingListeners.remove(listener);
    }
    
    /**
     * 更新滑动监听
     */
    protected void notifyScrollingListenersAboutStart() {
        for (OnWheelScrollListener listener : scrollingListeners) {
            listener.onScrollingStarted(this);
        }
    }
    
    protected void notifyScrollingListenersAboutEnd() {
        for (OnWheelScrollListener listener : scrollingListeners) {
            listener.onScrollingFinished(this);
        }
    }

    public int getCurrentItem() {
        return currentItem;
    }
    
    /**
     * 设置当前的Item 
     * 
     * @param index 下标
     * @param animated 是否开启动画效果
     */
    public void setCurrentItem(int index,boolean animated) {
        if(adapter == null || (adapter != null && adapter.getItemCount() == 0)) {
            return;
        }
        
        if(index < 0 || index >= adapter.getItemCount()) {
            if (isCyclic) {
                while (index < 0) {
                    index += adapter.getItemCount();
                }
                index %= adapter.getItemCount();
            } else{
                return;
            }
        } 
        
        if (index != currentItem) {
            if (animated) {
                scroll(index - currentItem, SCROLLING_DURATION);
            } else {
                invalidateLayouts();            
                int old = currentItem;
                currentItem = index;            
                notifyChangingListeners(old, currentItem);            
                invalidate();
            }
        }
    }
    
    public void setCurrentItem(int index) {
        setCurrentItem(index, false);
    }
    
    public boolean isCyclic() {
        return isCyclic;
    }
    
    public void setCyclic(boolean isCyclis) {
        this.isCyclic = isCyclis;
        invalidateLayouts();    
        invalidate();
    }
    
    private void initResourcesIfNecessary() {
        if(itemsPaint == null) {
            /** 设置粗体  抗锯齿   **/
            itemsPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FAKE_BOLD_TEXT_FLAG);
            itemsPaint.setTextSize(TEXT_SIZE);
        }
        
        if(valuePaint == null) {
            /** 设置粗体 抗锯齿  递色   **/
            valuePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG
                    | Paint.FAKE_BOLD_TEXT_FLAG | Paint.DITHER_FLAG);
            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);
    }
    
    /**
     * 计算布局的高度
     * 
     * @param layout
     * @return
     */
    private int getDesiredHeight(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());

        return desired;
    }
    
    /**
     * 设置TextItem
     * 
     * @param index
     * @return
     */
    private String getTextItem(int index) {
        if (adapter == null || adapter.getItemCount() == 0) {
            return null;
        }
        int count = adapter.getItemCount();
        if ((index < 0 || index >= count) && !isCyclic) {
            return null;
        } else {
            while (index < 0) {
                index = count + index;
            }
        }
        
        index %= count;
        return (String) adapter.getItem(index);
    }

    private String buildText(boolean useCurrentValue) {
        StringBuilder itemsText = new StringBuilder();
        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("
");
            }
        }
        
        return itemsText.toString();
    }
    
    private int getMaxTextLength() {
        WheelAdapter adapter = getAdapter();
        if (adapter == null) {
            return 0;
        }
        
        int adapterLength = adapter.getMaxinumLength();
        if (adapterLength > 0) {
            return adapterLength;
        }

        String maxText = null;
        int addItems = visibleItems / 2;
        for (int i = Math.max(currentItem - addItems, 0);
                i < Math.min(currentItem + visibleItems, adapter.getItemCount()); i++) {
            String text = (String) adapter.getItem(i);
            if (text != null && (maxText == null || maxText.length() < text.length())) {
                maxText = text;
            }
        }

        return maxText != null ? maxText.length() : 0;
    }
    
    private int getItemHeight() {
        if (itemHeight != 0) {
            return itemHeight;
        } else if (itemsLayout != null && itemsLayout.getLineCount() > 2) {
            itemHeight = itemsLayout.getLineTop(2) - itemsLayout.getLineTop(1);
            return itemHeight;
        }
        
        return getHeight() / visibleItems;
    }
    
    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;
    }
    
    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 = (String) (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);
    }
    
    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);
    }

    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();
        }
    }
    
    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();
    }
    
    private void drawCenterRect(Canvas canvas) {
        int center = getHeight() / 2;
        int offset = getItemHeight() / 2;
        centerDrawable.setBounds(0, center - offset, getWidth(), center + offset);
        centerDrawable.draw(canvas);
    }
    
    /**
     * 拦截onTouchEvent
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // TODO Auto-generated method stub
        WheelAdapter adapter = getAdapter();
        if (adapter == null) {
            return true;
        }
        
        if (!gestureDetector.onTouchEvent(event) 
                && event.getAction() == MotionEvent.ACTION_UP) {
            justify();    
        }
        return true;
    }

    /**
     * 滑动
     * 
     * @param delta
     */
    private void doScroll(int delta) {
        scrollingOffset += delta;
        
        int count = scrollingOffset / getItemHeight();
        int pos = currentItem - count;
        if (isCyclic && adapter.getItemCount() > 0) {
            // fix position by rotating
            while (pos < 0) {
                pos += adapter.getItemCount();
            }
            pos %= adapter.getItemCount();
        } else if (isScrollingPerformed) {
            // 
            if (pos < 0) {
                count = currentItem;
                pos = 0;
            } else if (pos >= adapter.getItemCount()) {
                count = currentItem - adapter.getItemCount() + 1;
                pos = adapter.getItemCount() - 1;
            }
        } else {
            // fix position
            pos = Math.max(pos, 0);
            pos = Math.min(pos, adapter.getItemCount() - 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(){

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2,
                float distanceX, float distanceY) {
            // TODO Auto-generated method stub
            startScrolling();
            doScroll((int)-distanceY);
            return true;
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
                float velocityY) {
            // TODO Auto-generated method stub
            lastScrollY = currentItem * getItemHeight() + scrollingOffset;
            int maxY = isCyclic ? 0x7FFFFFFF : adapter.getItemCount() * getItemHeight();
            int minY = isCyclic ? -maxY : 0;
            scroller.fling(0, lastScrollY, 0, (int) -velocityY / 2, 0, 0, minY, maxY);
            setNextMessage(MESSAGE_SCROLL);
            return true;
        }

        @Override
        public boolean onDown(MotionEvent e) {
            // TODO Auto-generated method stub
            if (isScrollingPerformed) {
                scroller.forceFinished(true);
                clearMessages();
                return true;
            }
            return false;
        }
    };
    
    private void setNextMessage(int message) {
        clearMessages();
        animationHandler.sendEmptyMessage(message);
    }
    
    /**
     * 清空Message
     */
    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);
            }
            
            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.getItemCount() : 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 itemsToScroll
     * @param time
     */
    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();
    }
}

WheelAdapter.java:

public interface WheelAdapter {
    /** 获取Size **/
    public int getItemCount();
    /** 获取Item对象  **/
    public Object getItem(int index);
    /** 获取每个Item的最大长度  用来设置画的时候的宽度  **/
    public int getMaxinumLength();
}

OnWheelScrollListener.java:

public interface OnWheelScrollListener {
    
    void onScrollingStarted(WheelView wheel);
    
    void onScrollingFinished(WheelView wheel);
}

OnWheelChangedListener.java:

public interface OnWheelChangedListener {
    
    void onChanged(WheelView wheel, int oldValue, int newValue);
}

适配器NumberWheelAdapter.java:

public class NumberWheelAdapter implements WheelAdapter{
    /** The default min value */
    public static final int DEFAULT_MAX_VALUE = 9;

    /** The default max value */
    private static final int DEFAULT_MIN_VALUE = 0;
        
    // Values
    private int minValue;
    private int maxValue;
    
    // format
    private String format;
    
    public NumberWheelAdapter() {
        this(DEFAULT_MIN_VALUE, DEFAULT_MAX_VALUE);
    }
    
    public NumberWheelAdapter(int minValue, int maxValue) {
        this(minValue, maxValue, null);
    }

    public NumberWheelAdapter(int minValue, int maxValue, String format) {
        this.minValue = minValue;
        this.maxValue = maxValue;
        this.format = format;
    }
    
    
    @Override
    public int getItemCount() {
        // TODO Auto-generated method stub
        return maxValue - minValue + 1;
    }

    @Override
    public Object getItem(int index) {
        // TODO Auto-generated method stub
        if (index >= 0 && index < getItemCount()) {
            int value = minValue + index;
            return format != null ? String.format(format, value) : Integer.toString(value);
        }
        return null;
    }

    @Override
    public int getMaxinumLength() {
        // TODO Auto-generated method stub
        int max = Math.max(Math.abs(maxValue), Math.abs(minValue));
        int maxLen = Integer.toString(max).length();
        if (minValue < 0) {
            maxLen++;
        }
        return maxLen;
    }
    
}

activity调用:

        mMonthWheel = (WheelView) findViewById(R.id.hour);
        mDayWheel = (WheelView) findViewById(R.id.mins);
        
        mMonthWheel.setAdapter(new NumberWheelAdapter(1, 12));
        mMonthWheel.setLabel("");
        
        mDayWheel.setAdapter(new NumberWheelAdapter(1, 31));
        mDayWheel.setLabel("");
        mDayWheel.setCyclic(true);