最近碰到个项目要使用到滚动选择器,原生的NumberPicker可定制性太差,不大符合UI要求。
网上开源的WheelView是用ScrollView写的,不能循环滚动,而且当数据量很大时要加载的Item太多,性能非常低。
然后,还是自己写一个比较靠谱,用的是ListView实现的。写完自己体验了一下,性能不错,再大的数据也不怕了。
感觉不错,重新封装了一下,提供了一些接口可以直接按照自己的需求定制,调用方法在MainActivity中。
不多说了,直接上代码:
CycleWheelView.java:
/** * Copyright (C) 2015 * * CycleWheelView.java * * Description: * * Author: Liao Longhui * * Ver 1.0, 2015-07-15, Liao Longhui, Create file */package com.example.wheelviewdemo;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.ColorFilter;import android.graphics.Paint;import android.graphics.drawable.Drawable;import android.os.Handler;import android.util.AttributeSet;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.AbsListView;import android.widget.BaseAdapter;import android.widget.ListView;import android.widget.TextView;import java.util.ArrayList;import java.util.List;/** * 可循环滚动的选择器 * @author Liao Longhui * */public class CycleWheelView extends ListView { public static final String TAG = CycleWheelView.class.getSimpleName(); private static final int COLOR_DIVIDER_DEFALUT = Color.parseColor("#747474"); private static final int HEIGHT_DIVIDER_DEFAULT = 2; private static final int COLOR_SOLID_DEFAULT = Color.parseColor("#3e4043"); private static final int COLOR_SOLID_SELET_DEFAULT = Color.parseColor("#323335"); private static final int WHEEL_SIZE_DEFAULT = 3; private Handler mHandler; private CycleWheelViewAdapter mAdapter; /** * Labels */ private List<String> mLabels; /** * Color Of Selected Label */ private int mLabelSelectColor = Color.WHITE; /** * Color Of Unselected Label */ private int mLabelColor = Color.GRAY; /** * Gradual Alph */ private float mAlphaGradual = 0.7f; /** * Color Of Divider */ private int dividerColor = COLOR_DIVIDER_DEFALUT; /** * Height Of Divider */ private int dividerHeight = HEIGHT_DIVIDER_DEFAULT; /** * Color of Selected Solid */ private int seletedSolidColor = COLOR_SOLID_SELET_DEFAULT; /** * Color of Unselected Solid */ private int solidColor = COLOR_SOLID_DEFAULT; /** * Size Of Wheel , it should be odd number like 3 or greater */ private int mWheelSize = WHEEL_SIZE_DEFAULT; /** * res Id of Wheel Item Layout */ private int mItemLayoutId; /** * res Id of Label TextView */ private int mItemLabelTvId; /** * Height of Wheel Item */ private int mItemHeight; private boolean cylceEnable; private int mCurrentPositon; private WheelItemSelectedListener mItemSelectedListener; public CycleWheelView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public CycleWheelView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public CycleWheelView(Context context) { super(context); } private void init() { mHandler = new Handler(); mItemLayoutId = R.layout.item_cyclewheel; mItemLabelTvId = R.id.tv_label_item_wheel; mAdapter = new CycleWheelViewAdapter(); setVerticalScrollBarEnabled(false); setScrollingCacheEnabled(false); setCacheColorHint(Color.TRANSPARENT); setFadingEdgeLength(0); setOverScrollMode(OVER_SCROLL_NEVER); setDividerHeight(0); setAdapter(mAdapter); setOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { if (scrollState == SCROLL_STATE_IDLE) { View itemView = getChildAt(0); if (itemView != null) { float deltaY = itemView.getY(); if (deltaY == 0) { return; } if (Math.abs(deltaY) < mItemHeight / 2) { smoothScrollBy(getDistance(deltaY), 50); } else { smoothScrollBy(getDistance(mItemHeight + deltaY), 50); } } } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { refreshItems(); } }); } private int getDistance(float scrollDistance) { if (Math.abs(scrollDistance) <= 2) { return (int) scrollDistance; } else if (Math.abs(scrollDistance) < 12) { return scrollDistance > 0 ? 2 : -2; } else { return (int) (scrollDistance / 6); } } private void refreshItems() { int offset = mWheelSize / 2; int firstPosition = getFirstVisiblePosition(); int position = 0; if (getChildAt(0) == null) { return; } if (Math.abs(getChildAt(0).getY()) <= mItemHeight / 2) { position = firstPosition + offset; } else { position = firstPosition + offset + 1; } if (position == mCurrentPositon) { return; } mCurrentPositon = position; if (mItemSelectedListener != null) { mItemSelectedListener.onItemSelected(getSelection(), getSelectLabel()); } resetItems(firstPosition, position, offset); } private void resetItems(int firstPosition, int position, int offset){ for (int i = position - offset - 1; i < position + offset + 1; i++) { View itemView = getChildAt(i - firstPosition); if (itemView == null) { continue; } TextView labelTv = (TextView) itemView.findViewById(mItemLabelTvId); if (position == i) { labelTv.setTextColor(mLabelSelectColor); itemView.setAlpha(1f); } else { labelTv.setTextColor(mLabelColor); int delta = Math.abs(i - position); double alpha = Math.pow(mAlphaGradual, delta); itemView.setAlpha((float) alpha); } } } /** * 设置滚轮的刻度列表 * * @param labels */ public void setLabels(List<String> labels) { mLabels = labels; mAdapter.setData(mLabels); mAdapter.notifyDataSetChanged(); initView(); } /** * 设置滚轮滚动监听 * * @param mItemSelectedListener */ public void setOnWheelItemSelectedListener(WheelItemSelectedListener mItemSelectedListener) { this.mItemSelectedListener = mItemSelectedListener; } /** * 获取滚轮的刻度列表 * * @return */ public List<String> getLabels() { return mLabels; } /** * 设置滚轮是否为循环滚动 * * @param enable true-循环 false-单程 */ public void setCycleEnable(boolean enable) { if (cylceEnable != enable) { cylceEnable = enable; mAdapter.notifyDataSetChanged(); setSelection(getSelection()); } } /* * 滚动到指定位置 */ @Override public void setSelection(final int position) { mHandler.post(new Runnable() { @Override public void run() { CycleWheelView.super.setSelection(getPosition(position)); } }); } private int getPosition(int positon) { if (mLabels == null || mLabels.size() == 0) { return 0; } if (cylceEnable) { int d = Integer.MAX_VALUE / 2 / mLabels.size(); return positon + d * mLabels.size(); } return positon; } /** * 获取当前滚轮位置 * * @return */ public int getSelection() { if (mCurrentPositon == 0) { mCurrentPositon = mWheelSize / 2; } return (mCurrentPositon - mWheelSize / 2) % mLabels.size(); } /** * 获取当前滚轮位置的刻度 * * @return */ public String getSelectLabel() { int position = getSelection(); position = position < 0 ? 0 : position; try { return mLabels.get(position); } catch (Exception e) { return ""; } } /** * 如果需要自定义滚轮每个Item,调用此方法设置自定义Item布局,自定义布局中需要一个TextView来显示滚轮刻度 * * @param itemResId 布局文件Id * @param labelTvId 刻度TextView的资源Id */ public void setWheelItemLayout(int itemResId, int labelTvId) { mItemLayoutId = itemResId; mItemLabelTvId = labelTvId; mAdapter = new CycleWheelViewAdapter(); mAdapter.setData(mLabels); setAdapter(mAdapter); initView(); } /** * 设置未选中刻度文字颜色 * * @param labelColor */ public void setLabelColor(int labelColor) { this.mLabelColor = labelColor; resetItems(getFirstVisiblePosition(), mCurrentPositon, mWheelSize/2); } /** * 设置选中刻度文字颜色 * * @param labelSelectColor */ public void setLabelSelectColor(int labelSelectColor) { this.mLabelSelectColor = labelSelectColor; resetItems(getFirstVisiblePosition(), mCurrentPositon, mWheelSize/2); } /** * 设置滚轮刻度透明渐变值 * * @param alphaGradual */ public void setAlphaGradual(float alphaGradual) { this.mAlphaGradual = alphaGradual; resetItems(getFirstVisiblePosition(), mCurrentPositon, mWheelSize/2); } /** * 设置滚轮可显示的刻度数量,必须为奇数,且大于等于3 * * @param wheelSize * @throws CycleWheelViewException 滚轮数量错误 */ public void setWheelSize(int wheelSize) throws CycleWheelViewException { if (wheelSize < 3 || wheelSize % 2 != 1) { throw new CycleWheelViewException("Wheel Size Error , Must Be 3,5,7,9..."); } else { mWheelSize = wheelSize; initView(); } } /** * 设置块的颜色 * @param unselectedSolidColor 未选中的块的颜色 * @param selectedSolidColor 选中的块的颜色 */ public void setSolid(int unselectedSolidColor, int selectedSolidColor){ this.solidColor = unselectedSolidColor; this.seletedSolidColor = selectedSolidColor; initView(); } /** * 设置分割线样式 * @param dividerColor 分割线颜色 * @param dividerHeight 分割线高度(px) */ public void setDivider(int dividerColor, int dividerHeight){ this.dividerColor = dividerColor; this.dividerHeight = dividerHeight; } @SuppressWarnings("deprecation") private void initView() { mItemHeight = measureHeight(); ViewGroup.LayoutParams lp = getLayoutParams(); lp.height = mItemHeight * mWheelSize; mAdapter.setData(mLabels); mAdapter.notifyDataSetChanged(); Drawable backgroud = new Drawable() { @Override public void draw(Canvas canvas) { int viewWidth = getWidth(); Paint dividerPaint = new Paint(); dividerPaint.setColor(dividerColor); dividerPaint.setStrokeWidth(dividerHeight); Paint seletedSolidPaint = new Paint(); seletedSolidPaint.setColor(seletedSolidColor); Paint solidPaint = new Paint(); solidPaint.setColor(solidColor); canvas.drawRect(0, 0, viewWidth, mItemHeight * (mWheelSize / 2), solidPaint); canvas.drawRect(0, mItemHeight * (mWheelSize / 2 + 1), viewWidth, mItemHeight * (mWheelSize), solidPaint); canvas.drawRect(0, mItemHeight * (mWheelSize / 2), viewWidth, mItemHeight * (mWheelSize / 2 + 1), seletedSolidPaint); canvas.drawLine(0, mItemHeight * (mWheelSize / 2), viewWidth, mItemHeight * (mWheelSize / 2), dividerPaint); canvas.drawLine(0, mItemHeight * (mWheelSize / 2 + 1), viewWidth, mItemHeight * (mWheelSize / 2 + 1), dividerPaint); } @Override public void setAlpha(int alpha) { } @Override public void setColorFilter(ColorFilter cf) { } @Override public int getOpacity() { return 0; } }; setBackgroundDrawable(backgroud); } private int measureHeight() { View itemView = LayoutInflater.from(getContext()).inflate(mItemLayoutId, null); itemView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); itemView.measure(w, h); int height = itemView.getMeasuredHeight(); // int width = view.getMeasuredWidth(); return height; } public interface WheelItemSelectedListener { public void onItemSelected(int position, String label); } public class CycleWheelViewException extends Exception { private static final long serialVersionUID = 1L; public CycleWheelViewException(String detailMessage) { super(detailMessage); } } public class CycleWheelViewAdapter extends BaseAdapter { private List<String> mData = new ArrayList<String>(); public void setData(List<String> mWheelLabels) { mData.clear(); mData.addAll(mWheelLabels); } @Override public int getCount() { if (cylceEnable) { return Integer.MAX_VALUE; } return mData.size() + mWheelSize - 1; } @Override public Object getItem(int position) { return ""; } @Override public long getItemId(int position) { return position; } @Override public boolean isEnabled(int position) { return false; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(getContext()).inflate(mItemLayoutId, null); } TextView textView = (TextView) convertView.findViewById(mItemLabelTvId); if (position < mWheelSize / 2 || (!cylceEnable && position >= mData.size() + mWheelSize / 2)) { textView.setText(""); convertView.setVisibility(View.INVISIBLE); } else { textView.setText(mData.get((position - mWheelSize / 2) % mData.size())); convertView.setVisibility(View.VISIBLE); } return convertView; } }}
MainActivity.java:
public class MainActivity extends Activity { private CycleWheelView cycleWheelView0,cycleWheelView1, cycleWheelView2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); cycleWheelView0 = (CycleWheelView) findViewById(R.id.cycleWheelView); List<String> labels = new ArrayList<>(); for (int i = 0; i < 12; i++) { labels.add("" + i); } cycleWheelView0.setLabels(labels); cycleWheelView0.setAlphaGradual(0.5f); cycleWheelView0.setOnWheelItemSelectedListener(new WheelItemSelectedListener() { @Override public void onItemSelected(int position, String label) { Log.d("test", label); } }); cycleWheelView1 = (CycleWheelView) findViewById(R.id.cycleWheelView1); List<String> labels1 = new ArrayList<>(); for (int i = 0; i < 24; i++) { labels1.add("" + i); } cycleWheelView1.setLabels(labels1); try { cycleWheelView1.setWheelSize(5); } catch (CycleWheelViewException e) { e.printStackTrace(); } cycleWheelView1.setSelection(2); cycleWheelView1.setWheelItemLayout(R.layout.item_cyclewheel_custom, R.id.tv_label_item_wheel_custom); cycleWheelView1.setOnWheelItemSelectedListener(new WheelItemSelectedListener() { @Override public void onItemSelected(int position, String label) { Log.d("test", label); } }); cycleWheelView2 = (CycleWheelView) findViewById(R.id.cycleWheelView2); List<String> labels2 = new ArrayList<>(); for (int i = 0; i < 60; i++) { labels2.add("" + i); } cycleWheelView2.setLabels(labels2); try { cycleWheelView2.setWheelSize(7); } catch (CycleWheelViewException e) { e.printStackTrace(); } cycleWheelView2.setCycleEnable(true); cycleWheelView2.setSelection(30); cycleWheelView2.setAlphaGradual(0.6f); cycleWheelView2.setDivider(Color.parseColor("#abcdef"), 2); cycleWheelView2.setSolid(Color.WHITE,Color.WHITE); cycleWheelView2.setLabelColor(Color.BLUE); cycleWheelView2.setLabelSelectColor(Color.RED); cycleWheelView2.setOnWheelItemSelectedListener(new WheelItemSelectedListener() { @Override public void onItemSelected(int position, String label) { Log.d("test", label); } }); }}
Item_cyclewheel.xml:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:background="@android:color/transparent" > <TextView android:id="@+id/tv_label_item_wheel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="20sp" android:singleLine="true" android:layout_centerHorizontal="true" android:layout_centerVertical="true" /></RelativeLayout>
Item_cyclewheel_custom.xml:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:background="@android:color/transparent" > <TextView android:id="@+id/tv_label_item_wheel_custom" android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" android:layout_alignParentLeft="true" android:layout_centerVertical="true" /> <ImageView android:layout_width="25dp" android:layout_height="25dp" android:layout_centerVertical="true" android:layout_alignParentRight="true" android:src="@drawable/ic_launcher" /></RelativeLayout>
activity_main.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <com.example.wheelviewdemo.CycleWheelView android:id="@+id/cycleWheelView" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" > </com.example.wheelviewdemo.CycleWheelView> <com.example.wheelviewdemo.CycleWheelView android:id="@+id/cycleWheelView1" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" > </com.example.wheelviewdemo.CycleWheelView> <com.example.wheelviewdemo.CycleWheelView android:id="@+id/cycleWheelView2" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" > </com.example.wheelviewdemo.CycleWheelView> </LinearLayout>