自定义View — 音乐相关控件

善良是很珍贵的,但善良没有长出牙齿来,那就是软弱。 — 《奇葩说》

写在前面

入职新公司已经快四个月了,进入公司就接手别人的项目,在改Bug这条路上越走越远...

还好最近不是很忙,花时间看了一下音乐模块中的自定义控件,难度系数一颗星,控件有三个,如下图:

自定义View.png

从上至下分别是:带有倒影的圆形图片(CircleImageView),可滑动的圆形SeekBar(CircleSeekBar),音乐播放中的跳动动画(FrequencyView)。

如何实现

下面依次来实现这三个控件,由于代码中已添加注释,所以就不做过多总结,只要认真看都能看的懂。

1.CircleImageView
public class CircleImageView extends AppCompatImageView {

    // 画笔
    private Paint mPaint;
    // 矩阵,对图像进行变换
    private Matrix mMatrix;
    // 图像着色器
    private BitmapShader mBitmapShader;
    // 线性渐变着色器
    private LinearGradient mLinearGradient;
    // 图像重叠时的显示方式
    private Xfermode mXferMode;

    // 是否倒影
    private boolean isReflect;
    // 倒影是否虚化
    private boolean isVirtual;

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

    public CircleImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    private void init(AttributeSet attrs) {
        if (attrs != null) {
            TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircleImageView);
            isReflect = typedArray.getBoolean(R.styleable.CircleImageView_reflect, false);
            isVirtual = typedArray.getBoolean(R.styleable.CircleImageView_virtual, false);
            typedArray.recycle();
        } else {
            isReflect = false;
            isVirtual = false;
        }

        // 创建抗锯齿画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        // 创建矩阵
        mMatrix = new Matrix();
        // 创建默认线性渐变着色器
        mLinearGradient = new LinearGradient(getWidth() / 2, 0, getWidth() / 2,
                getHeight(), 0, 0x60ffffff, Shader.TileMode.CLAMP);
        // 创建默认图像重叠的显示方式
        mXferMode = new PorterDuffXfermode(PorterDuff.Mode.SCREEN);
    }

    public void setLinearGradient(LinearGradient gradient) {
        mLinearGradient = gradient;
    }

    public void setXferMode(Xfermode xfermode) {
        mXferMode = xfermode;
    }

    public void setReflect(boolean isReflect) {
        this.isReflect = isReflect;
    }

    public void setVirtual(boolean isVirtual) {
        this.isVirtual = isVirtual;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Bitmap originBitmap = drawableToBitmap(getDrawable());
        if (originBitmap == null) {
            return;
        }

        if (isReflect) {
            // 通过矩阵将图像的y坐标全部取反,以此达到镜像效果
            mMatrix.setScale(1, -1);
            // 创建一个新的Bitmap
            originBitmap = Bitmap.createBitmap(originBitmap,0 , 0,
                    originBitmap.getWidth(), originBitmap.getHeight(), mMatrix, false);
        }

        float scale = computeScale(originBitmap.getWidth(), originBitmap.getHeight());
        float radius = getWidth() / 2;

        /**
         * 使用一个指定的图像给Paint进行着色,在绘制的时候根据设置的TitleMode模式和图像来形成不同的效果
         * 参数一:Bitmap对象
         * 参数二:水平方向的平铺模式
         * 参数三:垂直方向的平铺模式
         */
        mBitmapShader = new BitmapShader(originBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
        // 设置缩放比例
        mMatrix.setScale(scale, scale);
        // 将矩阵设置给图像着色器,通过矩阵对图像进行变换
        mBitmapShader.setLocalMatrix(mMatrix);
        // 给画笔设置图像着色器
        mPaint.setShader(mBitmapShader);
        /**
         * 画圆
         * 参数一:圆心x坐标
         * 参数二:圆心y坐标
         * 参数三:半径
         * 参数四:画笔
         */
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mPaint);

        if (isReflect && isVirtual) {
            mPaint.setShader(mLinearGradient);
            // 给画笔设置图像重叠时的显示方式
            mPaint.setXfermode(mXferMode);
            canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius, mPaint);
        }
    }

    /**
     * Drawable 转 Bitmap
     * @param drawable
     * @return
     */
    private Bitmap drawableToBitmap(Drawable drawable) {
        if (drawable == null) {
            return null;
        }

        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        }

        int width = drawable.getIntrinsicWidth();
        int height = drawable.getIntrinsicHeight();
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, width, height);
        drawable.draw(canvas);
        return bitmap;
    }

    /**
     * 计算缩放比例
     * @param originWidth
     * @param originHeight
     * @return
     */
    private float computeScale(float originWidth, float originHeight) {
        float widthScale = getWidth() / originWidth;
        float heightScale = getHeight() / originHeight;
        return Math.max(widthScale, heightScale);
    }
}
2.CircleSeekBar
public class CircleSeekBar extends View {

