不少人应该见过小米手机系统音量控制UI,一个圆形带动画效果的音量加减UI,效果很好看。它是怎么实现的呢?这篇博客来揭开它的神秘面纱。先上效果图

相信很多人都知道Android自定义控件的三种方式,Android自定义控件View(一)自绘控件Android自定义控件View(二)继承控件,还有就是这一节即将学习到的组合控件。我们通过实现圆形音量UI来讲解组合控件的定义和使用。

组合控件

所谓组合控件就是有多个已有的控件组合而成一个复杂的控件。比如上图的音量控件就是一个完美的组合控件。我们来分析一下,音量组合控件是由哪些子控件组合而成的?中间有一个ImageView和一个TextView实现,背景是有一个半透明圆形和白色圆环叠加构成的(我们暂且叫音量控件VolumeView)。因此音量组合控件(VolumeViewLayout)就是有3个子控件组合而成:VolumeView,ImageView,TextView。代码实现如下:

package com.xjp.customvolumeview;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView; /**
* Description:组合布局实现类似小米手机音量UI
* User: xjp
* Date: 2015/5/29
* Time: 18:06
*/ public class VolumeViewLayout extends FrameLayout { private VolumeView volumeView;
private ImageView icon;
private TextView title; public VolumeViewLayout(Context context) {
this(context, null);
} public VolumeViewLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
} public VolumeViewLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.volume_view_layout, this);
volumeView = (VolumeView) view.findViewById(R.id.volume);
icon = (ImageView) view.findViewById(R.id.img_volume);
title = (TextView) view.findViewById(R.id.text);
} /**
* 设置标题
*
* @param msg
*/
public void setTitle(String msg) {
title.setText(msg);
} /**
* 设置图片
*
* @param resId
*/
public void setIcon(int resId) {
icon.setImageResource(resId);
} /**
* 加音量
*/
public void volumeUp() {
volumeView.volumeUp();
} /**
* 减音量
*/
public void volumeDown() {
volumeView.volumeDown();
}
}

VolumeViewLayout类中的构造方法通过LayoutInflater加载XML布局来构成一个组合控件,因此可以看出,如果你需要修改组合控件显示效果的话,你可以修改LayoutInflater加载XML布局就ok了。VolumeViewLayout是继承FrameLayout,你可以继承任何ViweGroup的父容器View。

VolumeViewLayout暴露出4个方法,分别是设置中间的Image图片,设置中间的文字,和音量加减操作方法。布局代码中这么使用:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/back"
tools:context=".MainActivity"> <Button
android:id="@+id/buttonAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="80dp"
android:layout_marginTop="55dp"
android:text="音量+" /> <Button
android:id="@+id/buttonDelete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="55dp"
android:layout_toRightOf="@+id/buttonAdd"
android:text="音量-" /> <com.xjp.customvolumeview.VolumeViewLayout
android:id="@+id/volumeView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"></com.xjp.customvolumeview.VolumeViewLayout> </RelativeLayout>

代码调用中这么使用:

package com.xjp.customvolumeview;

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.widget.Button; public class MainActivity extends ActionBarActivity implements View.OnClickListener { private Button buttonAdd;
private Button buttonDelete;
private VolumeViewLayout volumeView; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); buttonAdd = (Button) findViewById(R.id.buttonAdd);
buttonAdd.setOnClickListener(this);
buttonDelete = (Button) findViewById(R.id.buttonDelete);
buttonDelete.setOnClickListener(this);
volumeView = (VolumeViewLayout) findViewById(R.id.volumeView);
} @Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.buttonAdd:
volumeView.volumeUp();
break;
case R.id.buttonDelete:
volumeView.volumeDown();
break;
}
}
}

如需要改变音量UI中的图片和文字,可以分别调用如下方法即可

volumeView.setIcon(R.drawable.icon);
volumeView.setTitle("音乐音量");

以上就是真个组合控件实现的过程。我们来梳理一下流程:

  1. 在XML布局文件中定义好一个组合布局。
  2. 继承ViewGroup类自定义组合控件。
  3. 在自定义组合控件的构造方法中通过LayoutInflater加载组合布局。
  4. 在xml布局中使用组合控件。

自绘圆形带动画效果音量控件 VolumeView

整体上实现了组合控件。我们来看看音量控件VolumeView怎么实现的?其实VolumeView根据 Android自定义控件View(一)自绘控件来实现的。我们来回顾一下自绘控件的流程

  1. 自定义控件View的属性。
  2. 在View的构造方法中获得属性值。
  3. 重写onMeasure方法
  4. 重写onDraw方法
  5. 布局中使用自定义控件

