自定义滚轮选择器Wheelview

来源:互联网 发布:当店家软件 编辑:程序博客网 时间:2024/05/04 20:26

源码下载:https://github.com/victorfan336/WheelView 喜欢的话就star下吧。


前言

当初写这个控件基于三个原因:

  • 想找个控件来练练手,再次熟悉熟悉自定义view
  • 最近开始流行学习kotlin语言了,我也已经学习了一段时间,想练练手
  • 最近发现公司的滚轮控件出现了一个bug

先上效果图:



一、使用


1.1 特点


* 完全使用自定义的view编写完成,我看过有些人不是完全基于view写的
* 实现了三种滚轮模式:  
   * 循环模式:  
   * 居中显示模式;    
   * 从头开始显示
* 自己处理了滚动事件和快速滑动事件
* 处理了边界检测和弹性效果
* 通过adapter快速添加数据

导入:compile 'com.victor.library:wheelview:1.0.7@aar'  
 version1.0.8更新介绍:
    1.新增itemHeight属性配置;      
    2.解决UI拖出可见范围后,有时回弹不准的问题,是由于没有做四舍五入的问题导致的;          
    3.拓展滚动监听方法,回传wheelview;        
    4.新增设置当前选择位置和获取当前选择位置方法:        
    public void setCurrItem(int index);    
     
    public int getSelectedItem();   
   
    java版本已在公司APP中使用! 


1.2 定义了三个可配置属性

     
    <attr name="textColor" format="color"/>
    <attr name="textSize" format="dimension" />
    <attr name="dragOut" format="boolean" />
    <attr name="itemheight" format="dimension" />
               

1.3 在xml中配置

   
<com.victor.library.wheelview.WheelView
       android:id="@+id/wheelview"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_weight="1.0"
       android:focusable="true"
       android:gravity="center"
       app:dragOut="true"
       app:textColor="@color/black"
       app:textSize="12sp"
        />
            

1.4 自定义Adapter

只需要实现IWheelviewAdapter即可     
 
public interface IWheelviewAdapter<T> {
    String getItemeTitle(int i);       
    int getCount();              
    T get(int index);            
    void clear();             
}
  
只要实现以上接口方法即可,下面示范一个自定义的Adapter:

public class WheelviewAdapter implements IWheelviewAdapter {

   private List<String> mList;

   public WheelviewAdapter(List<String> list) {
       mList = list;
   }

   @Override
   public String getItemeTitle(int i) {
       if (mList != null) {
           return mList.get(i);
       } else {
           return "";
       }
   }

   @Override
   public int getCount() {
       if (mList != null) {
           return mList.size();
       } else {
           return 0;
       }
   }

   @Override
   public String get(int index) {
       if (mList != null && index >= 0 && index < mList.size()) {
           return mList.get(index);
       } else {
           return null;
       }
   }

   @Override
   public void clear() {
       if (mList != null) {
           mList.clear();
       }
   }
}
 

1.5 在代码中配置

       
private String[] provides = {"天津市", "北京市", "黑龙江省", "江苏省", "浙江省", "安徽省",
            "福建省", "江西省", "山东省", "河南省", "湖北省", "湖南省", "广东省"};     
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        ArrayList<String> provider = new ArrayList<>();
        for (String name : provides) {
            provider.add(name);
        }
        wheelView1 = (WheelView) findViewById(R.id.wheelview1);
        IWheelviewAdapter providerAdapter = new WheelviewAdapter(provider);
        wheelView1.setAdapter(providerAdapter);    
        // 设置滚动选择监听
        wheelView1.setWheelScrollListener(new WheelView.WheelScrollListener() {


            @Override
            public void onChanged(WheelView wheelView, int selected, Object bean) {
                Toast.makeText(DemoActivity.this, bean + "被选中了第" + selected, Toast.LENGTH_SHORT).show();
            }


        });      


}       
     
设置对齐模式:默认是WheelViewCenterMode居中显示     

wheelView1.setMode(WheelView.getStartModeInstance(wheelView1));           
wheelView1.setMode(WheelView.getCenterModeInstance(wheelView1));     
wheelView1.setMode(WheelView.getRecycleModeInstance(wheelView1));      
 

二、源码分析


2.1自定义流程

    

自定义流程主要包括:

1)接收xml属性配置;

TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.WheelView, defStyleAttr, 0);int count = typedArray.getIndexCount();for (int i = 0; i < count; i++) {    int attr = typedArray.getIndex(i);    if (attr == R.styleable.WheelView_textColor) {        textColor = typedArray.getColor(attr, 0x000000);    } else if (attr == R.styleable.WheelView_textSize) {        textSize = typedArray.getDimension(attr, 19f);    } else if (attr == R.styleable.WheelView_dragOut) {        canDragOutBorder = typedArray.getBoolean(attr, true);    } else if (attr == R.styleable.WheelView_itemHeight) {        eachItemHeight = (int) typedArray.getDimension(attr, 12);    }}typedArray.recycle();

2)实现public voidonMeasure(intwidthMeasureSpec, intheightMeasureSpec);

@Overridepublic void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    int heightMode = MeasureSpec.getMode(heightMeasureSpec);    int widthMOde = MeasureSpec.getMode((widthMeasureSpec));    int heightM = MeasureSpec.getSize(heightMeasureSpec);    int widthM = MeasureSpec.getSize(widthMeasureSpec);    if (heightMode == MeasureSpec.EXACTLY && widthMOde == MeasureSpec.EXACTLY) {        setMeasuredDimension(widthM, heightM);    } else if (heightMode == MeasureSpec.EXACTLY) {        setMeasuredDimension(600, heightM);    } else if (widthMOde == MeasureSpec.EXACTLY) {        setMeasuredDimension(widthM, 600);    } else {        setMeasuredDimension(400, 600);    }}

3)实现public voidonDraw(Canvas canvas);

@Overridepublic void onDraw(Canvas canvas) {    super.onDraw(canvas);    canvas.drawColor(0xffffff);    clipView(canvas);// 指定绘制区域    drawText(canvas);// 绘制文本内容    drawLine(canvas);// 绘制两根分割线    drawShadows(canvas);// 绘制阴影遮罩}

关于怎么自定义控件,请参考:http://blog.csdn.net/column/details/androidcustomview.html


2.2 onTouch事件


自定义onTouch主要实现:

1)边界检查

2)滚动效果处理

3)滚动定位

4)选中位置回调


2.3 onFling事件


   通过手势实现onFling()方法来处理快速滑动效果,不然快速时界面会出现卡顿。代码中是通过scroller来处理滚动的。

   

