今天研究了一下android-shapeLoadingView,的源码,发现其关于view的绘制实现起来比较复杂,就自己写了一个简化版,主要用到的是属性动画,先看效果:
先说下实现思路,上面不断变化形状的view是一个自定义的LoadingView每次不断变换重新绘制新的形状,整个效果是一个自定义的layout,加载了一个布局,在该布局当中引入了LoadingView,底部的阴影是一个image,通过shape绘制一个椭圆,整个效果通过属性动画不断改变view的大小和位置。下面看代码:
这里自定义的loadingView主要是根据当前图形的状态不断绘制新的图形。仅此而已,对于当前loadingView位置的改变则交给父布局使用属性动画来维护。
private Paint mPaint;
private Shape mCurrentShape = Shape.RECT; //第一次绘制矩形
private Path mPath;
//定义一个枚举,用来标识当前需要绘制的形状类型
public enum Shape{
CIRCLE,
RECT,
RACTANGLE
}
由于LoadingView的功能比较简单,即根据当前图形的状态,绘制新的图形,所以属性也是比较少的。可以看到,这里我主要使用一个Shape枚举类型定义三种图形状态,并将初始值设置为矩形。
public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//一些初始化工作
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//使用switch来判断当前需要绘制图形的形状
switch (mCurrentShape) {
case RECT:
mPaint.setColor(Color.parseColor("#FF9999"));
canvas.drawRect(0,0,getWidth(),getHeight(), mPaint);
break;
case CIRCLE:
int circleRadius = Math.min(getWidth(), getHeight()) / 2;
mPaint.setColor(Color.parseColor("#99FFCC"));
canvas.drawCircle(getWidth() / 2, getHeight() / 2 ,circleRadius, mPaint);
break;
case RACTANGLE:
mPaint.setColor(Color.parseColor("#99CCFF"));
mPath.reset();
mPath.moveTo(getWidth() / 2,0);
mPath.lineTo(0,getHeight());
mPath.lineTo(getWidth(), getHeight());
mPath.close();
canvas.drawPath(mPath, mPaint);
break;
default:
break;
}
}
这里我根据当前view需要的图形状态来绘制不同的形状,比较简单。
/** * 改变当前需要绘制的形状,该接口是提供给外边调用的 * @param currentShape 当前已经绘制的形状 */
public void changeShape(Shape currentShape) {
if (currentShape == Shape.RECT) {
mCurrentShape = Shape.CIRCLE;
} else if (currentShape == Shape.CIRCLE) {
mCurrentShape = Shape.RACTANGLE;
} else if (currentShape == Shape.RACTANGLE) {
mCurrentShape = Shape.RECT;
}
//更改完成图形的形状之后,记得重绘
invalidate();
}
/** * 获得当前绘制的形状 * @return */
public Shape getCurrentShape() {
return mCurrentShape;
}
这两个接口是为当前LoadingView的父布局提供的,getCurrentShape可以获取当前的图形状态,changeShape用来重新设置下一个图形状态。
这里我新建一个布局,等下会在自定义的layout中加载。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:minWidth="260dp" android:orientation="vertical" >
<ImageView android:id="@+id/bottom_shadow" android:layout_width="23dp" android:layout_height="5dp" android:layout_centerHorizontal="true" android:layout_marginTop="82dp" android:src="@drawable/show" />
<TextView android:id="@+id/bottom_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/bottom_shadow" android:layout_centerHorizontal="true" android:layout_marginTop="18dp" android:minWidth="44dp" android:text="loading...." android:textColor="#757575" android:textSize="14sp" />
<com.example.selfloading.LoadingView android:id="@+id/shapeLoadingView" android:layout_width="18dp" android:layout_height="18dp" android:layout_centerHorizontal="true" />
</RelativeLayout>
这里将之前自定义的LoadingView放入到该布局中的顶部,bottom_shadow是一个Image类型,drawble/show如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" >
<solid android:color="@color/shadow"/>
</shape>
可以看到就是一个椭圆。
ContainLoadViewLayout主要是利用属性动画,实现整个效果的,LoadingView只是用来绘制不同的图形而已。
@Override
protected void onFinishInflate() {
super.onFinishInflate();
View view = LayoutInflater.from(getContext()).inflate(R.layout.loading,null);
mLoadingView = (LoadingView) view.findViewById(R.id.shapeLoadingView); //当前正在绘制的图形
mOval = (ImageView) view.findViewById(R.id.bottom_shadow); //用来显示底部的阴影
addView(view);
mFallDistance = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 70, getContext().getResources()
.getDisplayMetrics());
//加载完成布局结束以后启动下落的动画
startFall();
}
这里首先获得需要改变属性值的两个view,一个是LoadingView,一个是底部的阴影(ImageView), 然后将整个布局加入到该Layout当中。
mFallDistance 表示下落的距离。在当前布局加载完成之后,开始下落的属性动画,利用利用AnimatorListenerAdapter监听下落的属性动画完成之后,开始上升的属性动画。
/** * 下落的动画 */
public void startFall() {
ObjectAnimator rectAnimator = ObjectAnimator.ofFloat(mLoadingView, "translationY", mFallDistance,0);
ObjectAnimator ovalAnimator = ObjectAnimator.ofFloat(mOval, "scaleX", 1.0f,0.3f);
rectAnimator.setInterpolator(new DecelerateInterpolator());
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(1000);
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
startUp();
}
});
animatorSet.play(rectAnimator).with(ovalAnimator);
animatorSet.start();
}
/** * 弹跳起来的动画 */
public void startUp() {
ObjectAnimator rectUpAnimator = ObjectAnimator.ofFloat(mLoadingView, "translationY", 0,mFallDistance);
ObjectAnimator rectRotateAnimator = ObjectAnimator.ofFloat(mLoadingView, "rotation", 0,180);
if (mLoadingView.getCurrentShape() == Shape.RACTANGLE) {//如果是三角形,反向旋转
rectRotateAnimator = ObjectAnimator.ofFloat(mLoadingView, "rotation", 0,-180);
}
ObjectAnimator ovalAnimator = ObjectAnimator.ofFloat(mOval, "scaleX", 0.3f,1.0f);
rectUpAnimator.setInterpolator(new AccelerateInterpolator());
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setDuration(1000);
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//在弹跳动画完成之后,调用changeShape,该方法会重新设置当前需要绘制的图形,并重绘该图形
mLoadingView.changeShape(mLoadingView.getCurrentShape());
//交错执行上升和下落的动画
startFall();
}
});
animatorSet.play(rectUpAnimator).with(rectRotateAnimator).with(ovalAnimator);
animatorSet.start();
}
可以看到这里我使用AnimatorListenerAdapter监听动画结束,然后交替执行下落和上升的动画。在下落完成的时候,重新为LoadingView设置需要绘制的图形状态。
整个代码实现起来还是比较简单的,今天就到这里了,希望大家喜欢。
源码下载