善良是很珍贵的,但善良没有长出牙齿来,那就是软弱。 — 《奇葩说》
写在前面
入职新公司已经快四个月了,进入公司就接手别人的项目,在改Bug这条路上越走越远...
还好最近不是很忙,花时间看了一下音乐模块中的自定义控件,难度系数一颗星,控件有三个,如下图:
从上至下分别是:带有倒影的圆形图片(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);
}
}
运行效果如下:
总结
自定义控件在日常开发中必不可少,虽然百度会帮助我们解决大部分问题,但是不能过分依赖百度,每次遇到问题先找百度,为了工作而工作,事后不做总结,下次遇到还是不知道怎么解决,久而久之就会丧失学习能力,自定义控件并不难,实则很简单,勇敢的迈出第一步,进一步海阔天空,退一步昏天黑地。