本文共 14827 字,大约阅读时间需要 49 分钟。
Material Design系列的文章这是第五篇,今天讲滑块控件(Sliders)。
之前的传送门:(代码实现都靠画,学好View还是很重要的)
老规矩,先说下理论部分
滑块控件(Sliders,简称滑块)可以让我们通过在连续或间断的区间内滑动锚点来选择一个合适的数值。区间最小值放在左边,对应的,最大值放在右边。滑块(Sliders)可以在滑动条的左右两端设定图标来反映数值的强度。这种交互特性使得它在设置诸如音量、亮度、色彩饱和度等需要反映强度等级的选项时成为一种极好的选择。
这里贴一下我们的实现效果:
再贴一下官方的实现
当然,这个和其他控件一样,都有一套暗色主题的配色.
使用场景?
进度拖拽,选择范围值等。
为何使用?
引领用户返回合理的内容(输入的话出错率比较高,也很麻烦)
当然还有一种是可输入可滑动的,像这样:
文中例子实现是根据这个:
它的暗色主题是这样
原文地址:
再一部分是代码实现,老规矩,贴贴代码结构(这一系列的所有“栗子”,我都是单一做包的,方便大家使用)
另外2个类在之前的代码中已经解释过了,大家可以往前翻
主要说下Slider这个类
public class Slider extends CustomView
28行,继承于一个自定义的RelativeLayout
private int backgroundColor = Color.parseColor("#4CAF50"); private Ball ball; private Bitmap bitmap; private int max = 100; private int min = 0; private NumberIndicator numberIndicator; private OnValueChangedListener onValueChangedListener; private boolean placedBall = false; private boolean press = false; private boolean showNumberIndicator = false; private int value = 0; public Slider(Context context, AttributeSet attrs) { super(context, attrs); setAttributes(attrs); }
30-46行,构造函数调用初始化的方法,声明一系列所需变量,什么最大值最小值,是否显示气球什么的,补充一点这里有一个自己写的回调,之后会解释。
public int getMax() { return max; } public void setMax(int max) { this.max = max; } public int getMin() { return min; } public void setMin(int min) { this.min = min; }
48-62行,一系列的set get方法,主要用于设置范围。
public OnValueChangedListener getOnValueChangedListener() { return onValueChangedListener; } public void setOnValueChangedListener( OnValueChangedListener onValueChangedListener) { this.onValueChangedListener = onValueChangedListener; }
64-71,接口的set get方法,继续看下去。
public void setValue(final int value) { if (placedBall == false) post(new Runnable() { @Override public void run() { setValue(value); } }); else { this.value = value; float division = (ball.xFin - ball.xIni) / max; ViewHelper.setX(ball, value * division + getHeight() / 2 - ball.getWidth() / 2); ball.changeBackground(); } }
79-96行,设置进度的实现,如果中间变量判断为非初始化操作的时候,那么就绘制具体 进度条的触控点“圆”的位置,并且绘制条的颜色,球的状态等内容。
93行是具体改变状态的操作。 (会讲中间变量的操作,这里不理解可以带着疑问继续看下去)@Override public void invalidate() { ball.invalidate(); super.invalidate(); }
98-102行,刷新UI
public boolean isShowNumberIndicator() { return showNumberIndicator; } public void setShowNumberIndicator(boolean showNumberIndicator) { this.showNumberIndicator = showNumberIndicator; numberIndicator = (showNumberIndicator) ? new NumberIndicator( getContext()) : null; }
104-112,是否显示/设置 气泡球+进度值的操作
@Override public boolean onTouchEvent(MotionEvent event) { isLastTouch = true; if (isEnabled()) { if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_MOVE) { if (numberIndicator != null && numberIndicator.isShowing() == false) numberIndicator.show(); if ((event.getX() <= getWidth() && event.getX() >= 0)) { press = true; // calculate value int newValue = 0; float division = (ball.xFin - ball.xIni) / (max - min); if (event.getX() > ball.xFin) { newValue = max; } else if (event.getX() < ball.xIni) { newValue = min; } else { newValue = min + (int) ((event.getX() - ball.xIni) / division); } if (value != newValue) { value = newValue; if (onValueChangedListener != null) onValueChangedListener.onValueChanged(newValue); } // move ball indicator float x = event.getX(); x = (x < ball.xIni) ? ball.xIni : x; x = (x > ball.xFin) ? ball.xFin : x; ViewHelper.setX(ball, x); ball.changeBackground(); // If slider has number indicator if (numberIndicator != null) { // move number indicator numberIndicator.indicator.x = x; numberIndicator.indicator.finalY = Utils .getRelativeTop(this) - getHeight() / 2+80; numberIndicator.indicator.finalSize = getHeight() / 2; numberIndicator.numberIndicator.setText(""); } } else { press = false; isLastTouch = false; if (numberIndicator != null) numberIndicator.dismiss(); } } else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { if (numberIndicator != null) numberIndicator.dismiss(); isLastTouch = false; press = false; } } return true; }
114-174,具体滑动实现的操作,分析下流程
在手指按下去/挪动的时候判断是否有气泡,如果有就显示,如果手指的操作有效就产生一个数值如果挪动无效/没有挪动就还是最小值,在值有效地情况下把这个数值利用 onValueChangedListener.onValueChanged(newValue);传出去。在挪动的挪动的过程中或者挪动成功后还要改变ball的样式和效果。以及呈现我们的气球和为之所对应的数值。
如果操作手指离开/取消 控件区域 隐藏我们的气泡
@Override public void setBackgroundColor(int color) { backgroundColor = color; if (isEnabled()) beforeBackground = backgroundColor; }
176-181 设置颜色
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (!placedBall) { placeBall(); } Paint paint = new Paint(); if (value == min) { // Crop line to transparent effect if (bitmap == null) { bitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888); } Canvas temp = new Canvas(bitmap); paint.setColor(Color.parseColor("#B0B0B0")); paint.setStrokeWidth(Utils.dpToPx(2, getResources())); temp.drawLine(getHeight() / 2, getHeight() / 2, getWidth() - getHeight() / 2, getHeight() / 2, paint); Paint transparentPaint = new Paint(); transparentPaint.setColor(getResources().getColor( android.R.color.transparent)); transparentPaint.setXfermode(new PorterDuffXfermode( PorterDuff.Mode.CLEAR)); temp.drawCircle(ViewHelper.getX(ball) + ball.getWidth() / 2, ViewHelper.getY(ball) + ball.getHeight() / 2, ball.getWidth() / 2, transparentPaint); canvas.drawBitmap(bitmap, 0, 0, new Paint()); } else { paint.setColor(Color.parseColor("#B0B0B0")); paint.setStrokeWidth(Utils.dpToPx(2, getResources())); canvas.drawLine(getHeight() / 2, getHeight() / 2, getWidth() - getHeight() / 2, getHeight() / 2, paint); paint.setColor(backgroundColor); float division = (ball.xFin - ball.xIni) / (max - min); int value = this.value - min; canvas.drawLine(getHeight() / 2, getHeight() / 2, value * division + getHeight() / 2, getHeight() / 2, paint); } if (press && !showNumberIndicator) { paint.setColor(backgroundColor); paint.setAntiAlias(true); canvas.drawCircle(ViewHelper.getX(ball) + ball.getWidth() / 2, getHeight() / 2, getHeight() / 3, paint); } invalidate(); }
198-252,具体绘制的操作
一开始就对我们之前的那个中间值做了个判断,为ture就是初始化位置。
然后就是构建了画笔然后判断是否在最小值,如果是的话在最小值哪里画个空心圆,如果不是就在value的实际值位置画个实心圆,然后value值到最小值的位置涂抹颜色。如果,有气泡并且被解除,再画个圆。
// Set atributtes of XML to View protected void setAttributes(AttributeSet attrs) { setBackgroundResource(R.drawable.background_transparent); // Set size of view setMinimumHeight(Utils.dpToPx(48, getResources())); setMinimumWidth(Utils.dpToPx(80, getResources())); // Set background Color // Color by resource int bacgroundColor = attrs.getAttributeResourceValue(ANDROIDXML, "background", -1); if (bacgroundColor != -1) { setBackgroundColor(getResources().getColor(bacgroundColor)); } else { // Color by hexadecimal int background = attrs.getAttributeIntValue(ANDROIDXML, "background", -1); if (background != -1) setBackgroundColor(background); } showNumberIndicator = attrs.getAttributeBooleanValue(MATERIALDESIGNXML, "showNumberIndicator", false); min = attrs.getAttributeIntValue(MATERIALDESIGNXML, "min", 0); max = attrs.getAttributeIntValue(MATERIALDESIGNXML, "max", 0); value = attrs.getAttributeIntValue(MATERIALDESIGNXML, "value", min); ball = new Ball(getContext()); RelativeLayout.LayoutParams params = new LayoutParams(Utils.dpToPx(20, getResources()), Utils.dpToPx(20, getResources())); params.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE); ball.setLayoutParams(params); addView(ball); // Set if slider content number indicator // TODO if (showNumberIndicator) { numberIndicator = new NumberIndicator(getContext()); } }
254-295行,之前构造函数调用的初始化的方法,一系列的获取XML初始化的参数,当然如果没有就设置默认了
private void placeBall() { ViewHelper.setX(ball, getHeight() / 2 - ball.getWidth() / 2); ball.xIni = ViewHelper.getX(ball); ball.xFin = getWidth() - getHeight() / 2 - ball.getWidth() / 2; ball.xCen = getWidth() / 2 - ball.getWidth() / 2; placedBall = true; }
297-203,具体初值位置的操作。
public interface OnValueChangedListener { public void onValueChanged(int value); }
306-308,我们回调的接口,返回的是我们的进度值
class Ball extends View { float xIni, xFin, xCen; public Ball(Context context) { super(context); setBackgroundResource(R.drawable.background_switch_ball_uncheck); } public void changeBackground() { if (value != min) { setBackgroundResource(R.drawable.background_checkbox); LayerDrawable layer = (LayerDrawable) getBackground(); GradientDrawable shape = (GradientDrawable) layer .findDrawableByLayerId(R.id.shape_bacground); shape.setColor(backgroundColor); } else { setBackgroundResource(R.drawable.background_switch_ball_uncheck); } } }
310-331,我们的进度小球的实现。
class Indicator extends RelativeLayout { boolean animate = true; // Final size after animation float finalSize = 0; // Final y position after animation float finalY = 0; boolean numberIndicatorResize = false; // Size of number indicator float size = 0; // Position of number indicator float x = 0; float y = 0; public Indicator(Context context) { super(context); setBackgroundColor(getResources().getColor( android.R.color.transparent)); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (numberIndicatorResize == false) { LayoutParams params = (LayoutParams) numberIndicator.numberIndicator .getLayoutParams(); params.height = (int) finalSize * 2; params.width = (int) finalSize * 2; numberIndicator.numberIndicator.setLayoutParams(params); } Paint paint = new Paint(); paint.setAntiAlias(true); paint.setColor(backgroundColor); if (animate) { if (y == 0) y = finalY + finalSize * 2; y -= Utils.dpToPx(6, getResources()); size += Utils.dpToPx(2, getResources()); } canvas.drawCircle( ViewHelper.getX(ball) + Utils.getRelativeLeft((View) ball.getParent()) + ball.getWidth() / 2, y, size, paint); if (animate && size >= finalSize) animate = false; if (animate == false) { ViewHelper .setX(numberIndicator.numberIndicator, (ViewHelper.getX(ball) + Utils.getRelativeLeft((View) ball .getParent()) + ball.getWidth() / 2) - size); ViewHelper.setY(numberIndicator.numberIndicator, y - size); numberIndicator.numberIndicator.setText(value + ""); } invalidate(); } }
335-396,气泡的实现。
先初始化位置,声明画笔,计算尺寸,画圆,传值给数值控件,不断的根据手势位置移动而移动。
class NumberIndicator extends Dialog { Indicator indicator; TextView numberIndicator; public NumberIndicator(Context context) { super(context, android.R.style.Theme_Translucent); } @Override public void dismiss() { super.dismiss(); indicator.y = 0; indicator.size = 0; indicator.animate = true; } @Override public void onBackPressed() { } @Override protected void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); setContentView(R.layout.number_indicator_spinner); setCanceledOnTouchOutside(false); RelativeLayout content = (RelativeLayout) this .findViewById(R.id.number_indicator_spinner_content); indicator = new Indicator(this.getContext()); content.addView(indicator); numberIndicator = new TextView(getContext()); numberIndicator.setTextColor(Color.WHITE); numberIndicator.setGravity(Gravity.CENTER); content.addView(numberIndicator); indicator.setLayoutParams(new RelativeLayout.LayoutParams( RelativeLayout.LayoutParams.FILL_PARENT, RelativeLayout.LayoutParams.FILL_PARENT)); } }
398-441行,单纯的实现了一个显示进度值的实现,并不依存于外层的气球,但是会根据气球的状态而改变,其实本身就是一个TextView。
- 进度条+进度条的小球 - 一开始停留在最小处,空心,自身位置到最大值的条颜色为灰色。 - 在操作有效的情况下小球UI变化,进度条颜色变化。- 气泡+气泡的进度值 - 一开始不显示气泡+进度值。 - 在操作有效的情况下,气泡与进度值呈现,并且跟随小球的位置移动。
源码地址:
Eclipse的小伙伴可能还需要: