由于项目中须要使用开关切换button,和声音滑动控件,可是原生Android5.0版本号以下的控件实在是太挫了。尽管网上已经有非常多关于这两个控件的blog。可是我实在是找不到像iPhone这样简洁样式的,只是咱们程序猿总不能这点问题就被难道撒···所以我决定仿照iphone的样式自己写这两个控件,。

效果图例如以下:



一、ToggleButton

先直接上代码。后面会一步步分析

  1. package com.zuck.definitionview.views;
  2. import android.animation.ValueAnimator;
  3. import android.annotation.SuppressLint;
  4. import android.content.Context;
  5. import android.content.res.Resources;
  6. import android.graphics.Canvas;
  7. import android.graphics.Color;
  8. import android.graphics.Paint;
  9. import android.graphics.Paint.Cap;
  10. import android.graphics.Paint.Style;
  11. import android.graphics.RectF;
  12. import android.os.Looper;
  13. import android.util.AttributeSet;
  14. import android.util.TypedValue;
  15. import android.view.View;
  16. import android.view.animation.OvershootInterpolator;
  17. /**
  18. * 仿iPhone ToggleButton
  19. *
  20. * 2015-7-13
  21. *
  22. * @author zuck
  23. *
  24. */
  25. public class ToggleButton extends View{
  26. private float radius;
  27. // 开启颜色
  28. private int onColor;
  29. // 关闭颜色
  30. private int offColor;
  31. // 灰色带颜色
  32. private int offBorderColor;
  33. // 手柄颜色
  34. private int spotColor;
  35. // 边框颜色
  36. private int borderColor;
  37. // 画笔
  38. private Paint paint ;
  39. // 开关状态
  40. private boolean toggleOn = false;
  41. // 边框大小 默觉得2px
  42. private int borderWidth = 2;
  43. // 垂直中心
  44. private float centerY;
  45. // button的開始和结束位置
  46. private float startX, endX;
  47. // 手柄X位置的最小和最大值
  48. private float spotMinX, spotMaxX;
  49. // 手柄大小
  50. private int spotSize ;
  51. /// 手柄X位置
  52. private float spotX;
  53. // 关闭时内部灰色带高度
  54. private float offLineWidth;
  55. private RectF rect = new RectF();
  56. //开关切换监听器
  57. private OnToggleChanged listener;
  58. //属性动画
  59. private ValueAnimator animation;
  60. public ToggleButton(Context context) {
  61. this(context, null);
  62. }
  63. public ToggleButton(Context context, AttributeSet attrs) {
  64. this(context, attrs, 0);
  65. }
  66. public ToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {
  67. super(context, attrs, defStyleAttr);
  68. init();
  69. }
  70. public void init() {
  71. initPaint();
  72. initColor();
  73. initAnimation();
  74. this.setOnClickListener(new OnClickListener() {
  75. @Override
  76. public void onClick(View arg0) {
  77. toggle();
  78. }
  79. });
  80. }
  81. private void initPaint() {
  82. //初始化画笔(抗抖动)
  83. paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  84. //绘制风格为填充
  85. paint.setStyle(Style.FILL);
  86. //笔触风格为圆角
  87. paint.setStrokeCap(Cap.ROUND);
  88. }
  89. private void initColor() {
  90. onColor = Color.parseColor("#4ebb7f");
  91. offColor = Color.parseColor("#dadbda");
  92. offBorderColor = Color.parseColor("#ffffff");
  93. spotColor = Color.parseColor("#ffffff");
  94. //由于開始为关闭状态。所以这里边框背景色初始化为关闭状态颜色
  95. borderColor = offColor;
  96. }
  97. @SuppressLint("NewApi")
  98. private void initAnimation() {
  99. animation = new ValueAnimator();
  100. animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  101. @Override
  102. public void onAnimationUpdate(ValueAnimator animation) {
  103. calculateToggleEffect(Double.parseDouble(animation.getAnimatedValue().toString()));
  104. }
  105. });
  106. //OvershootInterpolator : 结束时会超过给定数值,可是最后一定返回给定值
  107. animation.setInterpolator(new OvershootInterpolator(1.5f));
  108. animation.setDuration(500);
  109. }
  110. @Override
  111. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  112. final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
  113. final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  114. int widthSize = MeasureSpec.getSize(widthMeasureSpec);
  115. int heightSize = MeasureSpec.getSize(heightMeasureSpec);
  116. Resources r = Resources.getSystem();
  117. if(widthMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.AT_MOST){
  118. widthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 55, r.getDisplayMetrics());
  119. widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
  120. }
  121. if(heightMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.AT_MOST){
  122. heightSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, r.getDisplayMetrics());
  123. heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
  124. }
  125. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  126. }
  127. @Override
  128. protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  129. super.onLayout(changed, left, top, right, bottom);
  130. final int width = getWidth();
  131. final int height = getHeight();
  132. //由宽高计算圆角的半径
  133. radius = Math.min(width, height) * 0.5f;
  134. centerY = radius;
  135. startX = radius;
  136. endX = width - radius;
  137. spotMinX = startX + borderWidth;
  138. spotMaxX = endX - borderWidth;
  139. spotSize = height - 4 * borderWidth;
  140. spotX = toggleOn ?
  141. spotMaxX : spotMinX;
  142. offLineWidth = 0;
  143. }
  144. private int clamp(int value, int low, int high) {
  145. return Math.min(Math.max(value, low), high);
  146. }
  147. @Override
  148. protected void onDraw(Canvas canvas) {
  149. super.onDraw(canvas);
  150. //1.绘制最外层边框背景-圆角矩形
  151. //绘制圆角矩形背景大小为測量的宽高
  152. rect.set(0, 0, getWidth(), getHeight());
  153. paint.setColor(borderColor);
  154. canvas.drawRoundRect(rect, radius, radius, paint);
  155. if(offLineWidth > 0){
  156. //1.1绘制整个开关区域中除手柄外的白色区域带
  157. final float cy = offLineWidth * 0.5f;
  158. rect.set(spotX - cy, centerY - cy, endX + cy, centerY + cy);
  159. paint.setColor(offBorderColor);
  160. canvas.drawRoundRect(rect, cy, cy, paint);
  161. }
  162. //2.绘制圆形手柄边框区域
  163. rect.set(spotX - 1 - radius, centerY - radius, spotX + 1.1f + radius, centerY + radius);
  164. paint.setColor(borderColor);
  165. canvas.drawRoundRect(rect, radius, radius, paint);
  166. //3.绘制圆形手柄区域
  167. //圆形手柄的半径大小(不包括边框)
  168. final float spotR = spotSize * 0.5f;
  169. rect.set(spotX - spotR, centerY - spotR, spotX + spotR, centerY + spotR);
  170. paint.setColor(spotColor);
  171. canvas.drawRoundRect(rect, spotR, spotR, paint);
  172. }
  173. private double mapValueFromRangeToRange(double value, double fromLow,
  174. double fromHigh, double toLow, double toHigh) {
  175. double fromRangeSize = fromHigh - fromLow;
  176. double toRangeSize = toHigh - toLow;
  177. double valueScale = (value - fromLow) / fromRangeSize;
  178. return toLow + (valueScale * toRangeSize);
  179. }
  180. /**
  181. * @param value
  182. */
  183. private void calculateToggleEffect(final double value) {
  184. final float mapToggleX = (float) mapValueFromRangeToRange(value, 0, 1, spotMinX, spotMaxX);
  185. spotX = mapToggleX;
  186. float mapOffLineWidth = (float) mapValueFromRangeToRange(1 - value, 0, 1, 10, spotSize);
  187. offLineWidth = mapOffLineWidth;
  188. //开启时候的背景色
  189. final int fr = Color.red(onColor);
  190. final int fg = Color.green(onColor);
  191. final int fb = Color.blue(onColor);
  192. //关闭后的背景色
  193. final int tr = Color.red(offColor);
  194. final int tg = Color.green(offColor);
  195. final int tb = Color.blue(offColor);
  196. //border颜色渐变
  197. int sr = (int) mapValueFromRangeToRange(1 - value, 0, 1, fr, tr);
  198. int sg = (int) mapValueFromRangeToRange(1 - value, 0, 1, fg, tg);
  199. int sb = (int) mapValueFromRangeToRange(1 - value, 0, 1, fb, tb);
  200. sr = clamp(sr, 0, 255);
  201. sg = clamp(sg, 0, 255);
  202. sb = clamp(sb, 0, 255);
  203. borderColor = Color.rgb(sr, sg, sb);
  204. //重绘
  205. if (Looper.myLooper() == Looper.getMainLooper()) {
  206. invalidate();
  207. } else {
  208. postInvalidate();
  209. }
  210. }
  211. @SuppressLint("NewApi")
  212. private void takeToggleAction(boolean isOn){
  213. if(isOn) {
  214. animation.setFloatValues(0.f, 1.f);
  215. } else {
  216. animation.setFloatValues(1.f, 0.f);
  217. }
  218. animation.start();
  219. }
  220. /**
  221. * 切换开关
  222. */
  223. public void toggle() {
  224. toggleOn = !toggleOn;
  225. takeToggleAction(toggleOn);
  226. if(listener != null){//触发toggle事件
  227. listener.onToggle(toggleOn);
  228. }
  229. }
  230. /**
  231. * 切换为打开状态
  232. */
  233. public void toggleOn() {
  234. toggleOn = true;
  235. takeToggleAction(toggleOn);
  236. if(listener != null){//触发toggle事件
  237. listener.onToggle(toggleOn);
  238. }
  239. }
  240. /**
  241. * 切换为关闭状态
  242. */
  243. public void toggleOff() {
  244. toggleOn = false;
  245. takeToggleAction(toggleOn);
  246. if(listener != null){//触发toggle事件
  247. listener.onToggle(toggleOn);
  248. }
  249. }
  250. /**
  251. * 设置显示成打开样式。不会触发toggle事件
  252. */
  253. public void setToggleOn(){
  254. toggleOn = true;
  255. calculateToggleEffect(1.0f);
  256. }
  257. /**
  258. * 设置显示成关闭样式。不会触发toggle事件
  259. */
  260. public void setToggleOff() {
  261. toggleOn = false;
  262. calculateToggleEffect(0.0f);
  263. }
  264. /**
  265. * 状态切换监听器
  266. */
  267. public interface OnToggleChanged{
  268. public void onToggle(boolean on);
  269. }
  270. public void setOnToggleChanged(OnToggleChanged onToggleChanged) {
  271. listener = onToggleChanged;
  272. }
  273. }

