Welcome

首页 / 移动开发 / Android / Android滚轮选择时间控件使用详解

滚轮选择控件
Android自带的选择时间控件有点丑,往往产品和设计都比较嫌弃,希望做成ios一样的滚轮选择,下面是我在NumberPicker的基础上自定义的选择控件,效果如下:


原理

  • 基于NumberPicker实现
  • 动态填充数值
  • 联动
  • 接口监听回调
实现滚轮效果有github上mark比较多的WheelView,但是阅读源码发现数据是一次性填入的,选择时间的话,填入10年就是10*365=3650条数据,也就是new出三千多个TextView,想想都觉得恐怖,肯定是不行的,于是便想到用NumberPicker,动态填充数据,一次只设置5个数据,当选中变化时,重新设置数据填充,所以关键在于填充的数据的计算。
设置数据部分逻辑代码:
/** * 更新左侧控件 * 日期:选择年控件 * 时间:选择月份和日期控件 * * @param timeMillis */private void updateLeftValue(long timeMillis) {SimpleDateFormat sdf;String str[] = new String[DATA_SIZE];if (mCurrentType == TYPE_PICK_DATE) {sdf = new SimpleDateFormat("yyyy");for (int i = 0; i < DATA_SIZE; i++) {Calendar cal = Calendar.getInstance();cal.setTimeInMillis(timeMillis);cal.add(Calendar.YEAR, (i - DATA_SIZE / 2));str[i] = sdf.format(cal.getTimeInMillis());}} else {sdf = new SimpleDateFormat("MM-dd EEE");for (int i = 0; i < DATA_SIZE; i++) {Calendar cal = Calendar.getInstance();cal.setTimeInMillis(timeMillis);cal.add(Calendar.DAY_OF_MONTH, (i - DATA_SIZE / 2));str[i] = sdf.format(cal.getTimeInMillis());}}mNpLeft.setDisplayedValues(str);mNpLeft.setValue(DATA_SIZE / 2);mNpLeft.postInvalidate();}
对滚轮的监听,并重新设置填充数据:

@Overridepublic void onValueChange(NumberPicker picker, int oldVal, int newVal) {Calendar calendar = Calendar.getInstance();calendar.setTimeInMillis(mTimeMillis);int year = calendar.get(Calendar.YEAR);int month = calendar.get(Calendar.MONTH);int day = calendar.get(Calendar.DAY_OF_MONTH);int hour = calendar.get(Calendar.HOUR_OF_DAY);int offset = newVal - oldVal;if (picker == mNpLeft) {if (mCurrentType == TYPE_PICK_DATE) {calendar.add(Calendar.YEAR, offset);} else {calendar.add(Calendar.DAY_OF_MONTH, offset);}updateLeftValue(calendar.getTimeInMillis());mTimeMillis = calendar.getTimeInMillis();} else if (picker == mNpMiddle) {if (mCurrentType == TYPE_PICK_DATE) {calendar.add(Calendar.MONTH, offset);if (calendar.get(Calendar.YEAR) != year) {calendar.set(Calendar.YEAR, year);}} else {calendar.add(Calendar.HOUR_OF_DAY, offset);if (calendar.get(Calendar.DAY_OF_MONTH) != day) {calendar.set(Calendar.DAY_OF_MONTH, day);}if (calendar.get(Calendar.MONTH) != month) {calendar.set(Calendar.MONTH, month);}if (calendar.get(Calendar.YEAR) != year) {calendar.set(Calendar.YEAR, year);}}updateMiddleValue(calendar.getTimeInMillis());updateRightValue(calendar.getTimeInMillis());mTimeMillis = calendar.getTimeInMillis();} else if (picker == mNpRight) {if (mCurrentType == TYPE_PICK_DATE) {int days = getMaxDayOfMonth(year, month + 1);if(day == 1 && offset < 0){calendar.set(Calendar.DAY_OF_MONTH,days);}else if(day == days && offset > 0){calendar.set(Calendar.DAY_OF_MONTH,1);}else{calendar.add(Calendar.DAY_OF_MONTH, offset);}if (calendar.get(Calendar.MONTH) != month) {calendar.set(Calendar.MONTH, month);}if (calendar.get(Calendar.YEAR) != year) {calendar.set(Calendar.YEAR, year);}Log.e(TAG,"time:::"+test.format(calendar.getTimeInMillis()));} else {calendar.add(Calendar.MINUTE, offset);if (calendar.get(Calendar.HOUR_OF_DAY) != hour) {calendar.set(Calendar.HOUR_OF_DAY, hour);}if (calendar.get(Calendar.DAY_OF_MONTH) != day) {calendar.set(Calendar.DAY_OF_MONTH, day);}if (calendar.get(Calendar.MONTH) != month) {calendar.set(Calendar.MONTH, month);}if (calendar.get(Calendar.YEAR) != year) {calendar.set(Calendar.YEAR, year);}}updateRightValue(calendar.getTimeInMillis());mTimeMillis = calendar.getTimeInMillis();}/** * 向外部发送当前选中时间 */if (mOnSelectedChangeListener != null) {mOnSelectedChangeListener.onSelected(this,mTimeMillis);}Log.e(TAG, "selected time:" + test.format(mTimeMillis));}
选择数值和字符串
同样的,使用NumberPicker进行封装,动态填充数值从而实现滚动变换的效果。
  • 考虑到通用性,传入的是Object类型的数组,在控件里进行判断。
  • 可以选择一列数值、两列数值、三列数值,字符串同理。每一列数值可以设置它的单位、标题等,默认是隐藏,需要自己设置。
  • 可以设置步长step