    // 进度条画笔
    private Paint mProgressPaint;
    // 滑块画笔
    private Paint mKnobPaint;

    // 进度条可绘制的范围
    private RectF mProgressRectF;

    // 圆形渐变着色器
    private SweepGradient mSweepGradient;

    // 进度条扫过的角度
    private float mSweepAngle;
    // 进度条半径
    private float mRadius;

    // 进度条宽度
    private int mProgressWidth;
    // 滑块宽度 = 直径
    private int mKnobWidth;
    // 可响应Touch事件范围
    private int mTrackArea;

    // 进度条背景色
    private int mBgColor;
    // 滑块颜色
    private int mKnobColor;

    // 当前进度
    private long mProgress;
    // 总进度
    private long mDuration;

    // 是否正在响应Touch事件
    private boolean isDragging;

    private OnSeekChangeListener mOnSeekChangeListener;

    public interface OnSeekChangeListener {

        void onStartTrackingTouch();

        void onProgressChanged(long progress);

        void onStopTrackingTouch();
    }

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

    public CircleSeekBar(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    public CircleSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        this(context, attrs, defStyleAttr);
    }

    private void init(AttributeSet attrs) {
        if (attrs != null) {
            TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircleSeekBar);
            mProgressWidth = typedArray.getDimensionPixelSize(R.styleable.CircleSeekBar_progress_width, 6);
            mKnobWidth = typedArray.getDimensionPixelSize(R.styleable.CircleSeekBar_knob_width, mProgressWidth * 2);
            mTrackArea = typedArray.getDimensionPixelSize(R.styleable.CircleSeekBar_track_area, mProgressWidth * 2);
            mBgColor = typedArray.getColor(R.styleable.CircleSeekBar_bg_color, Color.GRAY);
            mKnobColor = typedArray.getColor(R.styleable.CircleSeekBar_knob_color, Color.BLUE);
            mProgress = typedArray.getInt(R.styleable.CircleSeekBar_position, 0);
            mDuration = typedArray.getInt(R.styleable.CircleSeekBar_duration, 0);
            typedArray.recycle();
        } else {
            mProgressWidth = 6;
            mKnobWidth = mProgressWidth * 2;
            mTrackArea = mProgressWidth * 2;
            mBgColor = Color.GRAY;
            mKnobColor = Color.BLUE;
            mProgress = 0;
            mDuration = 0;
        }

        // 创建抗锯齿进度条画笔
        mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        // 设置画笔样式为描边
        mProgressPaint.setStyle(Paint.Style.STROKE);
        // 设置画笔的图形样式为矩形(前提需设置画笔样式为STROKE或FILL_AND_STROKE)
        mProgressPaint.setStrokeCap(Paint.Cap.SQUARE);

        // 创建抗锯齿滑块画笔
        mKnobPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        // 设置画笔样式为填充
        mKnobPaint.setStyle(Paint.Style.FILL);