这个控件总体看来,还是比較简单的,開始我们仍然是初始化一些须要用到的画笔、颜色、以及属性动画。

这里画笔和颜色就不说了,跟切白菜一样。

简单说说动画:为什么要用到属性动画,大家能够细致看看上面的效果图,当点击控件的时候手柄会从最左端移动到最右端,完毕一个打开的效果。

或者从最右端移动到最左端。完毕一个关闭的效果。

而且在手柄位移到最左端或者最右端的时候还会有一个回弹的效果。

而这些所有都是依据属性动画

  1. private void initAnimation() {
  2. animation = new ValueAnimator();
  3. animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  4. @Override
  5. public void onAnimationUpdate(ValueAnimator animation) {
  6. calculateToggleEffect(Double.parseDouble(animation.getAnimatedValue().toString()));
  7. }
  8. });
  9. //OvershootInterpolator : 结束时会超过给定数值,可是最后一定返回给定值
  10. animation.setInterpolator(new OvershootInterpolator(1.5f));
  11. animation.setDuration(500);
  12. }

这里属性动画大家应该都非常熟悉了,假设还有不懂他的基本使用方法的。能够去这个链接去学习下关于属性动画的使用:Android属性动画全然解析(上)。初识属性动画的基本使用方法。如今我们仅仅看OvershootInterpolator这个API。

