Android Open Source - android-wheel Wheel View






From Project

Back to project page android-wheel.

License

The source code is released under:

Apache License

If you think the Android project android-wheel listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 *  Android Wheel Control.//from w  w  w  .j  ava  2 s  .c  o m
 *  https://code.google.com/p/android-wheel/
 *  
 *  Copyright 2011 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.
 */

package kankan.wheel.widget;

import java.util.LinkedList;
import java.util.List;

import kankan.wheel.R;
import kankan.wheel.widget.adapters.WheelViewAdapter;
import android.content.Context;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.GradientDrawable.Orientation;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.animation.Interpolator;
import android.widget.LinearLayout;

/**
 * Numeric wheel view.
 * 
 * @author Yuri Kanivets
 */
public class WheelView extends View {

  /** Top and bottom shadows colors */
  private static final int[] SHADOWS_COLORS = new int[] { 0xFF111111,
      0x00AAAAAA, 0x00AAAAAA };

  /** Top and bottom items offset (to hide that) */
  private static final int ITEM_OFFSET_PERCENT = 10;

  /** 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 int currentItem = 0;
  
  // Count of visible items
  private int visibleItems = DEF_VISIBLE_ITEMS;
  
  // Item height
  private int itemHeight = 0;

  // Center Line
  private Drawable centerDrawable;

  // Shadows drawables
  private GradientDrawable topShadow;
  private GradientDrawable bottomShadow;

  // Scrolling
  private WheelScroller scroller;
    private boolean isScrollingPerformed; 
    private int scrollingOffset;

  // Cyclic
  boolean isCyclic = false;
  
  // Items layout
  private LinearLayout itemsLayout;
  
  // The number of first item in layout
  private int firstItem;

  // View adapter
  private WheelViewAdapter viewAdapter;
  
  // Recycle
  private WheelRecycle recycle = new WheelRecycle(this);

  // Listeners
  private List<OnWheelChangedListener> changingListeners = new LinkedList<OnWheelChangedListener>();
  private List<OnWheelScrollListener> scrollingListeners = new LinkedList<OnWheelScrollListener>();
    private List<OnWheelClickedListener> clickingListeners = new LinkedList<OnWheelClickedListener>();

  /**
   * Constructor
   */
  public WheelView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    initData(context);
  }

  /**
   * Constructor
   */
  public WheelView(Context context, AttributeSet attrs) {
    super(context, attrs);
    initData(context);
  }

  /**
   * Constructor
   */
  public WheelView(Context context) {
    super(context);
    initData(context);
  }
  
  /**
   * Initializes class data
   * @param context the context
   */
  private void initData(Context context) {
      scroller = new WheelScroller(getContext(), scrollingListener);
  }
  
  // Scrolling listener
  WheelScroller.ScrollingListener scrollingListener = new WheelScroller.ScrollingListener() {
        public void onStarted() {
            isScrollingPerformed = true;
            notifyScrollingListenersAboutStart();
        }
        
        public void onScroll(int distance) {
            doScroll(distance);
            
            int height = getHeight();
            if (scrollingOffset > height) {
                scrollingOffset = height;
                scroller.stopScrolling();
            } else if (scrollingOffset < -height) {
                scrollingOffset = -height;
                scroller.stopScrolling();
            }
        }
        
        public void onFinished() {
            if (isScrollingPerformed) {
                notifyScrollingListenersAboutEnd();
                isScrollingPerformed = false;
            }
            
            scrollingOffset = 0;
            invalidate();
        }

        public void onJustify() {
            if (Math.abs(scrollingOffset) > WheelScroller.MIN_DELTA_FOR_SCROLLING) {
                scroller.scroll(scrollingOffset, 0);
            }
        }
    };
  
  /**
   * Set the the specified scrolling interpolator
   * @param interpolator the interpolator
   */
  public void setInterpolator(Interpolator interpolator) {
    scroller.setInterpolator(interpolator);
  }
  
  /**
   * Gets count of visible items
   * 
   * @return the count of visible items
   */
  public int getVisibleItems() {
    return visibleItems;
  }

  /**
   * Sets the desired count of visible items.
   * Actual amount of visible items depends on wheel layout parameters.
   * To apply changes and rebuild view call measure(). 
   * 
   * @param count the desired count for visible items
   */
  public void setVisibleItems(int count) {
    visibleItems = count;
  }

  /**
   * Gets view adapter
   * @return the view adapter
   */
  public WheelViewAdapter getViewAdapter() {
    return viewAdapter;
  }

  // Adapter listener
    private DataSetObserver dataObserver = new DataSetObserver() {
        @Override
        public void onChanged() {
            invalidateWheel(false);
        }

        @Override
        public void onInvalidated() {
            invalidateWheel(true);
        }
    };

  /**
   * Sets view adapter. Usually new adapters contain different views, so
   * it needs to rebuild view by calling measure().
   *  
   * @param viewAdapter the view adapter
   */
  public void setViewAdapter(WheelViewAdapter viewAdapter) {
      if (this.viewAdapter != null) {
          this.viewAdapter.unregisterDataSetObserver(dataObserver);
      }
        this.viewAdapter = viewAdapter;
        if (this.viewAdapter != null) {
            this.viewAdapter.registerDataSetObserver(dataObserver);
        }
        
        invalidateWheel(true);
  }
  
  /**
   * Adds wheel changing listener
   * @param listener the listener 
   */
  public void addChangingListener(OnWheelChangedListener listener) {
    changingListeners.add(listener);
  }

  /**
   * Removes wheel changing listener
   * @param listener the listener
   */
  public void removeChangingListener(OnWheelChangedListener listener) {
    changingListeners.remove(listener);
  }
  
  /**
   * Notifies changing listeners
   * @param oldValue the old wheel value
   * @param newValue the new wheel value
   */
  protected void notifyChangingListeners(int oldValue, int newValue) {
    for (OnWheelChangedListener listener : changingListeners) {
      listener.onChanged(this, oldValue, newValue);
    }
  }

  /**
   * Adds wheel scrolling listener
   * @param listener the listener 
   */
  public void addScrollingListener(OnWheelScrollListener listener) {
    scrollingListeners.add(listener);
  }

  /**
   * Removes wheel scrolling listener
   * @param listener the listener
   */
  public void removeScrollingListener(OnWheelScrollListener listener) {
    scrollingListeners.remove(listener);
  }
  
  /**
   * Notifies listeners about starting scrolling
   */
  protected void notifyScrollingListenersAboutStart() {
    for (OnWheelScrollListener listener : scrollingListeners) {
      listener.onScrollingStarted(this);
    }
  }

  /**
   * Notifies listeners about ending scrolling
   */
  protected void notifyScrollingListenersAboutEnd() {
    for (OnWheelScrollListener listener : scrollingListeners) {
      listener.onScrollingFinished(this);
    }
  }

    /**
     * Adds wheel clicking listener
     * @param listener the listener 
     */
    public void addClickingListener(OnWheelClickedListener listener) {
        clickingListeners.add(listener);
    }

    /**
     * Removes wheel clicking listener
     * @param listener the listener
     */
    public void removeClickingListener(OnWheelClickedListener listener) {
        clickingListeners.remove(listener);
    }
    
    /**
     * Notifies listeners about clicking
     */
    protected void notifyClickListenersAboutClick(int item) {
        for (OnWheelClickedListener listener : clickingListeners) {
            listener.onItemClicked(this, item);
        }
    }

  /**
   * Gets current value
   * 
   * @return the current value
   */
  public int getCurrentItem() {
    return currentItem;
  }

  /**
   * Sets the current item. Does nothing when index is wrong.
   * 
   * @param index the item index
   * @param animated the animation flag
   */
  public void setCurrentItem(int index, boolean animated) {
    if (viewAdapter == null || viewAdapter.getItemsCount() == 0) {
      return; // throw?
    }
    
    int itemCount = viewAdapter.getItemsCount();
    if (index < 0 || index >= itemCount) {
      if (isCyclic) {
        while (index < 0) {
          index += itemCount;
        }
        index %= itemCount;
      } else{
        return; // throw?
      }
    }
    if (index != currentItem) {
      if (animated) {
          int itemsToScroll = index - currentItem;
          if (isCyclic) {
              int scroll = itemCount + Math.min(index, currentItem) - Math.max(index, currentItem);
              if (scroll < Math.abs(itemsToScroll)) {
                  itemsToScroll = itemsToScroll < 0 ? scroll : -scroll;
              }
          }
        scroll(itemsToScroll, 0);
      } else {
        scrollingOffset = 0;
      
        int old = currentItem;
        currentItem = index;
      
        notifyChangingListeners(old, currentItem);
      
        invalidate();
      }
    }
  }

  /**
   * Sets the current item w/o animation. Does nothing when index is wrong.
   * 
   * @param index the item index
   */
  public void setCurrentItem(int index) {
    setCurrentItem(index, false);
  }  
  
  /**
   * Tests if wheel is cyclic. That means before the 1st item there is shown the last one
   * @return true if wheel is cyclic
   */
  public boolean isCyclic() {
    return isCyclic;
  }

  /**
   * Set wheel cyclic flag
   * @param isCyclic the flag to set
   */
  public void setCyclic(boolean isCyclic) {
    this.isCyclic = isCyclic;
    invalidateWheel(false);
  }
  
  /**
   * Invalidates wheel
   * @param clearCaches if true then cached views will be clear
   */
    public void invalidateWheel(boolean clearCaches) {
        if (clearCaches) {
            recycle.clearAll();
            if (itemsLayout != null) {
                itemsLayout.removeAllViews();
            }
            scrollingOffset = 0;
        } else if (itemsLayout != null) {
            // cache all items
          recycle.recycleItems(itemsLayout, firstItem, new ItemsRange());         
        }
        
        invalidate();
  }

  /**
   * Initializes resources
   */
  private void initResourcesIfNecessary() {
    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);
  }
  
  /**
   * Calculates desired height for layout
   * 
   * @param layout
   *            the source layout
   * @return the desired layout height
   */
  private int getDesiredHeight(LinearLayout layout) {
    if (layout != null && layout.getChildAt(0) != null) {
      itemHeight = layout.getChildAt(0).getMeasuredHeight();
    }

    int desired = itemHeight * visibleItems - itemHeight * ITEM_OFFSET_PERCENT / 50;

    return Math.max(desired, getSuggestedMinimumHeight());
  }

  /**
   * Returns height of wheel item
   * @return the item height
   */
  private int getItemHeight() {
    if (itemHeight != 0) {
      return itemHeight;
    }
    
    if (itemsLayout != null && itemsLayout.getChildAt(0) != null) {
      itemHeight = itemsLayout.getChildAt(0).getHeight();
      return itemHeight;
    }
    
    return getHeight() / visibleItems;
  }

  /**
   * Calculates control width and creates text layouts
   * @param widthSize the input layout width
   * @param mode the layout mode
   * @return the calculated control width
   */
  private int calculateLayoutWidth(int widthSize, int mode) {
    initResourcesIfNecessary();

    // TODO: make it static
    itemsLayout.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
      itemsLayout.measure(MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.UNSPECIFIED), 
                  MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    int width = itemsLayout.getMeasuredWidth();

    if (mode == MeasureSpec.EXACTLY) {
      width = widthSize;
    } else {
      width += 2 * PADDING;

      // Check against our minimum width
      width = Math.max(width, getSuggestedMinimumWidth());

      if (mode == MeasureSpec.AT_MOST && widthSize < width) {
        width = widthSize;
      }
    }
    
        itemsLayout.measure(MeasureSpec.makeMeasureSpec(width - 2 * PADDING, MeasureSpec.EXACTLY), 
                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));

    return width;
  }

  @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);

    buildViewForMeasuring();
    
    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 onLayout(boolean changed, int l, int t, int r, int b) {
      layout(r - l, b - t);
    }

    /**
     * Sets layouts width and height
     * @param width the layout width
     * @param height the layout height
     */
    private void layout(int width, int height) {
    int itemsWidth = width - 2 * PADDING;
    
    itemsLayout.layout(0, 0, itemsWidth, height);
    }

  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    
    if (viewAdapter != null && viewAdapter.getItemsCount() > 0) {
          updateView();

          drawItems(canvas);
          drawCenterRect(canvas);
    }
    
        drawShadows(canvas);
  }

  /**
   * Draws shadows on top and bottom of control
   * @param canvas the canvas for drawing
   */
  private void drawShadows(Canvas canvas) {
    int height = (int)(1.5 * getItemHeight());
    topShadow.setBounds(0, 0, getWidth(), height);
    topShadow.draw(canvas);

    bottomShadow.setBounds(0, getHeight() - height, getWidth(), getHeight());
    bottomShadow.draw(canvas);
  }

  /**
   * Draws items
   * @param canvas the canvas for drawing
   */
  private void drawItems(Canvas canvas) {
    canvas.save();
    
    int top = (currentItem - firstItem) * getItemHeight() + (getItemHeight() - getHeight()) / 2;
    canvas.translate(PADDING, - top + scrollingOffset);
    
    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 = (int) (getItemHeight() / 2 * 1.2);
    centerDrawable.setBounds(0, center - offset, getWidth(), center + offset);
    centerDrawable.draw(canvas);
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    if (!isEnabled() || getViewAdapter() == null) {
      return true;
    }
    
    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE:
            if (getParent() != null) {
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            break;
            
        case MotionEvent.ACTION_UP:
            if (!isScrollingPerformed) {
                int distance = (int) event.getY() - getHeight() / 2;
                if (distance > 0) {
                    distance += getItemHeight() / 2;
                } else {
                        distance -= getItemHeight() / 2;
                }
                int items = distance / getItemHeight();
                if (items != 0 && isValidItemIndex(currentItem + items)) {
                      notifyClickListenersAboutClick(currentItem + items);
                }
            }
            break;
    }

    return scroller.onTouchEvent(event);
  }
  
  /**
   * Scrolls the wheel
   * @param delta the scrolling value
   */
  private void doScroll(int delta) {
    scrollingOffset += delta;
    
    int itemHeight = getItemHeight();
    int count = scrollingOffset / itemHeight;

    int pos = currentItem - count;
    int itemCount = viewAdapter.getItemsCount();
    
      int fixPos = scrollingOffset % itemHeight;
      if (Math.abs(fixPos) <= itemHeight / 2) {
          fixPos = 0;
      }
    if (isCyclic && itemCount > 0) {
        if (fixPos > 0) {
            pos--;
                count++;
        } else if (fixPos < 0) {
            pos++;
            count--;
        }
      // fix position by rotating
      while (pos < 0) {
        pos += itemCount;
      }
      pos %= itemCount;
    } else {
      // 
      if (pos < 0) {
        count = currentItem;
        pos = 0;
      } else if (pos >= itemCount) {
        count = currentItem - itemCount + 1;
        pos = itemCount - 1;
      } else if (pos > 0 && fixPos > 0) {
                pos--;
                count++;
            } else if (pos < itemCount - 1 && fixPos < 0) {
                pos++;
                count--;
            }
    }
    
    int offset = scrollingOffset;
    if (pos != currentItem) {
      setCurrentItem(pos, false);
    } else {
      invalidate();
    }
    
    // update offset
    scrollingOffset = offset - count * itemHeight;
    if (scrollingOffset > getHeight()) {
      scrollingOffset = scrollingOffset % getHeight() + getHeight();
    }
  }
    
  /**
   * Scroll the wheel
   * @param itemsToSkip items to scroll
   * @param time scrolling duration
   */
  public void scroll(int itemsToScroll, int time) {
    int distance = itemsToScroll * getItemHeight() - scrollingOffset;
        scroller.scroll(distance, time);
  }
  
  /**
   * Calculates range for wheel items
   * @return the items range
   */
  private ItemsRange getItemsRange() {
        if (getItemHeight() == 0) {
            return null;
        }
        
    int first = currentItem;
    int count = 1;
    
    while (count * getItemHeight() < getHeight()) {
      first--;
      count += 2; // top + bottom items
    }
    
    if (scrollingOffset != 0) {
      if (scrollingOffset > 0) {
        first--;
      }
      count++;
      
      // process empty items above the first or below the second
      int emptyItems = scrollingOffset / getItemHeight();
      first -= emptyItems;
      count += Math.asin(emptyItems);
    }
    return new ItemsRange(first, count);
  }
  
  /**
   * Rebuilds wheel items if necessary. Caches all unused items.
   * 
   * @return true if items are rebuilt
   */
  private boolean rebuildItems() {
    boolean updated = false;
    ItemsRange range = getItemsRange();
    if (itemsLayout != null) {
      int first = recycle.recycleItems(itemsLayout, firstItem, range);
      updated = firstItem != first;
      firstItem = first;
    } else {
      createItemsLayout();
      updated = true;
    }
    
    if (!updated) {
      updated = firstItem != range.getFirst() || itemsLayout.getChildCount() != range.getCount();
    }
    
    if (firstItem > range.getFirst() && firstItem <= range.getLast()) {
      for (int i = firstItem - 1; i >= range.getFirst(); i--) {
        if (!addViewItem(i, true)) {
            break;
        }
        firstItem = i;
      }      
    } else {
        firstItem = range.getFirst();
    }
    
    int first = firstItem;
    for (int i = itemsLayout.getChildCount(); i < range.getCount(); i++) {
      if (!addViewItem(firstItem + i, false) && itemsLayout.getChildCount() == 0) {
          first++;
      }
    }
    firstItem = first;
    
    return updated;
  }
  
  /**
   * Updates view. Rebuilds items and label if necessary, recalculate items sizes.
   */
  private void updateView() {
    if (rebuildItems()) {
      calculateLayoutWidth(getWidth(), MeasureSpec.EXACTLY);
      layout(getWidth(), getHeight());
    }
  }

  /**
   * Creates item layouts if necessary
   */
  private void createItemsLayout() {
    if (itemsLayout == null) {
      itemsLayout = new LinearLayout(getContext());
      itemsLayout.setOrientation(LinearLayout.VERTICAL);
    }
  }

  /**
   * Builds view for measuring
   */
  private void buildViewForMeasuring() {
    // clear all items
    if (itemsLayout != null) {
      recycle.recycleItems(itemsLayout, firstItem, new ItemsRange());      
    } else {
      createItemsLayout();
    }
    
    // add views
    int addItems = visibleItems / 2;
    for (int i = currentItem + addItems; i >= currentItem - addItems; i--) {
      if (addViewItem(i, true)) {
          firstItem = i;
      }
    }
  }

  /**
   * Adds view for item to items layout
   * @param index the item index
   * @param first the flag indicates if view should be first
   * @return true if corresponding item exists and is added
   */
  private boolean addViewItem(int index, boolean first) {
    View view = getItemView(index);
    if (view != null) {
      if (first) {
        itemsLayout.addView(view, 0);
      } else {
        itemsLayout.addView(view);
      }
      
      return true;
    }
    
    return false;
  }
  
  /**
   * Checks whether intem index is valid
   * @param index the item index
   * @return true if item index is not out of bounds or the wheel is cyclic
   */
  private boolean isValidItemIndex(int index) {
      return viewAdapter != null && viewAdapter.getItemsCount() > 0 &&
          (isCyclic || index >= 0 && index < viewAdapter.getItemsCount());
  }
  
  /**
   * Returns view for specified item
   * @param index the item index
   * @return item view or empty view if index is out of bounds
   */
    private View getItemView(int index) {
    if (viewAdapter == null || viewAdapter.getItemsCount() == 0) {
      return null;
    }
    int count = viewAdapter.getItemsCount();
    if (!isValidItemIndex(index)) {
      return viewAdapter.getEmptyItem(recycle.getEmptyItem(), itemsLayout);
    } else {
      while (index < 0) {
        index = count + index;
      }
    }
    
    index %= count;
    return viewAdapter.getItem(index, recycle.getItem(), itemsLayout);
  }
  
  /**
   * Stops scrolling
   */
  public void stopScrolling() {
      scroller.stopScrolling();
  }
}