        // 创建进度条可绘制的范围
        mProgressRectF = new RectF();

        // 创建默认圆形渐变着色器
        mSweepGradient = new SweepGradient(getWidth() / 2, getHeight() / 2, Color.YELLOW, Color.YELLOW);

        // 初始化进度条扫过的角度
        mSweepAngle = (float) mProgress / mDuration * 360;
    }

    public void setSweepGradient(int startColor, int endColor) {
        mSweepGradient = new SweepGradient(getWidth() / 2, getHeight() / 2, startColor, endColor);
    }

    public void setSweepGradient(SweepGradient gradient) {
        mSweepGradient = gradient;
    }

    public void setProgressWidth(int width) {
        mProgressWidth = width;
    }

    public void setKnobWidth(int width) {
        mKnobWidth = width;
    }

    public void setTrackArea(int area) {
        mTrackArea = area;
    }

    public void setBgColor(int color) {
        mBgColor = color;
    }

    public void setKnobColor(int color) {
        mKnobColor = color;
    }

    public void setProgress(long progress) {
        // 当响应Touch事件时,不接收外部传进来的进度
        if (isDragging) {
            return;
        }

        mProgress = progress;

        mSweepAngle = (float) progress / mDuration * 360;

        postInvalidate();
    }

    public void setMax(long max) {
        mDuration = max;
    }

    public void setOnSeekChangeListener(OnSeekChangeListener listener) {
        mOnSeekChangeListener = listener;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 计算进度条可绘制的范围
        mProgressRectF.top = mProgressWidth / 2 + mTrackArea;
        mProgressRectF.left = mProgressWidth / 2 + mTrackArea;
        mProgressRectF.bottom = h - mProgressWidth / 2 - mTrackArea;
        mProgressRectF.right = w - mProgressWidth / 2 - mTrackArea;

        // 计算进度条的半径
        mRadius = (w - mProgressWidth) / 2 - mTrackArea;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        mProgressPaint.setStrokeWidth(mProgressWidth);

        drawBackground(canvas);
        drawProgress(canvas);
        drawKnob(canvas);
    }

    /**
     * 绘制进度条背景
     *
     * @param canvas
     */
    private void drawBackground(Canvas canvas) {
        // 设置画笔的着色器为null
        mProgressPaint.setShader(null);
        // 设置画笔的颜色
        mProgressPaint.setColor(mBgColor);
        /**
         * 画圆弧
         * 参数一:确定圆弧形状与尺寸的椭圆边界
         * 参数二:起始角度
         * 参数三:扫过角度
         * 参数四:是否包含圆心
         * 参数五:画笔
         */
        canvas.drawArc(mProgressRectF, 0, 360, false, mProgressPaint);
    }

    /**
     * 绘制进度
     *
     * @param canvas
     */
    private void drawProgress(Canvas canvas) {
        canvas.rotate(90, (float) getWidth() / 2, (float) getHeight() / 2);
        mProgressPaint.setShader(mSweepGradient);
        mProgressPaint.setColor(0);
        mProgressPaint.setAlpha(255);
        canvas.drawArc(mProgressRectF, 0, mSweepAngle, false, mProgressPaint);
    }

    /**
     * 绘制滑块
     *
     * @param canvas
     */
    private void drawKnob(Canvas canvas) {
        // 根据角度求出弧度
        double radians = Math.toRadians(mSweepAngle);
        // 根据弧度求出滑块圆心x,y坐标
        float centerX = (float) (getWidth() / 2 + mRadius * Math.cos(radians));
        float centerY = (float) (getHeight() / 2 + mRadius * Math.sin(radians));

        mKnobPaint.setColor(mKnobColor);
        canvas.drawCircle(centerX, centerY, (float) mKnobWidth / 2, mKnobPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!isEnabled()) {
            return super.onTouchEvent(event);
        }

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!canTouch(event.getX(), event.getY())) {
                    return super.onTouchEvent(event);
                }
                isDragging = true;
                if (mOnSeekChangeListener != null) {
                    mOnSeekChangeListener.onStartTrackingTouch();
                }
            case MotionEvent.ACTION_MOVE:
                // 根据反三角函数求出弧度
                double radians = Math.atan2(event.getY() - getHeight() / 2, event.getX() - getWidth() / 2);
                // 通过弧度求出角度,因为0度默认从3点钟方向开始,这里的起始位置是从90度开始,所以要减去90度
                double angle = Math.toDegrees(radians) - 90;
                if (angle < 0) {
                    angle = 360 + angle;
                }

                mSweepAngle = (float) angle;
                mProgress = (long) (mSweepAngle / 360 * mDuration);

                if (mOnSeekChangeListener != null) {
                    mOnSeekChangeListener.onProgressChanged(mProgress);
                }
                invalidate();
                return true;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (isDragging) {
                    isDragging = false;
                    if (mOnSeekChangeListener != null) {
                        mOnSeekChangeListener.onStopTrackingTouch();
                    }
                }
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 计算可以响应Touch区域
     * 原理:三角形两边之和大于第三边
     *
     * @param x
     * @param y
     * @return
     */
    private boolean canTouch(float x, float y) {
        float centerX = (float) getWidth() / 2;
        float centerY = (float) getHeight() / 2;
        return Math.pow(centerX - x, 2) + Math.pow(centerY - y, 2) > Math.pow(mRadius - (mTrackArea * 2), 2);
    }
}
3.FrequencyView
public class FrequencyView extends View {