从上图能够看到OvershootInterpolator继承BaseInterpolator。那么Interpolator是什么?引用郭霖blog中的一句话:“Interpolator这个东西非常难进行翻译。直译过来的话是补间器的意思,它的主要作用是能够控制动画的变化速率”。那么按我的理解就是控制view动画效果的节奏。

从这个继承关系图我们能够看到,Android为我们提供了非常多Interpolator,有兴趣的童鞋能够挨个试一试。

看看他们的效果。这里带大家看看OvershootInterpolator:

/**

* An interpolator where the change flings forward and overshoots the last value

* then comes back.

*/

这是API中关于OvershootInterpolator的介绍。我的英文稀烂,大致意思是:值在变化时,快结束的时候值会超过给定的最大值,然后再返回给定的最大值。也就是说,当我们有例如以下设置的时候

animation.setFloatValues(0.f, 1.f);

onAnimationUpdate()回调方法最后会按 0.9f->1.0f->1.1f->1.2f->….1.0f。这种方式返回数值。

通过这种数值去控制手柄的执行轨迹从而达到一个回弹的效果。

另一点:在我创建OvershootInterpolator的时候传入一个tension參数,tension表示张力。默觉得2.0f。tension越大,那么最后超过给定最大边界值后继续增大数值也就越大,反之越小。

ok,以上就是关于初始化操作。

