相信大家在手机某个游戏APP的游戏列表中,都能看到这样类似的控件:
大家可以看到在这个界面的右侧下载的图标在点击后就会变成这样一个进度条的样子,下载完成后又会显示为打开,这样的ProgressButton是不是很有意思。我们这篇博客就讲一讲如何实现一个类似的控件,我们要完成的效果如下:
在显示为三角形的时候,下载暂停或者还没开始,点击开始会做一个放大淡出的动画,之后正方形的图片会出现做一个放大淡入的动画,这时进度条开始加载,在下载的时候暂停会保持下载进度。最后会有一个实心圆放大直到占满这个控件,下载完成的勾也会出现。
首先我们先自定义这个进度条,也就是演示中的弧度逐渐增加的弧形:
public class CustomProgress extends View {
private Paint myPaint;
private float startAngle;
float sweepAngle;
private RectF rect;
private MainLayout m;
private int pix = 0;
public CustomProgress(Context context, AttributeSet attrs, MainLayout mainLayout) {
super(context, attrs);
this.m = mainLayout;
init();
}
public CustomProgress(Context context, MainLayout mainLayout) {
super(context);
this.m = mainLayout;
init();
}
private void init() {
myPaint = new Paint();
DisplayMetrics metrics = getContext().getResources()
.getDisplayMetrics();
int width = metrics.widthPixels;
int height = metrics.heightPixels;
float scarea = width * height;
pix = (int) Math.sqrt(scarea * 0.0217);
myPaint.setAntiAlias(true);
myPaint.setStyle(Paint.Style.STROKE);
myPaint.setColor(Color.rgb(0, 161, 234));
myPaint.setStrokeWidth(7);
float startx = (float) (pix * 0.05);
float endx = (float) (pix * 0.95);
float starty = (float) (pix * 0.05);
float endy = (float) (pix * 0.95);
rect = new RectF(startx, starty, endx, endy);
}
public void setProgress(int progress) {
sweepAngle = (float) (progress * 3.6);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int desiredWidth = pix;
int desiredHeight = pix;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, widthSize);
} else {
width = desiredWidth;
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredHeight, heightSize);
} else {
height = desiredHeight;
}
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawArc(rect, startAngle, sweepAngle, false, myPaint);
startAngle = -90;
if (sweepAngle < 360) {
invalidate();
} else {
sweepAngle = 0;
startAngle = -90;
m.finalAnimation();
}
}
}
在init()方法中我们确定了控件在屏幕中的面积大小,以为是圆形,所以pix相当于控件的宽高。也为我们的画笔设置了属性,这个画笔就是用来画进度条的。
setProgress(int progress)是用来修改进度的,看了演示图我们知道进度是动态的,所以我们需要在外面为这个进度条设置进度,然后通过invalidate()方法调用onDraw()方法重写绘制进度条,达到缓慢增长的效果。
onMeasure()方法相信大家对获取specMode和specSize已经驾轻就熟了,这里也很简单,因为我们在init()方法中已经自定义了控件的宽高,所以在wrap_content的时候,我们就使用pix这个值就可以啦。
onDraw()方法就是画进度条,我们在canvas上画个弧形,startAngle,sweepAngle就是进度条的范围,startAngle因为弧形是把我们认为的90度那里设为0的所以应为-90,sweepAngle则没这些从0开始即可。如果sweep小于360,说明下载没有完成所以要不断重绘进度条。完成了就让我们的主控件播放下载完成的动画。
public class MainLayout extends FrameLayout {
public CustomProgress myView;
public int pix = 0;
public RectF rect;
private ImageView buttonImage, fillCircle, full_circle_image;
private Path stop, tick, play;
private Bitmap first_icon_bmp, third_icon_bmp, second_icon_bmp;
private Paint stroke_color, fill_color, icon_color, final_icon_color;
private AnimatorSet in, out;
private ObjectAnimator new_scale_in, scale_in, scale_out;
private ObjectAnimator fade_in, fade_out;
int flg_frmwrk_mode = 0;
boolean first_click = true;
public MainLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initialise();
setpaint();
setAnimation();
displayMetrics();
iconCreate();
init();
}
public MainLayout(Context context) {
super(context);
// TODO Auto-generated constructor stub
setBackgroundColor(Color.CYAN);
initialise();
setpaint();
setAnimation();
displayMetrics();
iconCreate();
init();
}
private void initialise() {
myView = new CustomProgress(getContext(), this);
buttonImage = new ImageView(getContext());
full_circle_image = new ImageView(getContext());
fillCircle = new ImageView(getContext());
myView.setClickable(false);
buttonImage.setClickable(false);
full_circle_image.setClickable(false);
myView.setClickable(false);
setClickable(true);
fillCircle.setClickable(false);
}
private void setpaint() {
stroke_color = new Paint(Paint.ANTI_ALIAS_FLAG);
stroke_color.setAntiAlias(true);
stroke_color.setColor(Color.rgb(0, 161, 234));
stroke_color.setStrokeWidth(3);
stroke_color.setStyle(Paint.Style.STROKE);
icon_color = new Paint(Paint.ANTI_ALIAS_FLAG);
icon_color.setColor(Color.rgb(0, 161, 234));
icon_color.setStyle(Paint.Style.FILL_AND_STROKE);
icon_color.setAntiAlias(true);
final_icon_color = new Paint(Paint.ANTI_ALIAS_FLAG);
final_icon_color.setColor(Color.WHITE);
final_icon_color.setStrokeWidth(12);
final_icon_color.setStyle(Paint.Style.STROKE);
final_icon_color.setAntiAlias(true);
fill_color = new Paint(Paint.ANTI_ALIAS_FLAG);
fill_color.setColor(Color.rgb(0, 161, 234));
fill_color.setStyle(Paint.Style.FILL_AND_STROKE);
fill_color.setAntiAlias(true);
}
private void setAnimation() {
in = new AnimatorSet();
out = new AnimatorSet();
out.setInterpolator(new AccelerateDecelerateInterpolator());
in.setInterpolator(new AccelerateDecelerateInterpolator());
PropertyValuesHolder p1 = PropertyValuesHolder.ofFloat("scaleX", 0.0f, 1.0f);
PropertyValuesHolder p2 = PropertyValuesHolder.ofFloat("scaleY", 0.0f, 1.0f);
scale_in = ObjectAnimator.ofPropertyValuesHolder(buttonImage, p1, p2).setDuration(150);
new_scale_in = ObjectAnimator.ofPropertyValuesHolder(fillCircle, p1, p2).setDuration(200);
PropertyValuesHolder p3 = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 3.0f);
PropertyValuesHolder p4 = PropertyValuesHolder.ofFloat("scaleY", 1.0f, 3.0f);
scale_out = ObjectAnimator.ofPropertyValuesHolder(buttonImage, p3, p4).setDuration(150);
fade_in = ObjectAnimator.ofFloat(buttonImage, "alpha", 0.0f, 1.0f).setDuration(150);
fade_out = ObjectAnimator.ofFloat(buttonImage, "alpha", 1.0f, 0.0f).setDuration(150);
in.play(scale_in).with(fade_in);
out.play(scale_out).with(fade_out);
out.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
buttonImage.setVisibility(View.GONE);
buttonImage.setImageBitmap(second_icon_bmp);
buttonImage.setVisibility(View.VISIBLE);
if (first_click == true) {
new ProgressActivity.DownLoadSigTask().execute();
first_click = false;
}
in.start();
full_circle_image.setVisibility(View.VISIBLE);
myView.setVisibility(View.VISIBLE);
flg_frmwrk_mode = 2;
}
});
new_scale_in.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
myView.setVisibility(View.GONE);
buttonImage.setVisibility(View.VISIBLE);
buttonImage.setImageBitmap(third_icon_bmp);
flg_frmwrk_mode = 3;
in.start();
}
});
}
private void displayMetrics() {
DisplayMetrics metrics = getContext().getResources()
.getDisplayMetrics();
int width = metrics.widthPixels;
int height = metrics.heightPixels;
float scarea = width * height;
pix = (int) Math.sqrt(scarea * 0.0217);
}
private void iconCreate() {
play = new Path();
play.moveTo(pix * 40 / 100, pix * 36 / 100);
play.lineTo(pix * 40 / 100, pix * 63 / 100);
play.lineTo(pix * 69 / 100, pix * 50 / 100);
play.close();
stop = new Path();
stop.moveTo(pix * 38 / 100, pix * 38 / 100);
stop.lineTo(pix * 62 / 100, pix * 38 / 100);
stop.lineTo(pix * 62 / 100, pix * 62 / 100);
stop.lineTo(pix * 38 / 100, pix * 62 / 100);
stop.close();
tick = new Path();
tick.moveTo(pix * 30 / 100, pix * 50 / 100);
tick.lineTo(pix * 45 / 100, pix * 625 / 1000);
tick.lineTo(pix * 65 / 100, pix * 350 / 1000);
}
public void init() {
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT);
lp.setMargins(10, 10, 10, 10);
fillCircle.setVisibility(View.GONE);
Bitmap.Config conf = Bitmap.Config.ARGB_8888; // see other conf types
Bitmap full_circle_bmp = Bitmap.createBitmap(pix, pix, conf);
Bitmap fill_circle_bmp = Bitmap.createBitmap(pix, pix, conf);
first_icon_bmp = Bitmap.createBitmap(pix, pix, conf); // Bitmap to draw
second_icon_bmp = Bitmap.createBitmap(pix, pix, conf); // Bitmap to draw
third_icon_bmp = Bitmap.createBitmap(pix, pix, conf); // Bitmap to draw
Canvas first_icon_canvas = new Canvas(first_icon_bmp);
Canvas second_icon_canvas = new Canvas(second_icon_bmp);
Canvas third_icon_canvas = new Canvas(third_icon_bmp);
Canvas fill_circle_canvas = new Canvas(fill_circle_bmp);
Canvas full_circle_canvas = new Canvas(full_circle_bmp);
float startx = (float) (pix * 0.05);
float endx = (float) (pix * 0.95);
float starty = (float) (pix * 0.05);
float endy = (float) (pix * 0.95);
rect = new RectF(startx, starty, endx, endy);
first_icon_canvas.drawPath(play, fill_color);
second_icon_canvas.drawPath(stop, icon_color);
third_icon_canvas.drawPath(tick, final_icon_color);
full_circle_canvas.drawArc(rect, 0, 360, false, stroke_color);
fill_circle_canvas.drawArc(rect, 0, 360, false, fill_color);
buttonImage.setImageBitmap(first_icon_bmp);
flg_frmwrk_mode = 1;
fillCircle.setImageBitmap(fill_circle_bmp);
full_circle_image.setImageBitmap(full_circle_bmp);
myView.setVisibility(View.GONE);
addView(full_circle_image, lp);
addView(fillCircle, lp);
addView(buttonImage, lp);
addView(myView, lp);
}
public void animation() {
if (flg_frmwrk_mode == 1) {
try {
Thread.sleep(500);
out.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void finalAnimation() {
buttonImage.setVisibility(View.GONE);
fillCircle.setVisibility(View.VISIBLE);
new_scale_in.start();
}
public void reset() {
buttonImage.setImageBitmap(first_icon_bmp);
flg_frmwrk_mode = 1;
}
}
从演示图可以看出我们的控件是由多个图片重叠在一起,通过设置是否可见达成的效果,所以这里我们的控件继承的是FrameLayout。
这里有很多变量,让我来一个个说明它们的用途。myView是我们的进度条,自不必说。pix与进度条一样是设置我们主控件的宽高。
buttonImage就是我们控件中心的暂停开始的图形,fillCircle是我们最后完成下载后的实心圆,full_circle_image则是当进度条为0是弧形的状态,那个比较细的弧形。
fill_color是三角形块和实心圆的画笔,icon_color是正方形块的画笔,final_icon_color则是那个白色勾的画笔。
下面几个就是我们要定义的变量了,这里我用的属性动画,都是简单的缩放,透明度的使用。
flg_frmwrk_mode是记录播放动画到了那个地方,我们看演示图可以知道整个下载可以分为三个阶段,首先是刚开始还没下载的时候(这里我把暂停也分为同一个阶段),然后就是下载,最后是结束动画。所以flg_frmwrk_mode在这个程序中有三个值1,2,3分别代表这三个阶段。
first_click是用来确定是否是第一次点击,因为我们这里是使用异步加载来实现进度变化,所以用它来判断是否开启异步任务。
initialise()应该不用说了,就是对对象实例化。setpaint()是为我们的画笔设置属性,这个大家自己看看就应该没问题了,FILL_AND_STROKE就是帮我们控制画笔填充内部颜色啦。
setAnimation()中定义动画的就不说了,不清楚的可以看我的博客Android–Property Animation介绍。我这里就讲讲动画监听中的逻辑。out是我们定义的淡出动画,在三角形块的动画播放完了,就说明要开始下载了,我们就把buttonImage的图片设置为正方形块,然后开启我们的异步任务,这个之后会讲解的,当然我们还要开始播放buttonImage的淡入动画,设置进度条可见后将flg_frmwrk_mode设为2。new_scale_in是最后的动画,我们为它设置的监听就是把白勾设置为buttonImage的图片,让它做淡入动画,将flg_frmwrk_mode设为3。
displayMetrics()确定控件的宽高pix。iconCreate()将那三个图片的绘制路径拿到。init()就是用我们的画笔在canvas上把那几张图画出来。最后的animation(),finalAnimation还有reset()方法经过上面的讲解应该很容易看懂。
然后就是我们在Activity中的布置了,我们的异步加载类也定义在这里。
public class ProgressActivity extends AppCompatActivity {
private static MainLayout mainLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_progress);
mainLayout = (MainLayout) findViewById(R.id.mainLayout);
mainLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
if (mainLayout.flg_frmwrk_mode == 1) {
mainLayout.animation();
runOnUiThread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Toast.makeText(ProgressActivity.this,
"Starting download", Toast.LENGTH_SHORT)
.show();
}
});
}
if (mainLayout.flg_frmwrk_mode == 2) {
mainLayout.reset();
runOnUiThread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Toast.makeText(ProgressActivity.this,
"Download stopped", Toast.LENGTH_SHORT)
.show();
}
});
}
if (mainLayout.flg_frmwrk_mode == 3) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Toast.makeText(ProgressActivity.this,
"Download complete", Toast.LENGTH_SHORT)
.show();
}
});
}
}
});
}
static class DownLoadSigTask extends AsyncTask<String, Integer, String> {
private int current = 0;
@Override
protected void onPreExecute() {
}
@Override
protected String doInBackground(final String... args) {
while(current <= 100) {
try {
Thread.sleep(50);
if (mainLayout.flg_frmwrk_mode == 1) {
Thread.yield();
} else {
current++;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
publishProgress(current);
}
return null;
}
@Override
protected void onProgressUpdate(Integer... progress) {
mainLayout.myView.setProgress(progress[0]);
}
}
}
点击事件对ProgressButton的三个阶段的操作很好理解,flg_frmwrk_mode为1点击时就开始播放动画,为2点击就重置回到1的状态,为3时就下载完成啦打印Toast即可。
在定义的异步类中逻辑也比较简单,在flg_frmwrk_mode=1的时候说明下载要处于暂停,于是线程就停下来,否则就让当前进度逐渐增加,每次变化都把current传入publishProgress(),进度一更新就会回调onProgressUpdate(Integer… progress)方法,我们就在这时调用自定义进度条中的setProgress()方法,修改进度。
这样我们就写好了所有的相关代码,在xml中应用下控件吧。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.ht.progress.ProgressActivity">
<com.ht.progress.MainLayout
android:id="@+id/mainLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:clickable="true">
</com.ht.progress.MainLayout>
</RelativeLayout>
最后的运行结果就如我们开头的图一样,多看别人好的效果是如何实现的对我们的帮助是很大的,这里有个GitHub上的项目汇总Android 开源项目分类汇总,希望对大家的学习有帮助。
结束语:本文仅用来学习记录,参考查阅。