Java Source Code List

kankan.wheel.demo.CitiesActivity.java
kankan.wheel.demo.DateActivity.java
kankan.wheel.demo.PasswActivity.java
kankan.wheel.demo.SlotMachineActivity.java
kankan.wheel.demo.SpeedActivity.java
kankan.wheel.demo.Time2Activity.java
kankan.wheel.demo.TimeActivity.java
kankan.wheel.demo.WheelDemo.java
kankan.wheel.widget.ItemsRange.java
kankan.wheel.widget.OnWheelChangedListener.java
kankan.wheel.widget.OnWheelClickedListener.java
kankan.wheel.widget.OnWheelScrollListener.java
kankan.wheel.widget.WheelAdapter.java
kankan.wheel.widget.WheelRecycle.java
kankan.wheel.widget.WheelScroller.java
kankan.wheel.widget.WheelView.java
kankan.wheel.widget.adapters.AbstractWheelAdapter.java
kankan.wheel.widget.adapters.AbstractWheelTextAdapter.java
kankan.wheel.widget.adapters.AdapterWheel.java
kankan.wheel.widget.adapters.ArrayWheelAdapter.java
kankan.wheel.widget.adapters.NumericWheelAdapter.java
kankan.wheel.widget.adapters.WheelViewAdapter.java