以下也就是按流程一步一步看了:onMeasure() -> onLayout() -> onDraw();

这些都是老生长谈了:

onMeasure

在onMeasure方法中去測量控件的大小这块逻辑非常easy。

这里仅仅处理了AT_MOST和UNSPECIFIED两种模式。当我们把宽度或者高度设置为wrap_content或者设置了weight,那么相应我们让ToggleButton的长宽也为默认值。

其他方式。測量方法中不做干涉。

用户设置的宽高是多少,那么控件的宽高就是多少。

onLayout

在onLayout方法中去计算了该控件在绘制的时候须要用到的几个參数,比如包括边框的手柄半径、手柄的開始位置、结束位置、手柄大小等等…看看也没啥好说的,都是一系列计算。

onDraw

最后看onDraw方法。控件的绘制主要分为四步,上述代码中有明确的凝视:

1.绘制控件最外层边框,这里注意这个边框是圆矩形,他们的rx 和ry的值也就是在onLayout中计算的radius值

2. 绘制圆角矩形内除去手柄区域以外的区域,该区域的大小是由offLineWidth去控制的,而关于offLineWidth是怎么算出来的,等下会跟大家讲明确的

3. 绘制手柄的边框区域。该区域的位置是由手柄的位置spotX去控制的。而spotX的计算,等下和offLineWidth的计算一并跟大家说明确(ps:为什么要一起,由于计算他们两个的值都是用的同一种算法)

4. 绘制手柄区域和绘制手柄边框区域差点儿是一致的,唯一不同的是绘制手柄区域的半径大小比绘制手柄边框区域的大小要小2个单位值。由于他们绘制的位置是一样的,而大小不一样,这样也就让整个手柄有了边框的效果同一时候看起来更加有扁平感了。

同一时候该手柄区域的位置也是由spotX去控制的

到此从init() 到 onMeasure() -> onLayout() -> onDraw()这些方法逻辑思路在大家心中应该是比較清晰明了的。

calculateToggleEffect -> mapValueFromRangeToRange

以下就来说说该控件中最重要的算法问题了,事实上这个算法也是相当的简单

当我们点击控件。内部是如何驱动手柄左右位移的呢,圆矩形内颜色是如何转变的呢?这一切要归功于calculateToggleEffect()这种方法,上面讲述为什么要使用属性动画的时候。我曾贴过一小段代码,在onAnimationUpdate()回调方法中我们调用了calculateToggleEffect()方法,依据属性动画回传给我们的值去计算控制位移、颜色变化等效果的三个參数值

  1. private void calculateToggleEffect(final double value) {
  2. final float mapToggleX = (float) mapValueFromRangeToRange(value, 0, 1, spotMinX, spotMaxX);
  3. spotX = mapToggleX;
  4. float mapOffLineWidth = (float) mapValueFromRangeToRange(1 - value, 0, 1, 10, spotSize);
  5. offLineWidth = mapOffLineWidth;
  6. //开启时候的背景色
  7. final int fr = Color.red(onColor);
  8. final int fg = Color.green(onColor);
  9. final int fb = Color.blue(onColor);
  10. //关闭后的背景色
  11. final int tr = Color.red(offColor);
  12. final int tg = Color.green(offColor);
  13. final int tb = Color.blue(offColor);
  14. //border颜色渐变
  15. int sr = (int) mapValueFromRangeToRange(1 - value, 0, 1, fr, tr);
  16. int sg = (int) mapValueFromRangeToRange(1 - value, 0, 1, fg, tg);
  17. int sb = (int) mapValueFromRangeToRange(1 - value, 0, 1, fb, tb);
  18. sr = clamp(sr, 0, 255);
  19. sg = clamp(sg, 0, 255);
  20. sb = clamp(sb, 0, 255);
  21. borderColor = Color.rgb(sr, sg, sb);
  22. //重绘
  23. if (Looper.myLooper() == Looper.getMainLooper()) {
  24. invalidate();
  25. } else {
  26. postInvalidate();
  27. }
  28. }

这段方法中。大家能够看到我们分别去计算了spotX、offLineWidth、borderColor这三个实例变量的值

而计算这三个实例变量的值主要是由mapValueFromRangeToRange()方法来完毕,那么mapValueFromRangeToRange()方法是个什么鬼?

  1. private double mapValueFromRangeToRange(double value, double fromLow,
  2. double fromHigh, double toLow, double toHigh) {
  3. double fromRangeSize = fromHigh - fromLow;
  4. double toRangeSize = toHigh - toLow;
  5. double valueScale = (value - fromLow) / fromRangeSize;
  6. return toLow + (valueScale * toRangeSize);
  7. }