    // 画笔
    private Paint mPaint;

    // 频率数量
    private int mFreqCount;
    // 每个频率的宽度
    private int mFreqWidth;
    // 两个频率的间隙
    private int mFreqOffset;
    // 频率颜色
    private int mFreqColor;

    // 频率高度最大值
    private int mMax;
    // 频率高度最小值
    private int mMin;
    // 频率高度步进
    private int mStep;

    // 存放频率的集合
    private List mFrequencies;

    // 是否正在播放中
    private boolean isPlaying;

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

    public FrequencyView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FrequencyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    private void init(AttributeSet attrs) {
        if (attrs != null) {
            TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.FrequencyView);
            mFreqCount = typedArray.getInteger(R.styleable.FrequencyView_freq_count, 3);
            mFreqWidth = typedArray.getDimensionPixelSize(R.styleable.FrequencyView_freq_width, 8);
            mFreqOffset = typedArray.getDimensionPixelSize(R.styleable.FrequencyView_freq_offset, 3);
            mFreqColor = typedArray.getColor(R.styleable.FrequencyView_freq_color, Color.WHITE);
            typedArray.recycle();
        } else {
            mFreqCount = 3;
            mFreqWidth = 8;
            mFreqOffset = 3;
            mFreqColor = Color.WHITE;
        }

        // 创建抗锯齿画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        // 设置画笔颜色
        mPaint.setColor(mFreqColor);

