前言:前几天看到了迅雷app上有个一元夺宝,那时无聊就参加了几次,到后来中奖结果出来了,哎,根本就没我的份啊(意料之中),还好投的钱不多,就一块两块,不过,我看到了那里有个数值输入的效果,我当时想了想这个实现挺简单了,应该有很多种方式,于是做了个决定,如果没中奖,我就把那个数值输入器做出来以惩罚自己(开玩笑~),现在,该是兑现自己承诺的时候了。
先看看我们即将要实现的效果图:
大家看到这个效果,要实现这么一个控件,我们可以怎么做?
一开始,我心想,这还不简单,自定义一个view,然后分别画出左边的减号按钮,中间的输入控件,和右边的加法按钮,包括最外边的边框,最后重新view的事件监听方法,在里面判断点击的坐标,如果是点击了左边按钮的坐标范围,那么让中间的输入控件数值递减,如果是点击了右边按钮的坐标范围,那么让中间的输入按钮数值递增。
嗯,这听起来不错,好吧,我动手试着按照这种思路开始做了,当我在画中间的输入按钮的时候,我懵了,怎么才能画出一个可以让用户输入的控件?于是,我放弃了这种思路,开始了另一种思路。
接着,我想,这用类似组合控件的形式不是很简单吗,干嘛搞得那么复杂啊,这样一来,不是不仅不用自己画一个输入控件了,而且还不用计算点击坐标了。所以,下面将用组合控件的形式实现这个控件。
1. 自定义属性
为了提供控件的可定制性,我们需要自定义属性,观察上面的效果图,我们可以定义下面9个属性供用户使用(当然,你也可以定义其他的属性供用户定制)
2. 自定义加减按钮
为了解决耦合问题,那个减号,加号按钮使用自定义view绘图来实现,不引用任何资源,这里以加号按钮为例,做个说明(在文章结尾将会贴上全部代码)。
class PlusView extends View{
private Paint mPlusPaint;
public PlusView(Context context) {
this(context, null);
}
public PlusView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PlusView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPlusPaint = new Paint();
mPlusPaint.setAntiAlias(true);
mPlusPaint.setStrokeWidth(mPlusWidth);
mPlusPaint.setStyle(Paint.Style.STROKE);
mPlusPaint.setStrokeCap(Paint.Cap.ROUND);
mPlusPaint.setColor(mPlusColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthMode == MeasureSpec.AT_MOST){
widthSize = Math.min(widthSize,100);
}
if(heightMode == MeasureSpec.AT_MOST){
heightSize = Math.min(heightSize,100);
}
setMeasuredDimension(widthSize,heightSize);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
canvas.drawLine(mWidth - mWidth / 5, mHeight / 2, mWidth / 5, mHeight / 2, mPlusPaint);
canvas.save();
canvas.rotate(90, mWidth / 2, mHeight / 2);
canvas.drawLine(mWidth / 5, mHeight / 2, mWidth - mWidth / 5, mHeight / 2, mPlusPaint);
canvas.restore();
}
}
这里自定义view,就是在测量的时候判断一些测量模式,如果是AT_MOST模式,那么给个最小值。测量完了以后,在onDraw()
方法中画个加号就行了,对于自定义view不熟悉的可以参考这篇文章:android自定义view(一),打造绚丽的验证码
3. 继承LinearLayout实现控件组合
观察上面的效果图,可以看出这个控件的三部分是等宽的,所以,使用LinearLayout实现非常简单(weight属性),下面,定义类继承LinearLayout:
public class NumberInput extends LinearLayout {
public NumberInput(Context context) {
this(context, null);
}
public NumberInput(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NumberInput(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// viewGroup必须设置背景,这样才会调用onDraw()方法
setBackgroundColor(Color.TRANSPARENT);
init(context,attrs);
initView(context);
}
}
这里,三个构造方法的调用情况就不多说了,不懂的可以问度娘~。我们在构造方法中做了一些初始化的工作,包括获取布局属性值,添加子view等等。这里,要注意的事,viewGroup默认是不会调用onDraw()
方法的,我们可以给它设置背景强制viewGroup调用onDraw()
方法。
获取属性值:
/** * 获取自定义属性值 */
private void init(Context context,AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.NumberInput);
mBorderColor = typedArray.getColor(R.styleable.NumberInput_border_color, Color.GRAY);
mBorderWidth = typedArray.getDimension(R.styleable.NumberInput_border_width, 6);
mMinusColor = typedArray.getColor(R.styleable.NumberInput_minus_color, Color.GRAY);
mMinusWidth = typedArray.getDimension(R.styleable.NumberInput_minus_width, 5);
mPlusColor = typedArray.getColor(R.styleable.NumberInput_plus_color, Color.GRAY);
mPlusWidth = typedArray.getDimension(R.styleable.NumberInput_plus_width, 5);
mInitialValue = typedArray.getInteger(R.styleable.NumberInput_initial_value, 0);
mMinValue = typedArray.getInteger(R.styleable.NumberInput_min_value, Integer.MIN_VALUE);
mMaxValue = typedArray.getInteger(R.styleable.NumberInput_max_value, Integer.MAX_VALUE);
// 回收资源
typedArray.recycle();
}
添加子View:
private void initView(Context context) {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
params.setMargins(20, 6, 20, 6);
params.weight = 1;
mMinusButton = new MinusView(context);
mMinusButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int currentNum = Integer.valueOf(mEditText.getText().toString());
currentNum--;
mEditText.setText(currentNum + "");
}
});
if(mInitialValue<=mMinValue){
mMinusButton.setEnabled(false);
}
addView(mMinusButton, params);
mEditText = new EditText(context);
// 做个判断,如果给的初始值小于最小值,那么初始值就是最小值
// 如果给的初始值大于最大值,那么初始值就是最大值
if(mInitialValue<=mMinValue){
mEditText.setText(mMinValue + "");
}else if(mInitialValue>=mMaxValue){
mEditText.setText(mMaxValue + "");
}else {
mEditText.setText(mInitialValue + "");
}
mEditText.requestFocus();
mEditText.setGravity(Gravity.CENTER);
mEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
// 去除默认的下划线
mEditText.setBackground(null);
mEditText.addTextChangedListener(mWatcher);
addView(mEditText, params);
mPlusButton = new PlusView(context);
params.weight = 1;
mPlusButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int currentNum = Integer.valueOf(mEditText.getText().toString());
currentNum++;
mEditText.setText(currentNum + "");
}
});
if(mInitialValue>=mMaxValue){
mPlusButton.setEnabled(false);
}
addView(mPlusButton,params);
}
这里相信大家都看得懂,就是用代码分别添加了左边按钮,中间的输入控件和右边的按钮,并做了一些初始判断操作和绑定点击事件的操作,其中,给EditView添加了文本改变监听:
private TextWatcher mWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
int value = Integer.valueOf(mEditText.getText().toString());
if(value<=mMinValue){
mMinusButton.setEnabled(false);
}else{
mMinusButton.setEnabled(true);
}
if(value>=mMaxValue){
mPlusButton.setEnabled(false);
}else{
mPlusButton.setEnabled(true);
}
}
@Override
public void afterTextChanged(Editable s) {
}
};
完成了子控件的添加,那么,这个容器还需要绘制最外边和中间的边框
@Override
protected void onDraw(Canvas canvas) {
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
Paint borderPaint = new Paint();
borderPaint.setAntiAlias(true);
borderPaint.setStrokeWidth(mBorderWidth);
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setColor(mBorderColor);
canvas.drawRect(0, 0, mWidth, mHeight, borderPaint);
borderPaint.setStrokeWidth(mBorderWidth/2);
canvas.drawLine(mWidth / 3 - mBorderWidth/2, 0, mWidth / 3 - mBorderWidth/2, mHeight, borderPaint);
canvas.drawLine(mWidth * 2 / 3 - mBorderWidth/2, 0, mWidth * 2 / 3 - mBorderWidth/2, mHeight, borderPaint);
}
最后将编辑控件中的数值暴露出去:
/** * 返回控件中的数值 * @return 数值 */
public int getNumber(){
return Integer.valueOf(mEditText.getText().toString());
}
都这里,这个简易的数值输入器就基本完工了。
4. 使用控件
万事具备,只欠测试了,所以,写一个测试的布局文件,如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:gravity="center" android:orientation="vertical" android:layout_height="match_parent" >
<com.example.lt.custom_number_input.NumberInput android:layout_width="wrap_content" android:layout_centerInParent="true" custom:initial_value="11" custom:min_value="0" custom:max_value="10" android:background="@android:color/transparent" android:layout_height="wrap_content">
</com.example.lt.custom_number_input.NumberInput>
<com.example.lt.custom_number_input.NumberInput android:layout_width="wrap_content" android:layout_centerInParent="true" android:layout_marginTop="30dp" custom:border_color="#69BAD8" custom:plus_color="#DD4814" custom:initial_value="-1" custom:min_value="0" custom:max_value="10" android:background="@android:color/transparent" android:layout_height="wrap_content">
</com.example.lt.custom_number_input.NumberInput>
<com.example.lt.custom_number_input.NumberInput android:layout_width="240dp" android:layout_marginTop="30dp" android:layout_centerInParent="true" custom:initial_value="0" custom:min_value="0" custom:max_value="10" android:background="@android:color/transparent" android:layout_height="80dp">
</com.example.lt.custom_number_input.NumberInput>
</LinearLayout>
将这个布局文件作为activity的内容视图,运行这个应用就可以测试效果了。
由于只是这个控件只是一个类而已,并没有涉及什么图片资源,所以就不麻烦大家下载了,直接贴上完整代码:
NumberInput.java
package com.example.lt.custom_number_input;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
/** * Created by lt on 2016/3/12. */
public class NumberInput extends LinearLayout {
private int mHeight;
private int mWidth;
private EditText mEditText;
private int mBorderColor;
private float mBorderWidth;
private int mMinusColor;
private float mMinusWidth;
private int mPlusColor;
private float mPlusWidth;
private int mInitialValue;
private int mMinValue;
private int mMaxValue;
private TextWatcher mWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
int value = Integer.valueOf(mEditText.getText().toString());
if(value<=mMinValue){
mMinusButton.setEnabled(false);
}else{
mMinusButton.setEnabled(true);
}
if(value>=mMaxValue){
mPlusButton.setEnabled(false);
}else{
mPlusButton.setEnabled(true);
}
}
@Override
public void afterTextChanged(Editable s) {
}
};
private View mPlusButton;
private View mMinusButton;
public NumberInput(Context context) {
this(context, null);
}
public NumberInput(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NumberInput(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// viewGroup必须设置背景,这样才会调用onDraw()方法
setBackgroundColor(Color.TRANSPARENT);
init(context,attrs);
initView(context);
}
/** * 获取自定义属性值 */
private void init(Context context,AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.NumberInput);
mBorderColor = typedArray.getColor(R.styleable.NumberInput_border_color, Color.GRAY);
mBorderWidth = typedArray.getDimension(R.styleable.NumberInput_border_width, 6);
mMinusColor = typedArray.getColor(R.styleable.NumberInput_minus_color, Color.GRAY);
mMinusWidth = typedArray.getDimension(R.styleable.NumberInput_minus_width, 5);
mPlusColor = typedArray.getColor(R.styleable.NumberInput_plus_color, Color.GRAY);
mPlusWidth = typedArray.getDimension(R.styleable.NumberInput_plus_width, 5);
mInitialValue = typedArray.getInteger(R.styleable.NumberInput_initial_value, 0);
mMinValue = typedArray.getInteger(R.styleable.NumberInput_min_value, Integer.MIN_VALUE);
mMaxValue = typedArray.getInteger(R.styleable.NumberInput_max_value, Integer.MAX_VALUE);
// 回收资源
typedArray.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
Paint borderPaint = new Paint();
borderPaint.setAntiAlias(true);
borderPaint.setStrokeWidth(mBorderWidth);
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setColor(mBorderColor);
canvas.drawRect(0, 0, mWidth, mHeight, borderPaint);
borderPaint.setStrokeWidth(mBorderWidth/2);
canvas.drawLine(mWidth / 3 - mBorderWidth/2, 0, mWidth / 3 - mBorderWidth/2, mHeight, borderPaint);
canvas.drawLine(mWidth * 2 / 3 - mBorderWidth/2, 0, mWidth * 2 / 3 - mBorderWidth/2, mHeight, borderPaint);
}
private void initView(Context context) {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
params.setMargins(20, 6, 20, 6);
params.weight = 1;
mMinusButton = new MinusView(context);
mMinusButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int currentNum = Integer.valueOf(mEditText.getText().toString());
currentNum--;
mEditText.setText(currentNum + "");
}
});
if(mInitialValue<=mMinValue){
mMinusButton.setEnabled(false);
}
addView(mMinusButton, params);
mEditText = new EditText(context);
// 做个判断,如果给的初始值小于最小值,那么初始值就是最小值
// 如果给的初始值大于最大值,那么初始值就是最大值
if(mInitialValue<=mMinValue){
mEditText.setText(mMinValue + "");
}else if(mInitialValue>=mMaxValue){
mEditText.setText(mMaxValue + "");
}else {
mEditText.setText(mInitialValue + "");
}
mEditText.requestFocus();
mEditText.setGravity(Gravity.CENTER);
mEditText.setInputType(InputType.TYPE_CLASS_NUMBER);
// 去除默认的下划线
mEditText.setBackground(null);
mEditText.addTextChangedListener(mWatcher);
addView(mEditText, params);
mPlusButton = new PlusView(context);
params.weight = 1;
mPlusButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int currentNum = Integer.valueOf(mEditText.getText().toString());
currentNum++;
mEditText.setText(currentNum + "");
}
});
if(mInitialValue>=mMaxValue){
mPlusButton.setEnabled(false);
}
addView(mPlusButton,params);
}
class MinusView extends View{
private Paint mMinusPaint;
public MinusView(Context context) {
this(context, null);
}
public MinusView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MinusView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mMinusPaint = new Paint();
mMinusPaint.setAntiAlias(true);
mMinusPaint.setStrokeWidth(mMinusWidth);
mMinusPaint.setStyle(Paint.Style.STROKE);
mMinusPaint.setStrokeCap(Paint.Cap.ROUND);
mMinusPaint.setColor(mMinusColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthMode == MeasureSpec.AT_MOST){
widthSize = Math.min(widthSize,80);
}
if(heightMode == MeasureSpec.AT_MOST){
heightSize = Math.min(heightSize,80);
}
setMeasuredDimension(widthSize, heightSize);
}
/** * 返回控件中的数值 * @return 数值 */
public int getNumber(){
return Integer.valueOf(mEditText.getText().toString());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
canvas.drawLine(mWidth/5,mHeight/2,mWidth-mWidth/5,mHeight/2,mMinusPaint);
}
}
class PlusView extends View{
private Paint mPlusPaint;
public PlusView(Context context) {
this(context, null);
}
public PlusView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PlusView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPlusPaint = new Paint();
mPlusPaint.setAntiAlias(true);
mPlusPaint.setStrokeWidth(mPlusWidth);
mPlusPaint.setStyle(Paint.Style.STROKE);
mPlusPaint.setStrokeCap(Paint.Cap.ROUND);
mPlusPaint.setColor(mPlusColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthMode == MeasureSpec.AT_MOST){
widthSize = Math.min(widthSize,100);
}
if(heightMode == MeasureSpec.AT_MOST){
heightSize = Math.min(heightSize,100);
}
setMeasuredDimension(widthSize,heightSize);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
canvas.drawLine(mWidth - mWidth / 5, mHeight / 2, mWidth / 5, mHeight / 2, mPlusPaint);
canvas.save();
canvas.rotate(90, mWidth / 2, mHeight / 2);
canvas.drawLine(mWidth / 5, mHeight / 2, mWidth - mWidth / 5, mHeight / 2, mPlusPaint);
canvas.restore();
}
}
}
总结
这个控件可能不是很完善,大家可以试着扩展一下它,比如:多定制一些属性,添加加减按钮不可用时按钮显示的颜色(这里提示一下,可以在我们添加的那个文本监听器中监听文本的变化,如果超出范围,那就改变一下画笔颜色的那个全局变量,再让加减按钮重新绘制即可(调用View的postInvalidate()
方法))。