一眼望去,各种加减乘除,不喜欢算法的童鞋头都大了。事实上这些运算是相当的简单,基本的思想就是:以一个范围内的值,去映射另外一个给定的范围。通俗讲就是:给你一个范围[0,1]rangeA。在给你一个用于映射的范围[50,100]rangeB。那么如今给你一个在rangeA中的值 0.3。你去在rangeB中计算出相应比例的值。

这个够简单吧。

result = 50 + ((0.3-0) / (1- 0))*(100 - 50) ;

上面的代码中仅仅是将每一步拆分开来,大家能够合在一起看看是不是与上面的式子相等,ok,这样我们就计算出spotX和offLineWidth的值,所以spotX的值也就是会在spotMinX 到 spotMaxX的范围中递增或者递减(由于属性动画回传过来的值也是递增或者递减)。同理offLineWidth和背景色的计算也是一样。我们搞懂mapValueFromRangeToRange后calculateToggleEffect()方法中的逻辑就非常非常easy了。我就不累赘了~那么到此为止ToggleButton的实现已经带大家所有分析完毕。

二、VoiceSeekBar

直接上代码

  1. package com.zuck.definitionview.views;
  2. import android.annotation.SuppressLint;
  3. import android.content.Context;
  4. import android.content.res.Resources;
  5. import android.graphics.Bitmap;
  6. import android.graphics.BitmapFactory;
  7. import android.graphics.Canvas;
  8. import android.graphics.Color;
  9. import android.graphics.Paint;
  10. import android.graphics.Rect;
  11. import android.graphics.Region;
  12. import android.util.AttributeSet;
  13. import android.util.TypedValue;
  14. import android.view.MotionEvent;
  15. import android.view.View;
  16. import com.zuck.definitionview.R;
  17. /**
  18. * 仿iPhone VoiceSeekBar
  19. *
  20. * 2015-7-16
  21. *
  22. * @author zuck
  23. *
  24. */
  25. @SuppressLint("NewApi")
  26. public class VoiceSeekBar extends View {
  27. /**
  28. * 手柄半径比率
  29. */
  30. private static final float SPOTRADIUSRATE = 13.f/40.f;
  31. /**
  32. * 横条默认高度
  33. */
  34. private static final int BAR_HEIGHT = 2;
  35. /**
  36. * 默认间隙宽度
  37. */
  38. private static final int SPACE = 30;
  39. /**
  40. * 最高级别[级别从0開始,而且级别的段数则为:MAXLEVEL + 1]
  41. */
  42. private static final int MAXLEVEL = 15;
  43. /**
  44. * 条形棒宽度
  45. */
  46. private int barWidth;
  47. /**
  48. * 手柄的区域
  49. */
  50. private Region spotRegion;
  51. /**
  52. * 绘制条形棒的矩形
  53. */
  54. private Rect rect;
  55. // 左边条形棒的颜色,右边条形棒的颜色,手柄的颜色
  56. private int leftBarColor, rightBarColor, spotColor;
  57. // 手指按下的时候x坐标
  58. private float pressX;
  59. // 条形棒矩形的左上角定点
  60. private int barLeft ,barTop;
  61. // 左側符号的X坐标,Y坐标
  62. private int signX,signY;
  63. // 手柄半径
  64. private float radius;
  65. private Paint paint, spotPaint;
  66. private Bitmap signLeft, signRigh;
  67. private int currentLevel;
  68. private OnSeekBarChangeListener onSeekBarChangeListener;
  69. public interface OnSeekBarChangeListener {
  70. void onProgressChanged(VoiceSeekBar seekBar, int progress);
  71. void onStartTrackingTouch(VoiceSeekBar seekBar);
  72. void onStopTrackingTouch(VoiceSeekBar seekBar);
  73. }
  74. public VoiceSeekBar(Context context) {
  75. this(context, null);
  76. }
  77. public VoiceSeekBar(Context context, AttributeSet attrs) {
  78. this(context, attrs, 0);
  79. }
  80. public VoiceSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
  81. super(context, attrs, defStyleAttr);
  82. // 关闭硬件加速
  83. setLayerType(LAYER_TYPE_SOFTWARE, null);
  84. init();
  85. }
  86. private void init() {
  87. //初始化当前级别
  88. currentLevel = 0;
  89. // 初始化载入左边与右边的两张符号图标
  90. signLeft = BitmapFactory.decodeResource(getResources(), R.drawable.minus);
  91. signRigh = BitmapFactory.decodeResource(getResources(), R.drawable.plus);
  92. // 手柄左边/右边的条形棒颜色
  93. leftBarColor = Color.parseColor("#4ebb7f");
  94. rightBarColor = Color.parseColor("#dadbda");
  95. // 手柄颜色
  96. spotColor = rightBarColor;
  97. paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
  98. paint.setStyle(Paint.Style.FILL);
  99. paint.setStrokeCap(Paint.Cap.ROUND);
  100. spotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  101. spotPaint.setColor(spotColor);
  102. spotPaint.setStyle(Paint.Style.FILL);
  103. // 为手柄加入阴影效果
  104. spotPaint.setShadowLayer(10, 0, 1, Color.DKGRAY);
  105. // 手柄所在的区域
  106. spotRegion = new Region();
  107. rect = new Rect();
  108. }
  109. @Override
  110. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  111. Resources r = Resources.getSystem();
  112. //測量高度,高度默觉得35
  113. int heigthSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 35, r.getDisplayMetrics());
  114. heightMeasureSpec = MeasureSpec.makeMeasureSpec(heigthSize, MeasureSpec.EXACTLY);
  115. //測量宽度
  116. int widthSize = MeasureSpec.getSize(widthMeasureSpec);
  117. int minWidth = (int) (signLeft.getWidth() + signRigh.getWidth() + SPACE * 4 + heigthSize * SPOTRADIUSRATE);
  118. widthSize = Math.max(minWidth, widthSize);
  119. widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
  120. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  121. }
  122. @Override
  123. protected void onLayout(boolean changed, int left, int top, int right,
  124. int bottom) {
  125. super.onLayout(changed, left, top, right, bottom);
  126. final int width = getWidth();
  127. final int height = getHeight();
  128. //手柄的半径
  129. radius = height * SPOTRADIUSRATE;
  130. //条形棒宽度
  131. barWidth = (int) (width - (signLeft.getWidth() + signRigh.getWidth() + SPACE * 4 + radius));
  132. //计算条形棒開始绘制的左上角顶点位置
  133. barLeft = (int) Math.ceil((width - barWidth) / 2.f);
  134. barTop = (int) Math.ceil((height - BAR_HEIGHT) / 2.f);
  135. //计算左边符号绘制的位置
  136. signX = (int) (barLeft - signLeft.getWidth() - SPACE);
  137. signY = (height - signLeft.getHeight()) / 2;
  138. calcCurrentPressX();
  139. }
  140. @Override
  141. protected void onDraw(Canvas canvas) {
  142. super.onDraw(canvas);
  143. //0.绘制左右两边的符号
  144. canvas.drawBitmap(signLeft, signX, signY, paint);
  145. canvas.drawBitmap(signRigh, barWidth + barLeft + SPACE, signY, paint);
  146. //1.绘制圆形手柄
  147. //计算手柄圆点坐标x值
  148. float cx = 0;
  149. float cy = barTop + BAR_HEIGHT / 2;
  150. if(pressX <= radius + barLeft){//手指触摸的范围在:从最左側到横条開始的一个手柄半径距离范围
  151. cx = barLeft + radius;
  152. }else if (pressX > barLeft + radius && pressX <= barWidth + barLeft - radius){//手指触摸点在 横条最左側開始加上一个手柄半径距离到横条最右側减去一个手柄半径距离范围之内
  153. cx = pressX;
  154. } else {//手指触摸点超过横条最右側减去一个手柄半径距离的范围
  155. cx = barLeft + barWidth - radius;
  156. }
  157. canvas.drawCircle(cx, cy, radius, spotPaint);
  158. //2.绘制左边的条形棒
  159. int leftBarRight = (int) (cx - radius);
  160. int barBottom = barTop + BAR_HEIGHT;
  161. rect.set(barLeft, barTop, leftBarRight, barBottom);
  162. paint.setColor(leftBarColor);
  163. canvas.drawRect(rect, paint);
  164. //3.绘制右边的条形棒
  165. int rightBarLeft = (int) (leftBarRight + radius * 2);
  166. rect.set(rightBarLeft, barTop, barWidth + barLeft, barBottom);
  167. paint.setColor(rightBarColor);
  168. canvas.drawRect(rect, paint);
  169. //4.记录当前手柄所在的区域(区域的范围扩大一个半径范围)
  170. int regionLeft = (int)(cx - 2 * radius);
  171. spotRegion.set(regionLeft, (int) -radius, (int)(radius * 4) + regionLeft, (int)(radius * 4));
  172. }
  173. @Override
  174. public boolean dispatchTouchEvent(MotionEvent ev) {
  175. getParent().requestDisallowInterceptTouchEvent(true);
  176. return super.dispatchTouchEvent(ev);
  177. }
  178. @SuppressLint("ClickableViewAccessibility")
  179. @Override
  180. public boolean onTouchEvent(MotionEvent event) {
  181. int action = event.getAction();
  182. switch (action) {
  183. case MotionEvent.ACTION_DOWN:
  184. pressX = event.getX();
  185. float pressY = event.getY();
  186. if(!spotRegion.contains((int)pressX, (int)pressY)) {
  187. return false;
  188. }
  189. if(onSeekBarChangeListener != null)
  190. onSeekBarChangeListener.onStartTrackingTouch(this);
  191. break;
  192. case MotionEvent.ACTION_MOVE:
  193. pressX = event.getX();
  194. int level = CalcCurrentLevel();
  195. if(onSeekBarChangeListener != null && currentLevel != level) {
  196. currentLevel = level;
  197. onSeekBarChangeListener.onProgressChanged(this, level);
  198. }
  199. invalidate();
  200. break;
  201. case MotionEvent.ACTION_UP:
  202. if(onSeekBarChangeListener != null)
  203. onSeekBarChangeListener.onStopTrackingTouch(this);
  204. break;
  205. }
  206. return true;
  207. }
  208. /**
  209. * 设置当前级别
  210. * @param currentLevel
  211. */
  212. public void setCurrentLevel(int currentLevel) {
  213. if(currentLevel < 0){
  214. this.currentLevel = 0;
  215. } else {
  216. this.currentLevel = currentLevel >= MAXLEVEL ?
  217. MAXLEVEL : currentLevel;
  218. }
  219. }
  220. /**
  221. * 获取当前级别
  222. * @return
  223. */
  224. public int getCurrentLevel() {
  225. return currentLevel;
  226. }
  227. /**
  228. * 依据当前级别计算当前pressX值(pressX用来计算手柄所在位置)
  229. */
  230. private void calcCurrentPressX() {
  231. pressX = barWidth * ((float)currentLevel / MAXLEVEL) + barLeft;
  232. }
  233. /**
  234. * 计算当前的级别
  235. * @return
  236. */
  237. private int CalcCurrentLevel() {
  238. int level ;
  239. if(pressX <= radius + barLeft){//手指触摸的范围在:从最左側到横条開始的一个手柄半径距离范围
  240. level = 0;
  241. }else if (pressX > barLeft + radius && pressX <= barWidth + barLeft - radius){//手指触摸点在 横条最左側開始加上一个手柄半径距离到横条最右側减去一个手柄半径距离范围之内
  242. level = (int) (((pressX - barLeft) * MAXLEVEL) / barWidth);
  243. } else {//手指触摸点超过横条最右側减去一个手柄半径距离的范围
  244. level = MAXLEVEL;
  245. }
  246. return level;
  247. }
  248. public void setOnSeekBarChangeListener(OnSeekBarChangeListener onSeekBarChangeListener) {
  249. this.onSeekBarChangeListener = onSeekBarChangeListener;
  250. }
  251. }

