Android View 自定义RangeSeekBar范围选择器 走在View进阶之路

前段时间群里兄弟项目中有类似这样的需求

我看到兄弟受苦受难,于心不忍。又因事不关己,打算高高挂起。正在爱恨纠结之时,日神对我说:没事多造点轮子,你的人生会有很多收获。这波鸡汤让我深受触动,于是决定拯救兄弟于水生火热之中。

重写onMeasure 决策自身大小

显而易见当可以拖拽的范围极限为零时,也就是RangeSeeBar正常显示能够接受的极限,粗略一看:Width > 2 * Height

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	int widthSize = MeasureSpec.getSize(widthMeasureSpec);
	int heightSize = MeasureSpec.getSize(heightMeasureSpec);
	if (heightSize * 2 > widthSize) {
		setMeasuredDimension(widthSize, widthSize / 2);
	} else {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	}
}

绘制拖动条背景 凡事先从简单开始

public class RangeSeekBar extends View {
    private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);

    private int lineTop, lineBottom, lineLeft, lineRight;
    private int lineCorners;
    private int lineWidth;
    private RectF line = new RectF();

    public RangeSeekBar(Context context) {
        this(context, null);
    }

    public RangeSeekBar(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (heightSize * 2 > widthSize) {
            setMeasuredDimension(widthSize, (int) (widthSize / 2));
        } else {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        int seekBarRadius = h / 2;
        /**
         * 属性 left right top bottom 描述了SeekBar按钮的位置
         * 蓝后根据它们预先设置确定出 RectF line 背景的三维
         * lineCorners 圆滑的边缘似乎会比直角更好看
         */
        lineLeft = seekBarRadius;
        lineRight = w - seekBarRadius;
        lineTop = seekBarRadius - seekBarRadius / 4;
        lineBottom = seekBarRadius + seekBarRadius / 4;
        lineWidth = lineRight - lineLeft;
        line.set(lineLeft, lineTop, lineRight, lineBottom);
        lineCorners = (int) ((lineBottom - lineTop) * 0.45f);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(0xFFD7D7D7);
        canvas.drawRoundRect(line, lineCorners, lineCorners, paint);
    }
}

很明显这里设计seekBarRadius作为SeekBar按钮的半径,值为RangeSeekBar自身高度一半。那么为了使默认状态的SeekBar按钮圆心能压在背景条的起点和终点

背景条的起点和终点当然就分别相对于自身宽度往内部偏移一个半径咯。

拖动舞台已经备好,SeekBar按钮半径也已定好。顺水推舟,下一步就绘制SeekBar把。

SeekBar按钮 拥有对象是极好的

粗略一想:按钮有颜色、有大小、有变色、被绘制,碰撞检测、边界检测、被拖拽等,最关键的是有多个。因此SeekBar按钮可以说是一个复杂的集合体,是时候来发对象了。

private class SeekBar {
	int widthSize;
	int left, right, top, bottom;
	Bitmap bmp;

	/**
	 * 当RangeSeekBar尺寸发生变化时,SeekBar按钮尺寸随之变化
	 *
	 * @param centerX    SeekBar按钮的X中心在RangeSeekBar中的相对位置
	 * @param centerY    SeekBar按钮的Y中心在RangeSeekBar中的相对位置
	 * @param heightSize RangeSeekBar期望SeekBar所拥有的高度
	 */
	void onSizeChanged(int centerX, int centerY, int heightSize) {
		/**
		 * 属性 left right top bottom 描述了SeekBar按钮的位置<br>
		 * widthSize = heightSize * 0.8f 可见按钮实际区域是个矩形而非正方形
		 * 圆圈按钮为什么要占有矩形区域?因为按钮阴影效果。不要阴影不行吗?我就不
		 * 那么 onMeasure 那边说好的2倍宽度?我就不
		 */
		widthSize = (int) (heightSize * 0.8f);
		left = centerX - widthSize / 2;
		right = centerX + widthSize / 2;
		top = centerY - heightSize / 2;
		bottom = centerY + heightSize / 2;

		bmp = Bitmap.createBitmap(widthSize, heightSize, Bitmap.Config.ARGB_8888);
		int bmpCenterX = bmp.getWidth() / 2;
		int bmpCenterY = bmp.getHeight() / 2;
		int bmpRadius = (int) (widthSize * 0.5f);
		Canvas defaultCanvas = new Canvas(bmp);
		Paint defaultPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
		// 绘制Shadow
		defaultPaint.setStyle(Paint.Style.FILL);
		int barShadowRadius = (int) (bmpRadius * 0.95f);
		defaultCanvas.save();
		defaultCanvas.translate(0, bmpRadius * 0.25f);
		RadialGradient shadowGradient = new RadialGradient(bmpCenterX, bmpCenterY, barShadowRadius, Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP);
		defaultPaint.setShader(shadowGradient);
		defaultCanvas.drawCircle(bmpCenterX, bmpCenterY, barShadowRadius, defaultPaint);
		defaultPaint.setShader(null);
		defaultCanvas.restore();
		// 绘制Body
		defaultPaint.setStyle(Paint.Style.FILL);
		defaultPaint.setColor(0xFFFFFFFF);
		defaultCanvas.drawCircle(bmpCenterX, bmpCenterY, bmpRadius, defaultPaint);
		// 绘制Border
		defaultPaint.setStyle(Paint.Style.STROKE);
		defaultPaint.setColor(0xFFD7D7D7);
		defaultCanvas.drawCircle(bmpCenterX, bmpCenterY, bmpRadius, defaultPaint);
	}

	void draw(Canvas canvas) {
        canvas.drawBitmap(bmp, left, top, null);
    }
}
public class RangeSeekBar extends View {

    private SeekBar seekBar = new SeekBar();

    private class SeekBar {
        ...
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        int seekBarRadius = h / 2;
        ...
		// 在RangeSeekBar确定尺寸时确定SeekBar按钮尺寸
        seekBar.onSizeChanged(seekBarRadius, seekBarRadius, h);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        ...
		// 在RangeSeekBar被绘制时绘制SeekBar按钮
        seekBar.draw(canvas);
    }
}

距离成功又进了一步

onTouchEvent 触摸监听 让SeekBar按钮动起来

@Override
public boolean onTouchEvent(MotionEvent event) {
	switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			boolean touchResult = false;
			// 进行检测,手指手指是否落在当前SeekBar上。即声明SeekBar时使用left、top、right、bottom属性所描述区域的内部
			if (seekbar.collide(event)) {
				touchResult = true;
			}
			return touchResult;
		case MotionEvent.ACTION_MOVE:
			float percent;
			float x = event.getX();
			if (x <= lineLeft) {
				percent = 0;
			} else if (x >= lineRight){
				percent = 1;
			} else {
				percent = (x - lineLeft) * 1f / (lineWidth);
			}
			// SeekBar按钮根据当前手指在拖动条上的滑动而滑动
			seekbar.slide(percent);
			invalidate();
			break;
	}
	return super.onTouchEvent(event);
}
private class SeekBar {
	int lineWidth; // 拖动条宽度 可在onSizeChanged时刻获得
	float currPercent;
	int left, right, top, bottom;

	boolean collide(MotionEvent event) {
		float x = event.getX();
		float y = event.getY();
		int offset = (int) (lineWidth * currPercent);
		return x > left + offset && x < right + offset && y > top && y < bottom;
	}

	void slide(float percent) {
		if (percent < 0) percent = 0;
		else if (percent > 1) percent = 1;
		currPercent = percent;
	}

	void draw(Canvas canvas) {
		int offset = (int) (lineWidth * currPercent);
		canvas.save();
		canvas.translate(offset, 0);
		canvas.drawBitmap(bmp, left, top, null);
		canvas.restore();
	}
}

更好的视觉体验

到目前位置,SeekBar被按压时显得死气沉沉,接下来为其添加强烈的视觉反馈。

那么之前通过onSizeChanged预设按钮的偷懒手段就GG了,因为SeekBar的UI效果需要随触摸状态的变化而变化。

首先在onTouchEvent中拿到这个变化

@Override
public boolean onTouchEvent(MotionEvent event) {
	switch (event.getAction()) {
		case MotionEvent.ACTION_MOVE:
			seekBar.material = seekBar.material >= 1 ? 1 : seekBar.material + 0.1f;
			...
			invalidate();
			break;
		case MotionEvent.ACTION_CANCEL:
		case MotionEvent.ACTION_UP:
			seekBar.materialRestore();
			break;
	}
	return super.onTouchEvent(event);
}

之后在SeekBar按钮中响应这个变化

private class SeekBar {
	float material = 0;
	ValueAnimator anim;
	final TypeEvaluator<Integer> te = new TypeEvaluator<Integer>() {
		@Override
		public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
			int alpha = (int) (Color.alpha(startValue) + fraction * (Color.alpha(endValue) - Color.alpha(startValue)));
			int red = (int) (Color.red(startValue) + fraction * (Color.red(endValue) - Color.red(startValue)));
			int green = (int) (Color.green(startValue) + fraction * (Color.green(endValue) - Color.green(startValue)));
			int blue = (int) (Color.blue(startValue) + fraction * (Color.blue(endValue) - Color.blue(startValue)));
			return Color.argb(alpha, red, green, blue);
		}
	};

	void draw(Canvas canvas) {
		int offset = (int) (lineWidth * currPercent);
		canvas.save();
		canvas.translate(left, 0);
		canvas.translate(offset, 0);
		drawDefault(canvas);
		canvas.restore();
	}

	private void drawDefault(Canvas canvas) {
		int centerX = widthSize / 2;
		int centerY = heightSize / 2;
		int radius = (int) (widthSize * 0.5f);
		// draw shadow
		defaultPaint.setStyle(Paint.Style.FILL);
		canvas.save();
		canvas.translate(0, radius * 0.25f);
		canvas.scale(1 + (0.1f * material), 1 + (0.1f * material), centerX, centerY);
		defaultPaint.setShader(shadowGradient);
		canvas.drawCircle(centerX, centerY, radius, defaultPaint);
		defaultPaint.setShader(null);
		canvas.restore();
		// draw body
		defaultPaint.setStyle(Paint.Style.FILL);
		defaultPaint.setColor(te.evaluate(material, 0xFFFFFFFF, 0xFFE7E7E7));
		canvas.drawCircle(centerX, centerY, radius, defaultPaint);
		// draw border
		defaultPaint.setStyle(Paint.Style.STROKE);
		defaultPaint.setColor(0xFFD7D7D7);
		canvas.drawCircle(centerX, centerY, radius, defaultPaint);
	}

	private void materialRestore() {
		if (anim != null) anim.cancel();
		anim = ValueAnimator.ofFloat(material, 0);
		anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
			@Override
			public void onAnimationUpdate(ValueAnimator animation) {
				material = (float) animation.getAnimatedValue();
				invalidate();
			}
		});
		anim.addListener(new AnimatorListenerAdapter() {
			@Override
			public void onAnimationEnd(Animator animation) {
				material = 0;
				invalidate();
			}
		});
		anim.start();
	}
}

draw方法中的直接绘制bmp的逻辑被替换为drawDefault

那么drawDefault的内部逻辑基本和预制bmp一样,唯二的区别在于对阴影Shadow做了个scale处理,对按钮Body颜色做了个渐变处理

materialRestore即当用户手指抬起后开个线程将状态渐变为初始态

Range

Range的意思就是范围,但是就算知道这些似乎并没有什么卵用 _(:3 」∠)_

