前言
最近在做 App 的开屏页,一般都是创建一个 SplashActivity 来展示 Logo 与广告图,所以这里我也不例外,需要展示的图片可以是本地固定好的,也可以是与服务器交互请求获取到 Url 再进行加载,图片请求加载很简单,这里就不多说了,接下来进入正题。
想法
本来我只是想实现网易云音乐那样的,如图:
但似乎领导比较喜欢带进度的按钮,如图: 网易云音乐的这个很简单,设置好定时策略,决定延迟多久后跳转主页,然后就是那个{ 跳过}按钮,普通的Button
换个背景就行了,像网易云音乐那样的背景,自己用
drawable-shape
切一个圆角矩形即可。而有道的这个稍微复杂一点,既要有进度又要能点击,普通的
ProgressBar
应该是实现不了的(因为普通的圆形
ProgressBar
是不确定状态,不知道这样说对不对),所以我选择自定义一个
ProgressBar
。
思路
作为开屏页,那肯定只是用来展示的(你也可以在这里初始化一些东西传递给下一个界面),既然如此那就需要在一定时间内进行跳转了,这里我是用handler.postDelayed()
设置延迟,在加载不到图片的情况下 2 秒后进行跳转,如果加载到图片了则调用removeCallbacksAndMessages()
来移除这个延时操作,当进度条走完时再进行跳转,也可以直接点击{跳过}按钮执行跳转。话不多说,自定义 View 搞起来。
自定义 View - RoundProgressBar
- Java 层
/**
* 一个圆形进度条,用于开屏广告显示进度
*
* @author Aaron Zheng
* @since 2019.04.19
*/
public class RoundProgressBar extends View {
// 这一块作为控件默认属性,在使用者没有对相应属性进行赋值的情况下
private static final int RING_COLOR = Color.parseColor("#4D000000");
private static final int PROGRESS_COLOR = Color.parseColor("#FFDDAE44");
private static final int RING_WIDTH = DisplayUtil.dp2px(3);
private static final int TEXT_SIZE = DisplayUtil.sp2px(10);
private static final int TEXT_COLOR = Color.WHITE;
private static final int WIDTH = DisplayUtil.dp2px(35);
private static final int MAX_PROGRESS = 100;
private static final int CUR_PROGRESS = 0;
private static final String TEXT = "跳过";
private static boolean sIsSkip = false; // 标记位,表示是否点击了跳过
private int mRingColor; // 圆环颜色
private int mProgressColor; // 圆环进度条颜色
private int mRingWidth; // 圆环宽度
private int mTextSize; // 字体大小
private int mTextColor; // 字体颜色
private String mText; // 内容
private int mMaxProgress; // 最大进度
private int mCurProgress; // 当前进度
private Paint mRingPaint; // 圆环画笔
private Paint mProgressPaint; // 圆环进度画笔
private Paint mTextPaint; // 文字画笔
public RoundProgressBar(Context context) {
this(context, null);
}
public RoundProgressBar(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RoundProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 因为需要判断 Activity 是否被销毁,所以当使用者传入
// 非 Activity 的 Context 时抛出一个异常
if (!(context instanceof Activity))
throw new IllegalArgumentException("Context must be activity.");
// 初始化 View 的参数
init(context, attrs);
}
/**
* 由于是继承自 View ,所以肯定是需要重写 onMeasure() 方法了
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int size = Math.min(measureSize(widthMeasureSpec), measureSize(heightMeasureSpec));
setMeasuredDimension(size, size);
}
@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
// draw ring
float circleX = (float) getWidth() / 2; // 确定圆环的中心点
float circleY = (float) getWidth() / 2; // 确定圆环的中心点
// 确定半径,需要注意的是圆环的宽度并不一定等于 View 的宽度,
// 因为环是有厚度的,在计算半径时需要减去环宽度的一半
float radius = (float) getWidth() / 2 - (float) mRingWidth / 2;
canvas.drawCircle(circleX, circleY, radius, mRingPaint);
// draw progress ring
float sweepAngle = (float) mCurProgress / mMaxProgress * 360; // 绘制当前进度
// 4 个坐标点,因为弧需要通过矩形来确定自身的位置与大小
float leftTop = (float) mRingWidth / 2;
float rightBottom = getWidth() - leftTop;
// 创建确定弧位置大小的矩形
RectF oval = new RectF(leftTop, leftTop, rightBottom, rightBottom);
// -90 表示在时钟的 0 点开始绘制,sweepAngle 就是绘制范围,
// useCenter 为 false 表示不以扇形绘制
canvas.drawArc(oval, -90, sweepAngle, false, mProgressPaint);
// draw text
// 包含全部文本的最小矩形
Rect bounds = new Rect();
mTextPaint.getTextBounds(mText, 0, mText.length(), bounds);
// x 和 y 决定在 View 的哪个位置开始绘制,文本的绘制是在矩形的左下角开始的
float x = (float) getWidth() / 2 - (float) bounds.width() / 2;
float y = (float) getWidth() / 2 + (float) bounds.height() / 2;
canvas.drawText(mText, x, y, mTextPaint);
}
/**
* 设置点击监听器,与 setOnClickListener 区别在于形参是实现了 OnClickListener 的抽象类,
* 在实现的 onClick(View v) 中设置了被点击标记位,并需要使用者实现抽象方法。
*
* @param listener 实现了 OnClickListener 的抽象类
*/
public void setOnPressListener(OnPressListener listener) {
setOnClickListener(listener);
}
/**
* 进度条开始滑动
*
* @param countDown 倒计时具体毫秒后停止滑动
*/
public void startSlide(long countDown, SlideCallback callback) {
// 每 mMaxProgress 分之一的进度需要休眠的毫秒数
long sleep = countDown / mMaxProgress;
new Thread(() -> {
// 循环设置当前进度,如果没有休眠的话是看不到进度滑动的
for (int i = 0; i < mMaxProgress; i++) {
// 如果被点击或 Context 已销毁则跳出循环停止滑动,并重新赋值 sIsSkip 为 false
// 避免因持有 Context 而造成内存泄漏
if (sIsSkip || ((Activity) getContext()).isFinishing()) {
sIsSkip = false;
return;
}
SystemClock.sleep(sleep); // 开始休眠
this.setCurProgress(i + 1); // 设置当前进度,在 onDraw() 中通过这个去绘制进度
this.postInvalidate(); // 通知 View 进行绘制
// 将当前进度回调给调用方,调用方可根据当前进度来实现具体逻辑
// 使用 post()是因为回调在主线程发出后,调用方就不用再去切换回主线程了
this.post(() -> callback.onProgress(mCurProgress, mMaxProgress));
}
}).start();
}
/**
* 初始化自定义属性
*/
private void init(Context context, AttributeSet attrs) {
if (attrs != null) {
// 这里属于 View 的自定义属性,通过使用者在 layout 文件中写入的参数进行赋值
// 如果没有主动赋值则使用 View 的默认参数进行赋值
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundProgressBar);
mRingColor = typedArray.getColor(R.styleable.RoundProgressBar_ringColor, RING_COLOR);
mProgressColor = typedArray.getColor(R.styleable.RoundProgressBar_progressColor, PROGRESS_COLOR);
mRingWidth = (int) typedArray.getDimension(R.styleable.RoundProgressBar_ringWidth, RING_WIDTH);
String text = typedArray.getString(R.styleable.RoundProgressBar_text);
mText = text != null ? text : TEXT;
mTextSize = (int) typedArray.getDimension(R.styleable.RoundProgressBar_textSize, TEXT_SIZE);
mTextColor = typedArray.getColor(R.styleable.RoundProgressBar_textColor, TEXT_COLOR);
mMaxProgress = typedArray.getInteger(R.styleable.RoundProgressBar_maxProgress, MAX_PROGRESS);
mCurProgress = typedArray.getInteger(R.styleable.RoundProgressBar_curProgress, CUR_PROGRESS);
typedArray.recycle();
} else {
// 这一段用于使用者通过 Java 代码直接创建 View 后进行默认参数赋值
mRingColor = RING_COLOR;
mProgressColor = PROGRESS_COLOR;
mRingWidth = RING_WIDTH;
mText = TEXT;
mTextSize = TEXT_SIZE;
mTextColor = TEXT_COLOR;
mMaxProgress = MAX_PROGRESS;
mCurProgress = CUR_PROGRESS;
}
initUtils(); // 初始化工具,如画笔
}
/**
* 初始化画笔等工具
*/
private void initUtils() {
mRingPaint = new Paint();
mRingPaint.setAntiAlias(true); // 开启抗锯齿
mRingPaint.setStyle(Paint.Style.FILL); // FILL 表示绘制实心,STROKE 表示绘制空心
mRingPaint.setColor(mRingColor);
mProgressPaint = new Paint();
mProgressPaint.setAntiAlias(true);
mProgressPaint.setStyle(Paint.Style.STROKE);
mProgressPaint.setStrokeWidth(mRingWidth);
mProgressPaint.setColor(mProgressColor);
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setColor(mTextColor);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setStrokeWidth(0);
}
/**
* 由于直接继承 View ,为了避免在使用 wrap_content 时 View 无限大,因此需要重新测量大小
* 这里没什么说的,继承自 View 的都需要自己测量大小
*/
private int measureSize(int measureSpec) {
int result;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = WIDTH;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
// 下面为 getter 和 setter 方法,为 View 自定义属性的主动赋值修改与获取,可动态更改属性
public int getRingColor() {
return mRingColor;
}
public void setRingColor(int ringColor) {
mRingColor = ringColor;
}
public int getProgressColor() {
return mProgressColor;
}
public void setProgressColor(int progressColor) {
mProgressColor = progressColor;
}
... // 省略余下的 getter 和 setter 方法
/**
* 自定义点击监听器,在实现方法内加入被点击标记位
*/
public static abstract class OnPressListener implements OnClickListener {
@Override
public void onClick(View v) {
sIsSkip = true;
onPress(view);
}
public abstract void onPress(View view);
}
/**
* 回调滑动进度
*/
public interface SlideCallback {
void onProgress(int curProgress, int maxProgress);
}
}
复制代码
- xml 层
- 自定义的属性,在 values 文件夹下创建一个 attrs.xml 文件。属性名最好是对应 Java 层里面的属性名,这样清晰直观
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RoundProgressBar">
<attr name="ringColor" format="color" />
<attr name="progressColor" format="color" />
<attr name="ringWidth" format="dimension" />
<attr name="text" format="string" />
<attr name="textSize" format="dimension" />
<attr name="textColor" format="color" />
<attr name="maxProgress" format="integer" />
<attr name="curProgress" format="integer" />
</declare-styleable>
</resources>
复制代码
- layout 文件中使用,使用 app 命名空间引用自定义的属性,也可以不对自定义属性赋值,有默认值,根据自己需要来修改
<com.xxx.xxx.RoundProgressBar
android:id="@+id/round_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:curProgress="0"
app:maxProgress="100"
app:progressColor="@android:color/holo_red_light"
app:ringColor="@android:color/darker_gray"
app:ringWidth="10dp"
app:text="跳过"
app:textColor="@android:color/white"
app:textSize="12sp"/>
复制代码
RoundProgressBar 的使用
- 伪代码
RoundProgressBar roundProgressBar = findViewById(R.id.round_progress_bar);
roundProgressBar.setOnPressListener(new RoundProgressBar.OnPressListener() {
@Override
public void onPress(View view) {
// 由于我用在开屏页,有定时跳转策略,因此点击后需要移除
handler.removeCallbacksAndMessages(null);
... // 这里你可以实现自己的逻辑
}
});
// 加载图片
ImageLoader.getInstance().loadImage(this, urls, mTarget, new ImageLoader.Listener<Drawable>() {
@Override
public void onSuccess(Drawable drawable) {
// 既然加载到了服务器的图片,那么定时器也必须移除
handler.removeCallbacksAndMessages(null);
// 在布局文件中 RoundProgressBar 的 visibility=gone
// 因此这里应该 VISIBLE ,原因很简单,如果加载不到图片
// 那么 RoundProgressBar 也没有必要显示出来
roundProgressBar.setVisibility(View.VISIBLE);
// 图片加载成功,开始滑动进度,这里是设置以 4000 毫秒的时长来滑动进度的,
// 如果 maxProgress 是 100 ,则每百分之一的进度需要耗时 40 毫秒,
// 滑动进度通过 RoundProgressBar 内的 SlideCallback 进行回调
roundProgressBar.startSlide(4000, new RoundProgressBar.SlideCallback() {
@Override
public void onProgress(int curProgress, int maxProgress) {
... // 这里根据回调实现逻辑
}
});
}
@Override
public void onFailure(Throwable throwable) {
// 当加载图片发生异常时回调,可处理也可不处理,根据个人需要
}
});
复制代码
效果图
后记
到这里,一个全新的控件就实现完成了,过程还是很简单的。文章中有写得不对或者不够严谨的地方,还希望读者们能提出指正,感谢!