该控件实现逻辑思路与ToggleButton差点儿一致。

我也就不打算讲述了。记录在这里,供以后方便查看。以上代码。能够直接copy粘贴到项目中使用。

改下包名就可以。

代码传送门

自己定义控件-仿iphone之ToggleButton&amp;VoiceSeekBar的更多相关文章

  1. 【Android】自己定义控件——仿天猫Indicator

    今天来说说类似天猫的Banner中的小圆点是怎么做的(图中绿圈部分) 在学习自己定义控件之前,我用的是很二的方法,直接在布局中放入多个ImageView,然后代码中依据Pager切换来改变图片.这样的 ...

  2. 自己定义控件:onDraw 方法实现仿 iOS 的开关效果

    概述 本文主要解说怎样在 Android 下实现高仿 iOS 的开关按钮,并不是是在 Android 自带的 ToggleButton 上改动,而是使用 API 提供的 onDraw.onMeasur ...

  3. Android应用之——自己定义控件ToggleButton

    我们经常会看到非常多优秀的app上面都有一些非常美丽的控件,用户体验非常好.比方togglebutton就是一个非常好的样例,IOS系统以下那个精致的togglebutton现在在android以下也 ...

  4. Android自定义控件1--自定义控件介绍

    Android控件基本介绍 Android本身提供了很多控件比如我们常用的有文本控件TextView和EditText:按钮控件Button和ImageButton状态开关按钮ToggleButton ...

  5. Android自己定义控件系列二:自己定义开关button(一)

    这一次我们将会实现一个完整纯粹的自己定义控件,而不是像之前的组合控件一样.拿系统的控件来实现.计划分为三部分:自己定义控件的基本部分,自己定义控件的触摸事件的处理和自己定义控件的自己定义属性: 以下就 ...

  6. Android 实现形态各异的双向側滑菜单 自己定义控件来袭

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/39670935.本文出自:[张鸿洋的博客] 1.概述 关于自己定义控件側滑已经写了 ...

  7. Android自己定义控件之应用程序首页轮播图

    如今基本上大多数的Android应用程序的首页都有轮播图.就是像下图这种(此图为转载的一篇博文中的图.拿来直接用了): 像这种组件我相信大多数的应用程序都会使用到,本文就是自己定义一个这种组件,能够动 ...

  8. Qt 界面使用自己定义控件 &quot;提升为&quot;

    1.效果图 我做了一个很easy的样例,一个能够显示颜色的QLabel,边上有个button,点击,跳出颜色选取的Dialog,然后选择一个颜色.这个QLabel会变成什么颜色. 2.ColorLab ...

  9. Android自己定义控件(状态提示图表)

    [工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处.尊重分享成果] 1 背景 前面分析那么多系统源代码了.也该暂停下来歇息一下,趁昨晚闲着看见一个有意思的需求就操 ...