自定义控件View的属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="radius" format="dimension"></attr>
<attr name="backgroundColor" format="color"></attr>
<attr name="primaryVolumeColor" format="color"></attr>
<attr name="volumeColor" format="color"></attr>
<attr name="borderWidth" format="dimension"></attr>
<attr name="maxVolume" format="integer"></attr> <declare-styleable name="VolumeView">
<attr name="radius"></attr>
<attr name="backgroundColor"></attr>
<attr name="primaryVolumeColor"></attr>
<attr name="volumeColor"></attr>
<attr name="borderWidth"></attr>
<attr name="maxVolume"></attr>
</declare-styleable> </resources>

在View的构造方法中获得属性值

 /**
* 获取自定义View的属性值
*
* @param context
* @param attrs
*/
private void setAttrs(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VolumeView);
if (null != a) {
radius = a.getDimensionPixelSize(R.styleable.VolumeView_radius, defaultRadius);
backgroundColor = a.getColor(R.styleable.VolumeView_backgroundColor, defaultBackgroundColor);
volumeColor = a.getColor(R.styleable.VolumeView_volumeColor, defaultVolumeColor);
primaryVolumeColor = a.getColor(R.styleable.VolumeView_primaryVolumeColor, defaultPrimaryVolumeColor);
borderWidth = a.getDimensionPixelSize(R.styleable.VolumeView_borderWidth, defaultBorderWidth);
maxVolume = a.getInt(R.styleable.VolumeView_maxVolume, 15);
a.recycle();
} }

重写onMeasure方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/**固定自定义圆形UI的大小,不管属性设置大小多少都不影响圆形UI大小,
唯一影响圆形UI的大小只有圆的半径,言外之意:
只能通过半径来控制圆形UI大小,所以属性里半径为必设值。*/
setMeasuredDimension(radius * 2, radius * 2);
}

重写onDraw方法

 @Override
protected void onDraw(Canvas canvas) {
//绘制背景
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(backgroundColor);
radius = getWidth() / 2;
canvas.drawCircle(radius, radius, radius, paint); //绘制音量线圈背景
paint.setAntiAlias(true);
paint.setColor(primaryVolumeColor);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(borderWidth);
canvas.drawCircle(radius, radius, radius - borderWidth, paint); //绘制音量线圈
paint.setAntiAlias(true);
paint.setColor(volumeColor);
rectF = new RectF(borderWidth, borderWidth, getWidth() - borderWidth, getHeight() - borderWidth);
if (isVolumeUp) {//音量增加时
canvas.drawArc(rectF, -90, angle * (volumeNum > 0 ? volumeNum - 1 : 0) + unitAngle * fraction, false, paint);
} else {//音量减小时
canvas.drawArc(rectF, -90, angle * (volumeNum + 1) - unitAngle * fraction, false, paint);
}
}

XML布局中使用控件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"> <com.xjp.customvolumeview.VolumeView
android:id="@+id/volume"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
custom:borderWidth="5dp"
custom:maxVolume="10"
custom:radius="65dp" /> <LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical"> <ImageView
android:id="@+id/img_volume"
android:layout_width="58dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/icon" /> <TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/img_volume"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:text="铃声音量"
android:textColor="@android:color/white"
android:textSize="13sp" />
</LinearLayout> </RelativeLayout>

完整代码