完整代码如下:

package com.example.moore.picktimeview.widget;import android.content.Context;import android.graphics.Color;import android.util.AttributeSet;import android.util.Log;import android.view.Gravity;import android.view.ViewGroup;import android.widget.LinearLayout;import android.widget.NumberPicker;import android.widget.TextView;/** * Created by Moore on 2016/10/21. */public class PickValueView extends LinearLayout implements NumberPicker.OnValueChangeListener {private Context mContext;/** * 组件 标题、单位、滚轮 */private TextView mTitleLeft, mTitleMiddle, mTitleRight;private TextView mUnitLeft, mUnitMiddle, mUnitRight;private MyNumberPicker mNpLeft, mNpMiddle, mNpRight;/** * 数据个数 1列 or 2列 or 3列 */private int mViewCount = 1;/** * 一组数据长度 */private final int DATA_SIZE = 3;/** * 需要设置的值与默认值 */private Object[] mLeftValues;private Object[] mMiddleValues;private Object[] mRightValues;private Object mDefaultLeftValue;private Object mDefaultMiddleValue;private Object mDefaultRightValue;/** * 当前正在显示的值 */private Object[] mShowingLeft = new Object[DATA_SIZE];private Object[] mShowingMiddle = new Object[DATA_SIZE];private Object[] mShowingRight = new Object[DATA_SIZE];/** * 步长 */private int mLeftStep = 5;private int mMiddleStep = 1;private int mRightStep = 1;/** * 回调接口对象 */private onSelectedChangeListener mSelectedChangeListener;public PickValueView(Context context) {super(context);this.mContext = context;generateView();}public PickValueView(Context context, AttributeSet attrs) {super(context, attrs);this.mContext = context;generateView();}public PickValueView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);this.mContext = context;generateView();}/** * 生成视图 */private void generateView() {//标题LinearLayout titleLayout = new LinearLayout(mContext);LayoutParams titleParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);titleParams.setMargins(0, 0, 0, dip2px(12));titleLayout.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));titleLayout.setOrientation(HORIZONTAL);mTitleLeft = new TextView(mContext);mTitleMiddle = new TextView(mContext);mTitleRight = new TextView(mContext);LayoutParams params = new LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1);TextView[] titles = new TextView[]{mTitleLeft, mTitleMiddle, mTitleRight};for (int i = 0; i < titles.length; i++) {titles[i].setLayoutParams(params);titles[i].setGravity(Gravity.CENTER);titles[i].setTextColor(Color.parseColor("#3434EE"));}titleLayout.addView(mTitleLeft);titleLayout.addView(mTitleMiddle);titleLayout.addView(mTitleRight);//内容LinearLayout contentLayout = new LinearLayout(mContext);contentLayout.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));contentLayout.setOrientation(HORIZONTAL);contentLayout.setGravity(Gravity.CENTER);mNpLeft = new MyNumberPicker(mContext);mNpMiddle = new MyNumberPicker(mContext);mNpRight = new MyNumberPicker(mContext);mUnitLeft = new TextView(mContext);mUnitMiddle = new TextView(mContext);mUnitRight = new TextView(mContext);MyNumberPicker[] nps = new MyNumberPicker[]{mNpLeft, mNpMiddle, mNpRight};for (int i = 0; i < nps.length; i++) {nps[i].setLayoutParams(params);nps[i].setDescendantFocusability(FOCUS_BLOCK_DESCENDANTS);nps[i].setOnValueChangedListener(this);}contentLayout.addView(mNpLeft);contentLayout.addView(mUnitLeft);contentLayout.addView(mNpMiddle);contentLayout.addView(mUnitMiddle);contentLayout.addView(mNpRight);contentLayout.addView(mUnitRight);this.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));this.setOrientation(VERTICAL);this.addView(titleLayout);this.addView(contentLayout);}/** * 初始化数据和值 */private void initViewAndPicker() {if (mViewCount == 1) {this.mNpMiddle.setVisibility(GONE);this.mNpRight.setVisibility(GONE);this.mUnitMiddle.setVisibility(GONE);this.mUnitRight.setVisibility(GONE);} else if (mViewCount == 2) {this.mNpRight.setVisibility(GONE);this.mUnitRight.setVisibility(GONE);}//初始化数组值if (mLeftValues != null && mLeftValues.length != 0) {if (mLeftValues.length < DATA_SIZE) {for (int i = 0; i < mLeftValues.length; i++) {mShowingLeft[i] = mLeftValues[i];}for (int i = mLeftValues.length; i < DATA_SIZE; i++) {mShowingLeft[i] = -9999;}} else {for (int i = 0; i < DATA_SIZE; i++) {mShowingLeft[i] = mLeftValues[i];}}mNpLeft.setMinValue(0);mNpLeft.setMaxValue(DATA_SIZE - 1);if (mDefaultLeftValue != null)updateLeftView(mDefaultLeftValue);elseupdateLeftView(mShowingLeft[0]);}/** * 中间控件 */if (mViewCount == 2 || mViewCount == 3) {if (mMiddleValues != null && mMiddleValues.length != 0) {if (mMiddleValues.length < DATA_SIZE) {for (int i = 0; i < mMiddleValues.length; i++) {mShowingMiddle[i] = mMiddleValues[i];}for (int i = mMiddleValues.length; i < DATA_SIZE; i++) {mShowingMiddle[i] = -9999;}} else {for (int i = 0; i < DATA_SIZE; i++) {mShowingMiddle[i] = mMiddleValues[i];}}mNpMiddle.setMinValue(0);mNpMiddle.setMaxValue(DATA_SIZE - 1);if (mDefaultMiddleValue != null)updateMiddleView(mDefaultMiddleValue);elseupdateMiddleView(mShowingMiddle[0]);}}/** * 右侧控件 */if (mViewCount == 3) {if (mRightValues != null && mRightValues.length != 0) {if (mRightValues.length < DATA_SIZE) {for (int i = 0; i < mRightValues.length; i++) {mShowingRight[i] = mRightValues[i];}for (int i = mRightValues.length; i < DATA_SIZE; i++) {mShowingRight[i] = -9999;}} else {for (int i = 0; i < DATA_SIZE; i++) {mShowingRight[i] = mRightValues[i];}}mNpRight.setMinValue(0);mNpRight.setMaxValue(DATA_SIZE - 1);if (mDefaultRightValue != null)updateRightView(mDefaultRightValue);elseupdateRightView(mShowingRight[0]);}}}private void updateLeftView(Object value) {updateValue(value, 0);}private void updateMiddleView(Object value) {updateValue(value, 1);}private void updateRightView(Object value) {updateValue(value, 2);}/** * 更新滚轮视图 * * @param value * @param index */private void updateValue(Object value, int index) {String showStr[] = new String[DATA_SIZE];MyNumberPicker picker;Object[] showingValue;Object[] values;int step;if (index == 0) {picker = mNpLeft;showingValue = mShowingLeft;values = mLeftValues;step = mLeftStep;} else if (index == 1) {picker = mNpMiddle;showingValue = mShowingMiddle;values = mMiddleValues;step = mMiddleStep;} else {picker = mNpRight;showingValue = mShowingRight;values = mRightValues;step = mRightStep;}if (values instanceof Integer[]) {for (int i = 0; i < DATA_SIZE; i++) {showingValue[i] = (int) value - step * (DATA_SIZE / 2 - i);int offset = (int) values[values.length - 1] - (int) values[0] + step;if ((int) showingValue[i] < (int) values[0]) {showingValue[i] = (int) showingValue[i] + offset;}if ((int) showingValue[i] > (int) values[values.length - 1]) {showingValue[i] = (int) showingValue[i] - offset;}showStr[i] = "" + showingValue[i];}} else {int strIndex = 0;for (int i = 0; i < values.length; i++) {if (values[i].equals(value)) {strIndex = i;break;}}for (int i = 0; i < DATA_SIZE; i++) {int temp = strIndex - (DATA_SIZE / 2 - i);if (temp < 0) {temp += values.length;}if (temp >= values.length) {temp -= values.length;}showingValue[i] = values[temp];showStr[i] = (String) values[temp];}}picker.setDisplayedValues(showStr);picker.setValue(DATA_SIZE / 2);picker.postInvalidate();}@Overridepublic void onValueChange(NumberPicker picker, int oldVal, int newVal) {if (picker == mNpLeft) {updateLeftView(mShowingLeft[newVal]);} else if (picker == mNpMiddle) {updateMiddleView(mShowingMiddle[newVal]);} else if (picker == mNpRight) {updateRightView(mShowingRight[newVal]);}if (mSelectedChangeListener != null) {mSelectedChangeListener.onSelected(this, mShowingLeft[DATA_SIZE / 2], mShowingMiddle[DATA_SIZE / 2], mShowingRight[DATA_SIZE / 2]);}}/** * 设置数据--单列数据 * * @param leftValues * @param mDefaultLeftValue */public void setValueData(Object[] leftValues, Object mDefaultLeftValue) {this.mViewCount = 1;this.mLeftValues = leftValues;this.mDefaultLeftValue = mDefaultLeftValue;initViewAndPicker();}/** * 设置数据--两列数据 * * @param leftValues * @param mDefaultLeftValue * @param middleValues * @param defaultMiddleValue */public void setValueData(Object[] leftValues, Object mDefaultLeftValue, Object[] middleValues, Object defaultMiddleValue) {this.mViewCount = 2;this.mLeftValues = leftValues;this.mDefaultLeftValue = mDefaultLeftValue;this.mMiddleValues = middleValues;this.mDefaultMiddleValue = defaultMiddleValue;initViewAndPicker();}/** * 设置数据--三列数据 * * @param leftValues * @param mDefaultLeftValue * @param middleValues * @param defaultMiddleValue * @param rightValues * @param defaultRightValue */public void setValueData(Object[] leftValues, Object mDefaultLeftValue, Object[] middleValues, Object defaultMiddleValue, Object[] rightValues, Object defaultRightValue) {this.mViewCount = 3;this.mLeftValues = leftValues;this.mDefaultLeftValue = mDefaultLeftValue;this.mMiddleValues = middleValues;this.mDefaultMiddleValue = defaultMiddleValue;this.mRightValues = rightValues;this.mDefaultRightValue = defaultRightValue;initViewAndPicker();}/** * 设置左边数据步长 * * @param step */public void setLeftStep(int step) {this.mLeftStep = step;initViewAndPicker();}/** * 设置中间数据步长 * * @param step */public void setMiddleStep(int step) {this.mMiddleStep = step;initViewAndPicker();}/** * 设置右边数据步长 * * @param step */public void setRightStep(int step) {this.mRightStep = step;initViewAndPicker();}/** * 设置标题 * * @param left * @param middle * @param right */public void setTitle(String left, String middle, String right) {if (left != null) {mTitleLeft.setVisibility(VISIBLE);mTitleLeft.setText(left);} else {mTitleLeft.setVisibility(GONE);}if (middle != null) {mTitleMiddle.setVisibility(VISIBLE);mTitleMiddle.setText(middle);} else {mTitleMiddle.setVisibility(GONE);}if (right != null) {mTitleRight.setVisibility(VISIBLE);mTitleRight.setText(right);} else {mTitleRight.setVisibility(GONE);}this.postInvalidate();}public void setUnitLeft(String unitLeft) {setUnit(unitLeft, 0);}public void setmUnitMiddle(String unitMiddle) {setUnit(unitMiddle, 1);}public void setUnitRight(String unitRight) {setUnit(unitRight, 2);}private void setUnit(String unit, int index) {TextView tvUnit;if (index == 0) {tvUnit = mUnitLeft;} else if (index == 1) {tvUnit = mUnitMiddle;} else {tvUnit = mUnitRight;}if (unit != null) {tvUnit.setText(unit);} else {tvUnit.setText(" ");}initViewAndPicker();}/** * 设置回调 * * @param listener */public void setOnSelectedChangeListener(onSelectedChangeListener listener) {this.mSelectedChangeListener = listener;}/** * dp转px * * @param dp * @return */private int dip2px(int dp) {float scale = mContext.getResources().getDisplayMetrics().density;return (int) (scale * dp + 0.5f);}/** * 回调接口 */public interface onSelectedChangeListener {void onSelected(PickValueView view, Object leftValue, Object middleValue, Object rightValue);}}
关于NumberPicker
默认的NumberPicker往往字体颜色、分割线颜色等都是跟随系统,不能改变,考虑到可能比较丑或者有其他需求,所以自定义的NumberPicker,通过反射的方式更改里面的一些属性,代码如下:

package com.example.moore.picktimeview.widget;import android.content.Context;import android.graphics.Color;import android.graphics.drawable.ColorDrawable;import android.util.AttributeSet;import android.util.Log;import android.view.View;import android.view.ViewGroup;import android.widget.EditText;import android.widget.ImageButton;import android.widget.NumberPicker;import java.lang.reflect.Field;/** * Created by Moore on 2016/10/20. */public class MyNumberPicker extends NumberPicker {private static int mTextSize = 16;private static int mTextColor = 0x000000;private static int mDividerColor = 0xFFFF00;public MyNumberPicker(Context context) {super(context);setNumberPickerDividerColor();}public MyNumberPicker(Context context, AttributeSet attrs) {super(context, attrs);setNumberPickerDividerColor();}public MyNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);setNumberPickerDividerColor();}@Overridepublic void addView(View child) {super.addView(child);updateView(child);}@Overridepublic void addView(View child, int index, ViewGroup.LayoutParams params) {super.addView(child, index, params);updateView(child);}@Overridepublic void addView(View child, ViewGroup.LayoutParams params) {super.addView(child, params);updateView(child);}private void updateView(View view) {if (view instanceof EditText) {//((EditText) view).setTextSize(mTextSize);((EditText) view).setTextSize(17);//((EditText) view).setTextColor(mTextColor);((EditText) view).setTextColor(Color.parseColor("#6495ED"));}}private void setNumberPickerDividerColor() {Field[] pickerFields = NumberPicker.class.getDeclaredFields();/** * 设置分割线颜色 */for (Field pf : pickerFields) {if (pf.getName().equals("mSelectionDivider")) {pf.setAccessible(true);try {//pf.set(this, new ColorDrawable(mDividerColor));pf.set(this, new ColorDrawable(Color.parseColor("#C4C4C4")));} catch (IllegalAccessException e) {e.printStackTrace();}break;}}/** * 设置分割线高度 */for (Field pf : pickerFields) {if (pf.getName().equals("mSelectionDividerHeight")) {pf.setAccessible(true);try {pf.set(this, 2);} catch (IllegalAccessException e) {e.printStackTrace();}break;}}for (Field pf : pickerFields) {if (pf.getName().equals("mSelectorElementHeight")) {pf.setAccessible(true);try {pf.set(this, 2);} catch (IllegalAccessException e) {e.printStackTrace();}break;}}}public void setDividerColor(int color) {this.mDividerColor = color;//this.postInvalidate();}public void setTextColor(int color) {this.mTextColor = color;//this.postInvalidate();}public void setTextSize(int textSize) {this.mTextSize = textSize;//this.postInvalidate();}}
完整Demo可前往github查看与下载,地址:https://github.com/lizebinbin/PickTimeView.git 谢谢!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。