Android 滑动数字选择器是一种用户界面控件,它允许用户从一系列数字中选择一个值。用户可以通过滑动手势或点击手势来选择数字。以下是一些关于 Android 滑动数字选择器的信息和链接:
-
Android NumberPicker:这是 Android 框架提供的原生数字选择器控件。它可以通过 XML 或代码创建,并支持自定义样式和属性。官方文档链接:
https://developer.android.com/reference/android/widget/NumberPicker
-
Android WheelPicker:这是一个第三方的数字选择器库,它提供了多种样式和配置选项。它可以通过 Gradle 或手动导入方式添加到项目中。GitHub 链接:
GitHub – AigeStudio/WheelPicker: Simple and fantastic wheel view in realistic effect for android.
-
Android ScrollableNumberPicker:这是另一个第三方的数字选择器库,它支持水平和垂直滚动模式,并提供了多种自定义选项。它可以通过 Gradle 或手动导入方式添加到项目中。GitHub 链接:
https://github.com/michaelbel/ScrollableNumberPicker
一、以下是一个简单的 Android NumberPicker 示例代码:
<NumberPicker
android:id="@+id/numberPicker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:maxValue="10"
android:minValue="1"
android:value="5" />
NumberPicker numberPicker = findViewById(R.id.numberPicker);
numberPicker.setMinValue(1);
numberPicker.setMaxValue(10);
numberPicker.setValue(5);
numberPicker.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
@Override
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
// 处理数值变化事件
}
});
二、以下是我个人写的一个滚动选择器
1.项目结构:
2.先定义两个类
NumPicker
package com.example.myapplication;
import android.app.Activity;
import android.app.Dialog;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
/**
* Author cjet
* Date 2018-1-16 14:02
*/
public class NumPicker {
private Activity mActivity;
private TextView tvCancel;
private Button tvComfirm;
private TextView tvTitle;
private NumPickView mNpv;
private Dialog mDialog;
private OnCancelClickListener mCancelListener;
private onComfirmClickListener mComfirmListener;
private int currentSelecedNum;
NumPicker(Activity activity) {
mActivity = activity;
initDialog();
}
private void initDialog() {
mDialog = new Dialog(mActivity, R.style.BottomSheetDialog);
mDialog.setContentView(mActivity.getLayoutInflater().inflate(R.layout.picker_layout, null));
Display dd = mActivity.getWindowManager().getDefaultDisplay();
DisplayMetrics dm = new DisplayMetrics();
dd.getMetrics(dm);
WindowManager.LayoutParams attributes = mDialog.getWindow().getAttributes();
mDialog.getWindow().setGravity(Gravity.BOTTOM);
attributes.height = (int) (dm.heightPixels * 0.4);
attributes.width = dm.widthPixels;
mDialog.getWindow().setWindowAnimations(R.style.dialogWindowAnimation);
//tvCancel = mDialog.findViewById(R.id.tvCancel);
tvComfirm = mDialog.findViewById(R.id.tvConfirm);
//tvTitle = mDialog.findViewById(R.id.tvTitle);
mNpv = mDialog.findViewById(R.id.numPickView);
currentSelecedNum = mNpv.getCurrentPostion();//当前选择数
setListener();
}
private void setListener() {
mNpv.setOnSelectNumListener(new NumPickView.OnSelectNumListener() {
@Override
public void onSelected(int num) {
currentSelecedNum = num;
}
});
tvComfirm.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mComfirmListener != null) {
mComfirmListener.onClick(currentSelecedNum);
}
}
});
// tvCancel.setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View v) {
// if (mCancelListener != null) {
// mCancelListener.onClick();
// }
// }
// });
}
public void show() {
if (mDialog != null) {
mDialog.show();
}
}
public void dismiss() {
if (mDialog != null) {
mDialog.cancel();
}
}
public void selecNum(int num) {
mNpv.select(num);
}
public void setOnCancelListener(OnCancelClickListener listener) {
this.mCancelListener = listener;
}
public void setOnComfirmListener(onComfirmClickListener listener) {
this.mComfirmListener = listener;
}
public void setTitle(String title) {
//tvTitle.setText(title);
}
public interface OnCancelClickListener {
void onClick();
}
public interface onComfirmClickListener {
void onClick(int num);
}
}
NumPickView
package com.example.myapplication;
import static java.lang.Math.abs;
import static java.lang.Math.min;
import android.animation.Animator;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* Author uidq1152
* Date 2018-1-12 10:27
*/
public class NumPickView extends View {
private static final String TAG = "NumPickView";
private static final String DEF_TEXT_COLOR = "#FA6909";
private static final String DEF_START_COLOR = "#ECECEC";
/**
* D0D1D2
* 64666B
* 4C4E53
* 3A3D41
*/
//高
private int mHeight;
//宽
private int mWidth;
//二分之一高
private int middleHeight;
//二分之一宽
private int middleWidht;
//单位高度
private int mUnitHeight;
//数据
private List<String> mData = new ArrayList<>();
//当前位置
private int mCurrentPostion = 0;
//偏移量
private float pivot;
//画笔
private Paint mPaint;
//字体的矩形
private Rect mRect;
//落点Y
private float downY;
//缩放扩大比例
private float mScale;
//滚轮状态
private Status mStatus = Status.IDEL;
//遮罩效果
private LinearGradient mLg;
//数值估值器
private ValueAnimator mValueAnimator;
//字体大小
private int textSize;
//字体大小差
private int textStep;
//显示个数
private int mShowNum;
//字体颜色
private int mTextColor = Color.parseColor(DEF_TEXT_COLOR);
//选择监听
private OnSelectNumListener mListener;
//颜色渐变计算器
private ArgbEvaluator mArgvEvlauator;
public NumPickView(Context context) {
super(context);
}
public NumPickView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public NumPickView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.NumPickView);
for (int i = 0; i < ta.getIndexCount(); i++) {
int index = ta.getIndex(i);
switch (index) {
case R.styleable.NumPickView_totalNum:
int total = ta.getInteger(index, 24);
for (int j = 6; j < total; j++) {
if (j < 10) {
mData.add("0" + j);
} else {
mData.add(String.valueOf(j));
}
}
break;
case R.styleable.NumPickView_showNum:
mShowNum = ta.getInteger(index, 6);
break;
case R.styleable.NumPickView_textColor:
mTextColor = ta.getColor(index, mTextColor);
}
}
ta.recycle();
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setTextAlign(Paint.Align.CENTER);
mRect = new Rect();
mArgvEvlauator = new ArgbEvaluator();
mValueAnimator = new ValueAnimator();
mValueAnimator.setDuration(300);
mValueAnimator.setInterpolator(new LinearInterpolator());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
if (abs(pivot) > mUnitHeight) {
return;
}
pivot = value;
mScale = min(1, abs(pivot / mUnitHeight));
invalidate();
}
});
mValueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
if (mStatus == Status.UP && pivot != 0) {
mCurrentPostion = clamp(mCurrentPostion + 1);
} else if (mStatus == Status.DOWN && pivot != 0) {
mCurrentPostion = clamp(mCurrentPostion - 1);
}
invalidate();
pivot = 0;
mStatus = Status.IDEL;
mScale = 0;
if (mListener != null) {
mListener.onSelected(mCurrentPostion);
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
//展示个数
@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mHeight = getMeasuredHeight();
mWidth = getMeasuredWidth();
middleHeight = mHeight / 2;
middleWidht = mWidth / 2;
mUnitHeight = (mHeight - getPaddingTop() + getPaddingBottom()) / mShowNum;
textSize = mUnitHeight / 2;
textStep = mUnitHeight / 9;
}
@Override
protected void onDraw(Canvas canvas) {
//画选中字体
drawText(canvas, mData.get(mCurrentPostion), 0, 1);
//画除中间外上下字体
int num = mShowNum / 2;
for (int i = 1; i <= num; i++) {
drawText(canvas, mData.get(clamp(mCurrentPostion + i)), i, 1);
drawText(canvas, mData.get(clamp(mCurrentPostion - i)), i, -1);
}
}
/**
* 选中当前数值
*
* @param num index
*/
public void select(int num) {
if (num < 0 || num >= mData.size()) {
throw new IllegalArgumentException("The num must be in the range betwwen 0 and " + (mData.size() - 1));
}
mCurrentPostion = num;
if (mListener != null) {
mListener.onSelected(mCurrentPostion);
}
invalidate();
}
/**
* @param canvas
* @param text 要画的 String
* @param level 选中为0级,每差一个 index 加一级
* @param direct 以选中的为基准的方向,direct < 0 在上方,direct > 0 在下方
*/
private void drawText(Canvas canvas, String text, int level, int direct) {
mPaint.reset();
mPaint.setShader(null);
//字的位置渐变量
float offset = direct * level * mUnitHeight;
//字体的大小变化
float step = (direct * mStatus.getValue() * mScale * textStep);
if (level == 0) {
//中间字体无论怎么样都是缩小的
mPaint.setColor(mTextColor);
mPaint.setTextSize(textSize - abs(step));
mPaint.getTextBounds(text, 0, text.length(), mRect);
canvas.drawText(text, middleWidht - mRect.width() / 2, mHeight / 2 + mRect.height() / 2 + pivot, mPaint);
} else {
//其他字体根据上下和滑动方向关系放大或缩小, 颜色渐变
int color = (int) mArgvEvlauator.evaluate(1 - abs(mRect.height() / 2 + offset + pivot)/middleHeight
, Color.parseColor(DEF_START_COLOR)
, mTextColor);
mPaint.setColor(color);
mPaint.setTextSize(textSize - textStep * level + step);
mPaint.getTextBounds(text, 0, text.length(), mRect);
canvas.drawText(text, middleWidht - mRect.width() / 2, middleHeight + mRect.height() / 2 + offset + pivot, mPaint);
}
}
/**
* distanceY > 0: 向下
* distanceY < 0: 向上
*
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downY = event.getY();
mValueAnimator.cancel();
break;
case MotionEvent.ACTION_MOVE:
pivot = event.getY() - downY;
if (pivot > 0) {
//向下
mStatus = Status.DOWN;
if (abs(pivot) > mUnitHeight) {
mCurrentPostion = clamp(mCurrentPostion - 1);
downY = event.getY();
pivot = 0;
} else {
invalidate();
}
} else {
//向上
mStatus = Status.UP;
if (abs(pivot) > mUnitHeight) {
mCurrentPostion = clamp(mCurrentPostion + 1);
downY = event.getY();
pivot = 0;
} else {
invalidate();
}
}
mScale = min(1, abs(pivot / mUnitHeight));
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (pivot == 0) {
//把点击事件统一为滑动事件处理,简化流程。
pivot = 0.00001f;
}
if (abs(pivot) > mUnitHeight / 2) {
//需要过渡
int rest = (int) abs(mUnitHeight / 2 - pivot);
if (mStatus == Status.UP) {
mValueAnimator.setFloatValues(pivot, -rest);
} else if (mStatus == Status.DOWN) {
//这里需要注意
mValueAnimator.setFloatValues(pivot, (int) pivot + rest + mUnitHeight / 2);
}
} else {
//过渡失败,返回原数值,所以终点都是 0
if (mStatus == Status.UP) {
mValueAnimator.setFloatValues(pivot, 0);
} else if (mStatus == Status.DOWN) {
mValueAnimator.setFloatValues(pivot, 0);
}
}
if (mValueAnimator.getValues() == null || mValueAnimator.getValues().length == 0) {
return false;
}
mValueAnimator.start();
break;
}
return true;
}
public int getCurrentPostion() {
return mCurrentPostion;
}
/**
* 保证 index 合法化
*
* @param p 下标
* @return 合法后的下标
*/
private int clamp(int p) {
if (p > mData.size() - 1) {
return p - mData.size();
} else if (p < 0) {
return mData.size() - abs(p);
}
return p;
}
/**
* 设置滚轮监听
*
* @param listener 监听
*/
public void setOnSelectNumListener(OnSelectNumListener listener) {
this.mListener = listener;
}
/**
* 滚轮状态
*/
private enum Status {
UP(1), DOWN(-1), IDEL(0);
int value;
Status(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
/**
* 监听接口
*/
public interface OnSelectNumListener {
void onSelected(int num);
}
}
主布局文件activity_main
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
dialog中需要的布局文件,也就是数字选择器的布局picker_layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<com.example.myapplication.NumPickView
android:id="@+id/numPickView"
android:layout_width="wrap_content"
android:layout_height="250dp"
app:textColor="#10b7ff"
app:showNum="6"
app:totalNum="49"/>
<TextView
android:layout_width="15dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_marginTop="110dp"
android:layout_marginLeft="-130dp"
android:textSize="20sp"
android:text="A"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-30dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:orientation="horizontal">
<Button
android:id="@+id/tvConfirm"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginBottom="5dp"
android:text="确定"
android:textSize="18sp"
android:textColor="@color/white"
android:layout_weight="1"
android:gravity="center"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
剩下最后activity中的主代码了:
package com.example.myapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button dundong = this.findViewById(R.id.btn);
final NumPicker np = new NumPicker(MainActivity.this);
np.setOnCancelListener(new NumPicker.OnCancelClickListener() {
@Override
public void onClick() {
np.dismiss();
}
});
np.setOnComfirmListener(new NumPicker.onComfirmClickListener() {
@Override
public void onClick(int num) {
np.dismiss();
num = num + 6;
Toast.makeText(MainActivity.this, "你选择了"+num, Toast.LENGTH_SHORT).show();
}
});
dundong.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
np.show();
}
});
}
}
运行效果