• Android自定义控件View(三)组合控件


    不少人应该见过小米手机系统音量控制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 测试
    android 创建快捷方式
    POJ 3320 尺取法(基础题)
    HDOJ 1260 DP
    数位DP练习
    P2727 Stringsobits
    poj 2229 DP
    Canada Cup 2016 C. Hidden Word
    hdoj 1231 最大连续子列和
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/6113499.html
Copyright © 2020-2023  润新知