        // 将全部频率创建出来
        mFrequencies = new ArrayList<>();
        Random random = new Random();
        for (int i = 0; i < mFreqCount; i++) {
            // 初始频率高度随机生成,状态都为PLUS
            int freq = random.nextInt(30);
            mFrequencies.add(new Frequency(freq, Frequency.PLUS));
        }
    }

    public void setPlaying(boolean isPlaying) {
        this.isPlaying = isPlaying;

        // 若正在播放,需要绘制
        if (isPlaying) {
            postInvalidate();
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        // 给频率高度最大值赋值,需要对内边距进行处理
        mMax = h - getPaddingTop() - getPaddingBottom();
        // 给频率高度最小值赋值
        mMin = mMax / 3;
        // 给频率步进赋值
        mStep = mMin / 5;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        for (int i = 0; i < mFreqCount; i++) {
            Frequency frequency = mFrequencies.get(i);
            switch (frequency.status) {
                case Frequency.PLUS:
                    /**
                     * 当前频率需要向上加时:
                     * 如果频率高度还没有到最大值,则继续向上加
                     * 否则就向下减,并将状态设置为REDUCE
                     */
                    if (frequency.freq < mMax) {
                        frequency.freq += mStep;
                    } else {
                        frequency.freq -= mStep;
                        frequency.status = Frequency.REDUCE;
                    }
                    break;
                case Frequency.REDUCE:
                    /**
                     * 当前频率需要向下减时:
                     * 如果频率高度还没有到最小值,则继续向下减
                     * 否则就向上加,并将状态设置为PLUS
                     */
                    if (frequency.freq > mMin) {
                        frequency.freq -= mStep;
                    } else {
                        frequency.freq += mStep;
                        frequency.status = Frequency.PLUS;
                    }
                    break;
                default:
                    break;
            }

            // 开始画频率,其中要考虑内边距问题,所以对内边距进行处理
            Rect rect = new Rect();
            rect.top = getHeight() - frequency.freq - getPaddingBottom();
            rect.left = i * (mFreqWidth + mFreqOffset) + getPaddingLeft();
            rect.bottom = getHeight() - getPaddingBottom();
            rect.right = i * (mFreqWidth + mFreqOffset) + getPaddingLeft() + mFreqWidth;
            canvas.drawRect(rect, mPaint);
        }

        // 若正在播放,需要重复绘制
        if (isPlaying) {
            postInvalidateDelayed(30);
        }
    }

    /**
     * 频率
     * 包括加减状态和高度
     */
    private class Frequency {
        // 频率需要向上加
        public static final int PLUS = 0;
        // 频率需要向下减
        public static final int REDUCE = 1;

        public int freq;
        public int status;

        public Frequency(int freq, int status) {
            this.freq = freq;
            this.status = status;
        }
    }
}
5.attrs


    
        
        
    

    
        
        
        
        
        
        
        
    

    
        
        
        
        
    

如何使用

通过一个小Demo演示一下这三个控件。

1.编写xml布局文件


    

    

    

    


2.编写CircleActivity

public class CircleActivity extends AppCompatActivity {

    private static final String TAG = CircleActivity.class.getSimpleName();

    private CircleImageView mOriginView;
    private CircleImageView mReflectView;

    private CircleSeekBar mSeekBar;

    private FrequencyView mFrequencyView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_circle);

        initView();
    }

    private void initView() {
        mOriginView = findViewById(R.id.view_circle_origin);
        mOriginView.setImageResource(R.drawable.pic_girl);

        mReflectView = findViewById(R.id.view_circle_reflect);
        mReflectView.setImageResource(R.drawable.pic_girl);

        mSeekBar = findViewById(R.id.view_seek_bar);
        mSeekBar.setOnSeekChangeListener(new CircleSeekBar.OnSeekChangeListener() {
            @Override
            public void onStartTrackingTouch() {
                Log.d(TAG, "onStartTrackingTouch");
            }

            @Override
            public void onProgressChanged(long progress) {
                Log.d(TAG, "onProgressChanged : progress = " + progress);
            }

            @Override
            public void onStopTrackingTouch() {
                Log.d(TAG, "onStopTrackingTouch");
            }
        });

        mFrequencyView = findViewById(R.id.view_frequency);
        mFrequencyView.setPlaying(true);
    }
}

运行效果如下:

自定义View.png

总结

自定义控件在日常开发中必不可少,虽然百度会帮助我们解决大部分问题,但是不能过分依赖百度,每次遇到问题先找百度,为了工作而工作,事后不做总结,下次遇到还是不知道怎么解决,久而久之就会丧失学习能力,自定义控件并不难,实则很简单,勇敢的迈出第一步,进一步海阔天空,退一步昏天黑地。

你可能感兴趣的:(自定义View — 音乐相关控件)