本文目的
平时Android项目中看到的加载中的动效基本上就是转圈的形式,有点审美疲劳。前一篇文章通过ViewGroup做了一个简单的加载中的动效,上一篇文章的主要知识点基于ViewGroup实现自定义组合视图。本文仍然是基于ViewGroup实现自定义组合视图,将重点展示如何充分利用Android提供的属性动效实现一个有趣的加载中动效。文中较多的使用了位移动效,透明度动效。
效果介绍
实现了一个太阳升级/落下,月亮升起落下/星星眨眼的动效,组合起来之后可以作为加载中动效
自定义View代码
本次基于自定义视图绘制的形状包括太阳/月亮/星星三类自然景物。首先太阳绘制的基本思路是画完圆形之后,绘制横线并且进行画布的旋转,即可实现太阳光线。关键代码如下:
/**
* 绘制太阳
* @param canvas
*/
private void drawSun(Canvas canvas){
//将画布平移至中心点
canvas.translate(width/2+getPaddingLeft(),height/2+getPaddingTop());
//画圆
canvas.drawCircle(0,0,sunRadius,sunPaint);
//画光线
drawSunshine(canvas);
}
……
private void drawSunshine(Canvas canvas){
/**
* 逐个绘制光线,绘制10条线
*/
if(index_draw_sunshine < 10 && index_draw_sunshine >= 0){
for(int i=0 ;i<=index_draw_sunshine;i++){
canvas.drawLine(sunRadius+10, 0, sunRadius+30,0,sunshinePaint);
canvas.rotate(-360/MAX_COUNT);
}
index_draw_sunshine++;
postInvalidateDelayed(20);
}else{
for(int i=0 ;i<index_draw_sunshine;i++){
canvas.drawLine(sunRadius+10, 0, sunRadius+30,0,sunshinePaint);
canvas.rotate(-360/MAX_COUNT);
}
}
}
绘制月亮的基本方法是通过绘制两端圆弧连接实现,过程中需要平移画布的位置实现,过程代码如下:
//绘制月亮
private void drawMoon(Canvas canvas){
canvas.translate(width/2+getPaddingLeft(),height/2+getPaddingTop());
//绘制大圆弧
RectF oval = new RectF(-sunRadius , -sunRadius , sunRadius ,sunRadius);
canvas.drawArc(oval , -120 , 240 , false , sunPaint);
//绘制小圆弧
RectF oval1 = new RectF(-2*sunRadius , -sunRadius , 0 ,sunRadius);
canvas.drawArc(oval1 , -60 , 120 ,false ,sunPaint);
}
绘制星星的基本思路是先计算得到星星绘制所需要的10个点的横纵坐标,然后基于Path连接这些坐标点即可,代码如下
/**
* 自定义视图实现星星
*/
public class StarView extends View {
/**
* 绘制星星所需的内径和外径
*/
private int outRadius , innerRadius;
private Paint mPaint;
private int[][] points = new int[10][2];
private static final int NUM_POINTS = 10;
/**
* 视图的宽度和高度
*/
private int width,height;
/**
* 星星在父类视图的左上坐标,半径
*/
private int x , y;
private int size;
public StarView(Context context) {
super(context);
initPaint();
}
public StarView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint();
}
public StarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
private void initPaint(){
if(null == mPaint){
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(3);
mPaint.setColor(getResources().getColor(R.color.purple_200));
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
outRadius = Math.min(width,height)/4;
innerRadius = outRadius/2;
getPositions();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawStar(canvas);
}
/**
* 使用Path绘制星星
* @param canvas
*/
private void drawStar(Canvas canvas){
Path path = new Path();
for(int i = 0 ; i < NUM_POINTS ; i++){
if(i == 0){
path.moveTo(points[i][0] , points[i][1]);
}else{
path.lineTo(points[i][0] , points[i][1]);
}
}
path.close();
canvas.drawPath(path,mPaint);
}
/**
* 计算星星的顶点坐标
* @return
*/
private int[][] getPositions(){
int center_x = width/2 + getPaddingLeft();
int center_y = height/2 + getPaddingTop();
for(int i = 0 ; i < NUM_POINTS ; i++){
int r = (i%2 == 0 ? outRadius : innerRadius);
double angle = -Math.PI / 10 + i*2*Math.PI / 10 ;
//横坐标
points[i][0] = (int)(r*Math.cos(angle)) + center_x;
//纵坐标
points[i][1] = (int)(r*Math.sin(angle)) + center_y;
}
return points;
}
public int getPositionX() {
return x;
}
public void setPositionX(int x) {
this.x = x;
}
public int getPositionY() {
return y;
}
public void setPositionY(int y) {
this.y = y;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
}
实现动效的代码
本次加载中动效分为4个步骤实现:1.太阳落下{@link #sunSet()};2.月亮升起并且星星显示{@link #moonRise()};月亮和星星收起{@link #moonStarVanish()};4.太阳升起{@link #sunRise()}。过程代码如下:
/**
* 本次加载中动效分为4个步骤实现:1.太阳落下{@link #sunSet()};2.月亮升起并且星星显示{@link #moonRise()};
* 3.月亮和星星收起{@link #moonStarVanish()};4.太阳升起{@link #sunRise()}
*/
public class LoadingView extends FrameLayout {
private SunMoonView sunMoonView;
/**
* 全部的星星将通过addView添加,此处缓存添加的星星对象,用于对星星设置动效
*/
private List<StarView> starViews = new ArrayList<>();
/**
* 整体布局宽度,高度
*/
private int width , height;
/**
* 太阳的视图半径,星星最大半径
*/
private int sunRadius , starMaxRadius;
/**
* 太阳和月亮落下,升起时的位移距离
*/
private int shiftDistance;
/**
* 上升和下降的组合动效
*/
private AnimatorSet upAnimatorSet,downAnimatorSet;
private static final String TAG = "LoadingView";
public LoadingView(Context context) {
super(context);
}
public LoadingView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public LoadingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context , AttributeSet attrs){
LayoutInflater.from(context).inflate(R.layout.layout_loading_view,this , true);
sunMoonView = findViewById(R.id.sun_moon_view);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
sunRadius = Math.min(width , height) / 5;
starMaxRadius = sunRadius / 2;
shiftDistance = height/2 + sunRadius;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
removeAllViews();
post(new Runnable() {
@Override
public void run() {
layoutSunMoon();
sunSet();
}
});
}
/**
* 设置太阳月亮初始位置
*/
private void layoutSunMoon(){
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)sunMoonView.getLayoutParams();
layoutParams.width = 2*sunRadius;
layoutParams.height = layoutParams.width;
layoutParams.leftMargin = width/2 - sunRadius;
layoutParams.topMargin = height/2 - sunRadius;
sunMoonView.setLayoutParams(layoutParams);
addView(sunMoonView);
}
/**
* 随机设置星星的位置
*/
private void layoutStar(){
Random random = new Random();
int maxNum = height*2/5/2/starMaxRadius * width/2/starMaxRadius;
int setNum = maxNum /2;
//可以布置的行数
int rows = height*2/5/2/starMaxRadius;
//可以布置的列数
int lines = width/2/starMaxRadius;
starViews.clear();
Log.d(TAG,"starMaxRadius = "+starMaxRadius+",sunRadius = "+sunRadius+",rows = "+rows + ",setNum = "+setNum);
if(setNum > 0 ){
for(int i = 0 ; i < setNum ; i++){
StarView starView = new StarView(getContext());
int x = i%lines*2*starMaxRadius + random.nextInt(starMaxRadius);
int y = i/lines*2*starMaxRadius + random.nextInt(starMaxRadius);
int size = starMaxRadius/2 + random.nextInt(starMaxRadius/2);
starView.setPositionX(x);
starView.setPositionY(y);
starView.setSize(size);
ViewGroup.MarginLayoutParams layoutParams = new ViewGroup.MarginLayoutParams(size*2,size*2);
layoutParams.leftMargin = x;
layoutParams.topMargin = y;
starView.setLayoutParams(layoutParams);
addView(starView);
starViews.add(starView);
}
}
}
/**
* 将dp值转为像素
* @param dp 输入dp类型尺寸
* @return
*/
private int dp2px(int dp){
float dpi = getContext().getResources().getDisplayMetrics().density;
return (int)(dp*dpi + 0.5f);
}
/**
* 1.日落
*/
private void sunSet(){
ObjectAnimator bottomShiftAnimator = ObjectAnimator.ofFloat(sunMoonView,"translationY",
0,shiftDistance);
ObjectAnimator rotateAnimator = ObjectAnimator.ofFloat(sunMoonView , "rotation",0,360);
downAnimatorSet = new AnimatorSet();
downAnimatorSet.playTogether(bottomShiftAnimator , rotateAnimator);
downAnimatorSet.setDuration(3000);
downAnimatorSet.setInterpolator(new DecelerateInterpolator());
downAnimatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
postDelayed(new Runnable() {
@Override
public void run() {
moonRise();
}
} , 1000);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
downAnimatorSet.start();
}
/**
* 2.月出:月亮升起,星星出现和闪烁
*/
private void moonRise(){
layoutStar();
sunMoonView.changeShape();
ObjectAnimator upShiftAnimator = ObjectAnimator.ofFloat(sunMoonView,"translationY",
shiftDistance,0);
ObjectAnimator rotateAnimator = ObjectAnimator.ofFloat(sunMoonView , "rotation",-20,20);
rotateAnimator.setRepeatCount(2);
rotateAnimator.setRepeatMode(ObjectAnimator.REVERSE);
upAnimatorSet = new AnimatorSet();
upAnimatorSet.playTogether(upShiftAnimator , rotateAnimator);
upAnimatorSet.setDuration(3000);
upAnimatorSet.setInterpolator(new AccelerateInterpolator());
upAnimatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
postDelayed(new Runnable() {
@Override
public void run() {
starShink();
}
} , 1000);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
upAnimatorSet.start();
starShow();
}
/**
* 2.2设置星星闪烁
*/
private void starShink(){
for(int i = 0 ; i < starViews.size() ; i++){
if(i % 2 == 0){
ObjectAnimator alpha = ObjectAnimator.ofFloat(starViews.get(i),"alpha",
1.0f,0f);
alpha.setDuration(1000);
alpha.setRepeatCount(2);
alpha.setRepeatMode(ValueAnimator.REVERSE);
alpha.start();
if((i == (starViews.size()-1)) | (i == (starViews.size()-1))){
alpha.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
postDelayed(new Runnable() {
@Override
public void run() {
moonStarVanish();
}
} , 1000);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
}
}
}
/**
* 2.1设置星星显示
*/
private void starShow(){
for(int i = 0 ; i < starViews.size() ; i++){
ObjectAnimator bottomShiftAnimator = ObjectAnimator.ofFloat(starViews.get(i),"translationY",
-3*starViews.get(i).getSize(),starViews.get(i).getPositionY());
bottomShiftAnimator.setDuration(3000);
bottomShiftAnimator.start();
}
}
/**
* 3.1设置星星隐藏
*/
private void starHide(){
for(int i = 0 ; i < starViews.size() ; i++){
ObjectAnimator upShiftAnimator = ObjectAnimator.ofFloat(starViews.get(i),"translationY",
starViews.get(i).getPositionY(),-3*starViews.get(i).getSize());
upShiftAnimator.setDuration(3000);
upShiftAnimator.start();
}
}
/**
* 3.月亮落下,星星收起
*/
private void moonStarVanish(){
ObjectAnimator bottomShiftAnimator = ObjectAnimator.ofFloat(sunMoonView,"translationY",
0,shiftDistance);
ObjectAnimator rotateAnimator = ObjectAnimator.ofFloat(sunMoonView , "rotation",-20,20);
rotateAnimator.setRepeatCount(1);
rotateAnimator.setRepeatMode(ObjectAnimator.REVERSE);
downAnimatorSet = new AnimatorSet();
downAnimatorSet.playTogether(bottomShiftAnimator , rotateAnimator);
downAnimatorSet.setDuration(3000);
downAnimatorSet.setInterpolator(new DecelerateInterpolator());
downAnimatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
postDelayed(new Runnable() {
@Override
public void run() {
sunRise();
}
} , 1000);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
downAnimatorSet.start();
starHide();
}
/**
* 4.太阳升起
* 结束后重新执行onFinishInflated()方法
*/
private void sunRise(){
sunMoonView.changeShape();
ObjectAnimator upShiftAnimator = ObjectAnimator.ofFloat(sunMoonView,"translationY",
shiftDistance,0);
upShiftAnimator.setDuration(3000);
upShiftAnimator.setInterpolator(new AccelerateInterpolator());
upShiftAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
postDelayed(new Runnable() {
@Override
public void run() {
//重写执行整个动效过程
onFinishInflate();
}
} , 1000);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
upShiftAnimator.start();
}
}
动效使用
在需要引用的布局文件中引用相应的类,如下示例
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
tools:context=".MainActivity">
<!--引用的加载中动效-->
<com.guo.loadinganim.LoadingView
android:layout_width="300dp"
android:layout_height="300dp"
android:background="@color/gray"/>
</LinearLayout>
源码位置
https://gitee.com/com_mailanglidegezhe/loading-view2.git