@Overridepublic boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {    int dy = (int) (-vTracker.getYVelocity() / 8);    // 边距检测    if (getScrollY() + dy <= wheelViewMode.getTopMaxScrollHeight()) {        dy = wheelViewMode.getTopMaxScrollHeight() - getScrollY();    }    if (getScrollY() + dy >= wheelViewMode.getBottomMaxScrollHeight()) {        dy = wheelViewMode.getBottomMaxScrollHeight() - getScrollY();    }    // 取整每次onfling的距离    int scrollDy = getScrollY() % eachItemHeight;    if (scrollDy + dy % eachItemHeight > eachItemHeight / 2) {        dy += eachItemHeight - (scrollDy + dy % eachItemHeight);    } else if (scrollDy + dy % eachItemHeight > 0 && scrollDy + dy % eachItemHeight <= eachItemHeight / 2) {        dy -= scrollDy + dy % eachItemHeight;    } else if (scrollDy + dy % eachItemHeight < 0 && scrollDy + dy % eachItemHeight >= -eachItemHeight / 2) {        dy -= scrollDy + dy % eachItemHeight;    } else if (scrollDy + dy % eachItemHeight < -eachItemHeight / 2) {        dy -= scrollDy + dy % eachItemHeight + eachItemHeight;    }    Log.e(TAG, "GestureDetector: dy = " + dy + " -- getScrollY() = " + getScrollY());    scroller.startScroll(0, getScrollY(), 0, dy, 400);    invalidate();    return true;}

         首先,我们拿到vTracker当前y方向上的速度,换算成我们需要滚动的距离,我这里处理比较简单,每次去的距离是-vTracker.getYVelocity() / 8;负号的话只是方向问题;

    其次,检测快速滚动dy距离后,是否已经滚出了屏幕;

    最后,取整滚动的距离,因为onFling方法是在松手后才会执行的,所以为了确保滚动后能够让文本信息还是居中显示,松后前滚动的距离和快速滑动的距离dy之和必须是eachItemHeight的倍数。

    最终再通过scroller滚动界面,来达到快速滚动的效果,scroller可以设置弹性动画,这就是使用scroller很大的一个好处。


2.4 三种显示模式


显示模式目前处理了三种,当然你也可以还有其他的显示方式。

显示模式主要实现以下接口即可:

public abstract class IWheelViewMode {    int eachItemHeight;    int childrenSize;    public int getEachItemHeight() {        return eachItemHeight;    }    public void setEachItemHeight(int eachItemHeight) {        this.eachItemHeight = eachItemHeight;    }    public int getChildrenSize() {        return childrenSize;    }    public void setChildrenSize(int childrenSize) {        this.childrenSize = childrenSize;    }    public IWheelViewMode(int eachItemHeight, int childrenSize) {        this.eachItemHeight = eachItemHeight;        this.childrenSize = childrenSize;    }    public abstract int getSelectedIndex(int baseIndex);// 将滚动的位置换算成当前的选中位置    public abstract int getTopMaxScrollHeight(); // 向上最大滚动距离    public abstract int getBottomMaxScrollHeight(); // 想下最大滚动距离    public abstract float getTextDrawY(int height, int index, Paint paint); // 绘制text的Y方向的位置    public float getCenterY(int height, Paint paint) {        return (height - paint.getFontMetrics().bottom - paint.getFontMetrics().top) / 2;    }}

getSelectedIndex(int baseIndex):baseIndex是scrollY滚动后的当前位置,

float offset = 0.5f;if (getScrollY() < 0) {    offset = -0.5f;}int moveIndex = (int) (getScrollY() * 1f / eachItemHeight + offset);int selected = wheelViewMode.getSelectedIndex(moveIndex);

代码很容易,scrollY / eachItemHeight就是滚动了多少个item,然后四舍五入,因为这是在松手后调用的,后面可能还有onFling方法会执行。


1)WheelViewStartMode:当前默认位置是0,返回的参数和baseIndex参数是一样,也就是说从第一个文本内容开始显示;向下滚动2,则返回2;

public class WheelViewStartMode extends IWheelViewMode {    public WheelViewStartMode(int eachItemHeight, int childrenSize) {        super(eachItemHeight, childrenSize);    }    @Override    public int getSelectedIndex(int baseIndex) {        return baseIndex;    }    @Override    public int getTopMaxScrollHeight() {        return 0;    }    @Override    public int getBottomMaxScrollHeight() {        return eachItemHeight * (childrenSize - 1);    }    @Override    public float getTextDrawY(int height, int index, Paint paint) {        return (getCenterY(height, paint) + index * eachItemHeight);    }}

2)WheelViewCenterMode:默认显示位置为(childrenSize - 1) / 2 ;所以滚动后的位置就是baseIndex + (childrenSize-1) /2就很好理解了。

public class WheelViewCenterMode extends IWheelViewMode {    public WheelViewCenterMode(int eachItemHeight, int childrenSize) {        super(eachItemHeight, childrenSize);    }    @Override    public int getSelectedIndex(int baseIndex) {        return baseIndex + (childrenSize - 1) / 2;    }    @Override    public int getTopMaxScrollHeight() {        return (childrenSize - 1) / 2 * (-eachItemHeight);    }    @Override    public int getBottomMaxScrollHeight() {        return childrenSize / 2 * eachItemHeight;    }    @Override    public float getTextDrawY(int height, int index, Paint paint) {        return (getCenterY(height, paint) + (index - (childrenSize - 1) / 2) * eachItemHeight);    }}

3)WheelViewRecycleMode:默认显示位置为0,因为除数不能为0,所以避免出错,多加了个判断。

public class WheelViewRecycleMode extends IWheelViewMode {    public WheelViewRecycleMode(int eachItemHeight, int childrenSize) {        super(eachItemHeight, childrenSize);    }    @Override    public int getSelectedIndex(int baseIndex) {        int index = baseIndex;        while (index <= 0) {            index += childrenSize;        }        if (childrenSize == 0) {            Log.e("Wheelview", "WheelViewRecycleMode childrenSize == 0");        }        return index % (childrenSize == 0?1:childrenSize);    }    @Override    public int  getTopMaxScrollHeight() {        return Integer.MIN_VALUE;    }    @Override    public int getBottomMaxScrollHeight() {        return Integer.MAX_VALUE;    }    @Override    public float getTextDrawY(int height, int index, Paint paint) {        return (getCenterY(height, paint) + index * eachItemHeight);    }}


2.5 边界检查




原创粉丝点击