package com.xjp.customvolumeview;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View; /**
* Description:圆形音量控件
* User: xjp
* Date: 2015/5/29
* Time: 14:08
*/ public class VolumeView extends View { private static final String TAG = "VolumeView";
private static final boolean DEBUG = false; //圆形半径
private int radius = 0;
//音量边框底色
private int primaryVolumeColor = 0;
//音量边框颜色
private int volumeColor = 0;
//圆形音量背景颜色
private int backgroundColor = 0;
//音量边框宽度
private int borderWidth = 0;
//动画百分比
private int fraction = 0; //以下都是默认值
private int defaultRadius = 60;
private int defaultBorderWidth = 8;
private int defaultBackgroundColor = 0x60000000;
private int defaultVolumeColor = Color.WHITE;
private int defaultPrimaryVolumeColor = 0x80000000; private RectF rectF = null; private Paint paint = null; //最大音量次数
private int maxVolume = 15;
//音量每增加一次,对于的角度
private float angle = 0;
//动画的最大值
private int maxAnimationValue = 10;
//音量每增加一次的单位角度
private float unitAngle = 0;
//当前音量的次数
private int volumeNum = 0;
//是否是加音量
private boolean isVolumeUp = true; public VolumeView(Context context) {
this(context, null);
} public VolumeView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
} public VolumeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setAttrs(context, attrs);
initPaint();
} /**
* 初始化画笔
*/
private void initPaint() {
angle = 360f / maxVolume;
unitAngle = angle / maxAnimationValue;
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setAntiAlias(true);
paint.setDither(true);
} /**
* 获取自定义View的属性值
*
* @param context
* @param attrs
*/
private void setAttrs(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.VolumeView);
if (null != a) {
radius = a.getDimensionPixelSize(R.styleable.VolumeView_radius, defaultRadius);
backgroundColor = a.getColor(R.styleable.VolumeView_backgroundColor, defaultBackgroundColor);
volumeColor = a.getColor(R.styleable.VolumeView_volumeColor, defaultVolumeColor);
primaryVolumeColor = a.getColor(R.styleable.VolumeView_primaryVolumeColor, defaultPrimaryVolumeColor);
borderWidth = a.getDimensionPixelSize(R.styleable.VolumeView_borderWidth, defaultBorderWidth);
maxVolume = a.getInt(R.styleable.VolumeView_maxVolume, 15);
a.recycle();
} } /**
* 设置圆形半径
*
* @param radius
*/
public void setRadius(int radius) {
this.radius = radius;
} /**
* 设置音量边框的宽度
*
* @param borderWidth
*/
public void setBorderWidth(int borderWidth) {
this.borderWidth = borderWidth;
} /**
* 设置最大音量值
*
* @param maxVolume
*/
public void setMaxVolume(int maxVolume) {
this.maxVolume = maxVolume;
} /**
* 设置音量边框底色
*
* @param color
*/
public void setPrimaryVolumeColor(int color) {
primaryVolumeColor = color;
} /**
* 设置音量边框颜色
*
* @param color
*/
public void setVolumeColor(int color) {
volumeColor = color;
} /**
* 设置圆形音量的背景颜色
*
* @param color
*/
public void setBackgroundColor(int color) {
backgroundColor = color;
} @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/**固定自定义圆形UI的大小,不管属性设置大小多少都不影响圆形UI大小,
唯一影响圆形UI的大小只有圆的半径,言外之意:
只能通过半径来控制圆形UI大小,所以属性里半径为必设值。*/
setMeasuredDimension(radius * 2, radius * 2);
} @Override
protected void onDraw(Canvas canvas) {
//绘制背景
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setColor(backgroundColor);
radius = getWidth() / 2;
canvas.drawCircle(radius, radius, radius, paint); //绘制音量线圈背景
paint.setAntiAlias(true);
paint.setColor(primaryVolumeColor);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(borderWidth);
canvas.drawCircle(radius, radius, radius - borderWidth, paint); //绘制音量线圈
paint.setAntiAlias(true);
paint.setColor(volumeColor);
rectF = new RectF(borderWidth, borderWidth, getWidth() - borderWidth, getHeight() - borderWidth);
if (isVolumeUp) {//音量增加时
canvas.drawArc(rectF, -90, angle * (volumeNum > 0 ? volumeNum - 1 : 0) + unitAngle * fraction, false, paint);
} else {//音量减小时
canvas.drawArc(rectF, -90, angle * (volumeNum + 1) - unitAngle * fraction, false, paint);
}
} /**
* 控制音量增加减少时的动画效果
*/
private void startAnim() {
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, maxAnimationValue);
valueAnimator.setDuration(300);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
fraction = (int) animation.getAnimatedValue();
if (DEBUG) {
Log.e(TAG, "the fraction is " + fraction);
}
invalidate();
}
});
valueAnimator.start();
} /**
* 加音量
*/
public void volumeUp() {
isVolumeUp = true;
if (volumeNum < maxVolume) {
volumeNum++;
startAnim();
}
} /**
* 减音量
*/
public void volumeDown() {
isVolumeUp = false;
if (volumeNum > 0) {
volumeNum--;
startAnim();
}
} }

VolumeView类暴露了很多方法,便于用户自定义圆形音量的UI风格。以上代码中实现了音量加减的动画效果,也就是如下代码:

/**
* 控制音量增加减少时的动画效果
*/
private void startAnim() {
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, maxAnimationValue);
valueAnimator.setDuration(300);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
fraction = (int) animation.getAnimatedValue();
if (DEBUG) {
Log.e(TAG, "the fraction is " + fraction);
}
invalidate();
}
});
valueAnimator.start();
}

代码中通过属性动画监听动画更新接口获取每个时刻的动画值,根据这个值每次去重新绘制UI,也就是调用invalidate();之后系统会重新调用onDraw()方法绘制UI。

不了解属性动画这一块的童鞋可以参考前面关于属性动画的博客 Android属性动画Property Animation系列一之ValueAnimator
以上就是全部的实现思路,代码就不一一解释了,毕竟有注释,效果还是很Nice~的。喜欢的童鞋,点赞吧!
~。

源码下载地址