随机推荐

  1. 跨服务器进行SQL Server数据库的数据处理

    exec sp_addlinkedserver 'ITDB', ' ', 'SQLOLEDB', '服务器IP' exec sp_addlinkedsrvlogin 'ITDB', 'false ', ...

  2. python框架之Flask基础篇(二)-------- 数据库的操作

    1.flask连接数据库的四步: 倒入第三方数据库扩展包:from flask_sqlalchemy import SQLAlchemy 配置config属性,连接数据库: app.config[&q ...

  3. Android中ViewPager动态创建的ImageView铺满屏幕

    ImageView imageView=new ImageView(context); imageView.setScaleType(ScaleType.FIT_XY);//铺满屏幕

  4. day02-操作系统、编程语言分类及python安装

    目录 操作系统 编程语言分类 安装python解释器 操作系统 操作系统有什么用 操作系统能接受外部指令转化成0和1,并把一些对硬件的复杂操作简化成一个个简单的接口,作为中间人连接硬件和软件 计算机三 ...

  5. 字符串str

    字符串: #字符串的索引从0开始的,如果倒数最后一位是-1,索引的位置是唯一的.var1 = var[0:2] #从第一个字符到第2个字符var2 = var[:] #从第一个到最后var3 = va ...

  6. Python 之数据类型

    # Numbers(数字) # int(有符号整型) # long(长整型[也可以代表八进制和十六进制]) # float(浮点型) # complex(复数) # String(字符串) # Lis ...

  7. Uncaught TypeError: str.replace is not a function

    在做审核页面时,点击审核通过按钮不执行 后来F12控制台查看发现有报错 是因为flisnullandxyzero未执行 然后找出这个方法,此方法为公共方法,将这个方法复制出来 然后使用console. ...

  8. 面试bb

    1.面试者进行自我介绍 2.学校/学历/大学生活及课程的心得总结(人品,性格评估) 3.技能介绍(是否对应聘工作有帮助,评估学习能力):编程/操作系统/测试工具/测试方法/脚本 4.离职原因/项目经验 ...

  9. Linux:在安装虚拟机时如何选择网络类型?

    如图所示工作站提供了5种网络模式,我们主要用的就是上面3种:桥接模式,NAT,仅主机 1,仅主机模式 仅主机模式:虚拟机用过vmnet1网卡与宿主机通信,但是不能与物理局域网内其他主机通信,可利用虚拟 ...

  10. render: h => h(App) $mount 什么意思

    初始一个vue.js项目时,常常发现main.js里有如下代码: new Vue({ render: h => h(App) }).$mount('#app') 这什么意思?那我自己做项目怎么改 ...