so为了了解其中规律,本宝宝使劲摸索。最终发现

如果分开来看它们都拥有自己的固定滑动区间,右边的SeekBar按钮就是左边SeekBar按钮向右平移了个SeekBar按钮宽度而已。

public class RangeSeekBar extends View {
    private SeekBar leftSB = new SeekBar();
    private SeekBar rightSB = new SeekBar();
    /**
     * 用来记录当前用户触摸的到底是哪个SB
     */
    private SeekBar currTouch;

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        ...
        // rightSB就如同分析的一样,紧紧贴在leftSB的右边而已
        rightSB.left += leftSB.widthSize;
        rightSB.right += leftSB.widthSize;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        ...
        leftSB.draw(canvas);
        rightSB.draw(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                boolean touchResult = false;
                /**
                 * 为什么不先检测leftSB而先检测rightSB?为什么? (●'?'●)
                 */
                if (rightSB.collide(event)) {
                    currTouch = rightSB;
                    touchResult = true;
                } else if (leftSB.collide(event)) {
                    currTouch = leftSB;
                    touchResult = true;
                }
                return touchResult;
            case MotionEvent.ACTION_MOVE:
                float percent;
                float x = event.getX();

                if (currTouch == leftSB) {
                    if (x < lineLeft) {
                        percent = 0;
                    } else {
                        percent = (x - lineLeft) * 1f / (lineWidth - rightSB.widthSize);
                    }

                    if (percent > rightSB.currPercent) {
                        percent = rightSB.currPercent;
                    }
                    leftSB.slide(percent);
                } else if (currTouch == rightSB) {
                    if (x > lineRight) {
                        percent = 1;
                    } else {
                        percent = (x - lineLeft - leftSB.widthSize) * 1f / (lineWidth - leftSB.widthSize);
                    }
                    if (percent < leftSB.currPercent) {
                        percent = leftSB.currPercent;
                    }
                    rightSB.slide(percent);
                }

                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }
}

通过触摸改变一些属性的值,通过这些属性的值绘制出对应的UI效果,套路一切都是套路

那么继SwitchButton后,又算是重新温习了一次该套路

那么本宝宝的RangeSeekBar还能做到什么?

支持负数

支持预留(保留)范围

什么是预留(保留)范围?比如那个,你懂得。只可意会,不可言传。(? ? ?)?

比如现在2个按钮直接就保留了一个距离,当然也可以保留n个

支持刻度模式

当然支持刻度的同时也支持预留范围

支持自定义UI按钮样式背景颜色

似乎少了按压状态变化

如何使用?

[戳我转到RangeSeekBar使用教程]

如果您喜欢这篇文章,您也可以进行打赏, 金额不限

      

版权声明:欢迎转载,但请尊重作者劳动成果,转载请注明出处-->http://blog.csdn.net/bfbx5173 QQ群:274306954

时间: 2024-10-14 00:44:04

Android View 自定义RangeSeekBar范围选择器 走在View进阶之路的相关文章

Android中自定义视图View之---前奏篇

前言 好长时间没写blog了,心里感觉有点空荡荡的,今天有时间就来写一个关于自定义视图的的blog吧.关于这篇blog,网上已经有很多案例了,其实没什么难度的.但是我们在开发的过程中有时候会用到一些自定义的View以达到我们所需要的效果.其实网上的很多案例我们看完之后,发现这部分没什么难度的,我总结了两点: 1.准备纸和笔,计算坐标 2.在onDraw方法中开始画图,invalidate方法刷新,onTouchEvent方法监听触摸事件 对于绘图相关的知识,之前在弄JavaSE相关的知识的时候,

Android中自定义视图View之---开发案例

自定义视图View的案例 下面我们就是开始正式的进入自定义视图View了 在讲解正式内容之前,我们先来看一下基本知识 1.我们在自定义视图View的时候正确的步骤和方法 1).必须定义有Context/Attrbuite参数的构造方法,并且调用父类的方法 public LabelView(Context context, AttributeSet attrs) 不然会报错: 2).重写onMeasure方法 @Override protected void onMeasure(int width

关于android开发自定义view

Android App开发过程中,很多时候会遇到系统框架中提供的控件无法满足我们产品的设计需求,那么这时候我们可以选择先Google下有没有比较成熟的开源项目可以让我们用,或者Github上面的项目也非常丰富,能够满足我们绝不多数的开发需求,但是在使用这些炫酷的第三方控件时,需要我们掌控里面实现的细节,所以就滋生了今天“关于android开发中自定义view的探索” 粗体是我个人的疑问,下面小字部分是结合查阅资料以及实际操作的归纳理解. 什么是View? Android所有的控件都是View或者

Android中自定义View的MeasureSpec使用

有时,Android系统控件无法满足我们的需求,因此有必要自定义View.具体方法参见官方开发文档:http://developer.android.com/guide/topics/ui/custom-components.html 一般来说,自定义控件都会去重写View的onMeasure方法,因为该方法指定该控件在屏幕上的大小. protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) onMeasure传

android中自定义view涉及到的绘制知识

android中自定义view的过程中,需要了解的绘制知识. 1.画笔paint: 画笔设置: <span style="font-size:14px;"> paint.setAntiAlias(true);//抗锯齿功能 paint.setColor(Color.RED); //设置画笔颜色 paint.setStyle(Style.FILL);//设置填充样式 paint.setStrokeWidth(30);//设置画笔宽度 paint.setShadowLayer(

【2014年最后的分享啦】Android实现自定义刮刮卡效果View

一.简介: 今天是2014年最后一天啦,首先在这里,我祝福大家在新的2015年都一个个的新健康,新收入,新顺利,新如意!!! 上一偏,我介绍了用Xfermode实现自定义圆角和椭圆图片view的博文<Android实现自定义圆形.圆角和椭圆ImageView(使用Xfermode图形渲染方法)>, 今天我们来看看如何实现电商app里常用到的刮刮卡效果的view组件,其实原理和实现圆角图片的差不多,都是使用Xfermode渲染模式来实现的. (老规矩,源码在博文最后给出哈) 基本原理步骤是这样的

Android 开发自定义View

作者:卿笃军 原文地址:http://blog.csdn.net/qingdujun/article/details/41551151 [附:--自定义View常处理的回调函数 onFinishInflate() 当View中所有的子控件均被映射成xml后触发 onMeasure(int, int) 确定所有子元素的大小 onLayout(boolean, int, int, int, int) 当View分配所有的子元素的大小和位置时触发 onSizeChanged(int, int, int

Android自定义View——自定义搜索框(SearchView)

概述 在Android开发中,当系统数据项比较多时,常常会在app添加搜索功能,方便用户能快速获得需要的数据.搜索栏对于我们并不陌生,在许多app都能见到它,比如豌豆荚 在某些情况下,我们希望我们的自动补全信息可以不只是纯文本,还可以像豌豆荚这样,能显示相应的图片和其他数据信息,因此Android给我们提供的AutoCompleteTextView往往就不够用,在大多情况下我们都需要自己去实现搜索框. 分析 根据上面这张图,简单分析一下自定义搜索框的结构与功能,有 1. 搜索界面大致由三部门组成

Android 中自定义View的初步总结

概述 在开发过程中,经常会遇到系统中提供的控件无法满足产品的设计需求,这时可能就需要考虑使用自定义的View来实现产品的设计细节了.对于自定义View,可以分为两种,一种是自定义控件(继承View),另一种是自定义布局容器(继承ViewGroup),下面就针对自定义控件View的应用进行简单的总结. 自定义View 自定义View时,我们大部分只需要重写两个方法onMeasure(),onDraw().onMeasure()负责对当前View尺寸进行测量,onDraw()负责把当前这个View绘