Android自定义控件View(三)组合控件的更多相关文章

  1. Android自定义控件之自定义组合控件

    前言: 前两篇介绍了自定义控件的基础原理Android自定义控件之基本原理(一).自定义属性Android自定义控件之自定义属性(二).今天重点介绍一下如何通过自定义组合控件来提高布局的复用,降低开发 ...

  2. Android自定义控件之自定义组合控件(三)

    前言: 前两篇介绍了自定义控件的基础原理Android自定义控件之基本原理(一).自定义属性Android自定义控件之自定义属性(二).今天重点介绍一下如何通过自定义组合控件来提高布局的复用,降低开发 ...

  3. android自定义控件(五) 自定义组合控件

    转自http://www.cnblogs.com/hdjjun/archive/2011/10/12/2209467.html 代码为自己编写 目标:实现textview和ImageButton组合, ...

  4. Android 手机卫士--自定义组合控件构件布局结构

    由于设置中心条目中的布局都很类似,所以可以考虑使用自定义组合控件来简化实现 本文地址:http://www.cnblogs.com/wuyudong/p/5909043.html,转载请注明源地址. ...

  5. Android自己定义View之组合控件 ---- LED数字时钟

    先上图 LEDView效果如图所看到的. 之前看到一篇博客使用两个TextView实现了该效果.于是我想用自己定义控件的方式实现一个LEDView.使用时就可以直接使用该控件. 採用组合控件的方式,将 ...

  6. [android] 手机卫士自定义组合控件

    设置中心 新建SettingActivity 设置GridView条目的点击事件 调用GridView对象的setOnItemClickListenner()方法,参数:OnItemClickList ...

  7. Android开发之自定义组合控件

    自定义组合控件的步骤1.自定义一个View,继承ViewGroup,比如RelativeLayout2.编写组合控件的布局文件,在自定义的view中加载(使用View.inflate())3.自定义属 ...

  8. android:自己定义组合控件Weight(高仿猫眼底部菜单条)

    在我们实际开发其中.会碰见一些布局结构类似或者同样的界面.比如应用的设置界面.tabbutton界面等. 这时候.对于刚開始学习的人来说,xml里面一个个绘制出来也许是最初的想法.可能随着经验的积累, ...

  9. Android 自定义控件之 日期选择控件

    效果如下: 调用的代码: @OnClick(R.id.btn0) public void btn0() { final AlertDialog dialog = new AlertDialog.Bui ...

  10. Android 组合控件

    前言 自定义组合控件就是多个控件组合起来成为一个新的控件,主要用来解决多次重复的使用同一类型的布局.比如我们应用的顶部的标题栏,还有弹出的固定样式的dialog,这些都是常用的,所以把他们所需要的控件 ...

随机推荐

  1. 手把手教你用NDK9编译ffmpeg2.4.2

    编译环境: 32位 ubuntu12.10 android-ndk-r9c-linux-x86.tar.bz2 ffmpeg-2.4.2.tar.bz2 网上的教程都是以低版本号ffmpeg编译居多. ...

  2. 鸟哥的Linux私房菜-----16、程序与资源管理

    watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/ ...

  3. 15、python学习手册之:python语句、赋值、表达式和打印

    1.语句的另一个特殊规则是用一对括号把语句括起来就可以:括号().方括号[].字典的大括号{}.任何括在这些符号里的程序代码都可横跨好几行. 2.括号是可以包含一切的,因为任何表达式都可以包含在内,只 ...

  4. LM4990音频功放芯片

    我们选用的一种封装:我们用的是DGK封装. 典型电路图: 下面是示意图:四中封装的示意图是不一样的: 下面是真正的原理图: 高放大倍数的原理图: 查分式的: 单个输入的原理图: 下面是有关电源的选择:

  5. common daemon

    http://zdsyouxiang.iteye.com/blog/1940202 http://commons.apache.org/proper/commons-daemon/procrun.ht ...

  6. amazeui学习笔记--js插件(UI增强4)--下拉组件Dropdown

    amazeui学习笔记--js插件(UI增强4)--下拉组件Dropdown 一.总结 1.am-dropdown(及其孩子):控制下拉列表的样式 2.data-am-dropdown(及其孩子):控 ...

  7. 【例题 6-5 UVA 12657 】Boxes in a Line

    [链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] 双向链表模拟题. 移动的时候,要注意它就在所需要的位置的情况.那种情况不移动. (如果已经在所需位置了,还用链表的插入方式强行移动的 ...

  8. [WASM] Compile C Code into WebAssembly

    We use the C language instead of pure WAST to create a square root function using WASM Fiddle (https ...

  9. 【note】缩写词

    CoE CANopen EtherCAT应用程序概要文件CANopen™是一个注冊商标的能够自己主动化汽车集团..纽伦堡.德国CiA402CANopen™驱动器配置文件里指定的IEC 61800-7- ...

  10. 排查一般MySQL性能问题

    排查一般MySQL性能问题,通常要记录下面几项信息: 1.mysql> show processlist; 2.mysql> show engine innodb status\G 3.表 ...