一直對貝塞爾曲線的效果感興趣, 然后前一陣子看到同事寫的一個貝塞爾曲線做的動畫loading, 我也學著寫了一下.
我們先看一下效果圖。我覺得在加載時使用一個這個動畫還是很不錯的伦籍。具體大小和速度可以自己配置瘾晃。
BesselLoadingView是一個貝塞爾曲線效果的加載過渡動畫贷痪。使用canvas繪制的自定義view。
引入
project's build.gradle (工程下的 build.gradle)
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
module's build.gradle (模塊的build.gradle)
dependencies {
compile 'com.github.Jerey-Jobs:BesselLoadingView:1.1'
}
Usage/用法
上圖效果的layout是這樣的.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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"
tools:context="com.jerey.besselloadingview.MainActivity">
<com.jerey.besselloadingviewlib.BesselLoadingView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:loadingduration="4000"
app:loadingcolor="#555555"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.2"/>
<com.jerey.besselloadingviewlib.BesselLoadingView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.4"/>
<com.jerey.besselloadingviewlib.BesselLoadingView
android:layout_width="400dp"
android:layout_height="150dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.6"/>
</android.support.constraint.ConstraintLayout>
編寫過程
首先我們提供三個可配置選項,分別是顏色,動畫周期時長,圓的半徑(半徑同時會根據(jù)設置的大小變化)
<declare-styleable name="BesselLoadingView">
<attr name="loadingradius" format="dimension"></attr>
<attr name="loadingcolor" format="color"></attr>
<attr name="loadingduration" format="integer"></attr>
</declare-styleable>
與一般自定義View一樣,我們在構造方法中獲取自定義的幾個屬性,
public BesselLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initUI(context, attrs);
}
private void initUI(Context context, AttributeSet attrs) {
mPaint = new Paint();
//路徑
mPath = new Path();
mCirclesX = new int[3];
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.BesselLoadingView);
mLoadingColor = ta.getColor(R.styleable.BesselLoadingView_loadingcolor, DEFAULT_COLOR);
mRadius = ta.getDimension(R.styleable.BesselLoadingView_loadingradius, DEFAULT_RADIUS);
mDuration = ta.getInt(R.styleable.BesselLoadingView_loadingduration, DEFAULT_DURATION);
mPaint.setColor(mLoadingColor);
mPaint.setAntiAlias(true); //抗鋸齒
mRadiusFloat = mRadius * 0.9f;
}
onMeasure
構造方法里面我們只初始化了一些必要的配置參數(shù), 但是我們的圓與圓之間的距離啊什么的還沒初始化,我們在onMeasure中進行初始化一些大小的參數(shù)
我做了比如配置的半徑比實際的上下高度還要大的情況下自動縮小啊,等一系列自適應操作.并且默認為
android:layout_width="wrap_content"
android:layout_height="wrap_content"
時,默認大小為480像素寬,100像素高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mWidth;
int mHeight;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
mWidth = widthSize;
} else {
mWidth = getPaddingLeft() + 480 + getPaddingRight();
}
if (heightMode == MeasureSpec.EXACTLY) {
mHeight = heightSize;
} else {
mHeight = getPaddingTop() + 100 + getPaddingBottom();
}
setMeasuredDimension(mWidth, mHeight);
log("width: " + mWidth + " h: " + mHeight);
//計算x方向三個圓心 -.-.-.-
int lenth = mWidth / 4;
for (int i = 0; i < 3; i++) {
mCirclesX[i] = lenth * (i + 1);
}
//計算三個圓心Y坐標
mCirClesY = mHeight / 2;
//三個初始圓的半徑
mRadius = mHeight / 3;
mRadiusFloat = mRadius * 0.9f;
log("mCirclesX: " + mCirclesX[0] + "," + mCirclesX[1] + "," + mCirclesX[2] + " Y: " + mCirClesY);
if (mRadius >= lenth / 4) {
log("圓的半徑大于間隙了,自動縮小");
mRadius = lenth / 4;
mRadiusFloat = mRadius * 0.9f;
}
mMinDistance = lenth;
log("mMinDistance " + mMinDistance);
ValueAnimator valueAnimator = ValueAnimator.ofFloat(mRadius, mWidth - mRadius);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setDuration(mDuration);
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mFloatX = (float) animation.getAnimatedValue();
postInvalidate();
}
});
valueAnimator.start();
}
onDraw
接下來重點來了, 我們需要畫幾個東西, 三個固定圓, 一個浮動圓, 貝塞爾曲線, 固定圓半徑變化.
在繪制貝塞爾曲線的時候,需要先計算浮動圓離哪個固定圓最近,然后繪制相聚最近的圓的貝塞爾曲線,再計算距離,計算出圓應該變化多大.
@Override
protected void onDraw(Canvas canvas) {
//畫三個圓
for (int i = 0; i < 3; i++) {
canvas.drawCircle(mCirclesX[i], mCirClesY, mRadius, mPaint);
}
//畫滑動圓
canvas.drawCircle(mFloatX, mCirClesY, mRadiusFloat, mPaint);
drawBesselLine(canvas);
}
/**
* 繪制貝塞爾曲線與定點圓變大
*
* @param canvas
*/
private void drawBesselLine(Canvas canvas) {
float minDis = mMinDistance;
int minLocation = 0;
for (int i = 0; i < 3; i++) {
float dis = Math.abs((mFloatX - mCirclesX[i]));
if (dis < minDis) {
minDis = dis;
minLocation = i;
}
}
// log("最小距離為 " + minDis + "位置:" + minLocation);
if (minDis < mMinDistance) {
float middleX = (mCirclesX[minLocation] + mFloatX) / 2;
//繪制上半部分貝塞爾曲線
mPath.moveTo(mCirclesX[minLocation], mCirClesY + mRadius);
mPath.quadTo(middleX, mCirClesY, mFloatX, mCirClesY + mRadiusFloat);
mPath.lineTo(mFloatX, mCirClesY - mRadiusFloat);
mPath.quadTo(middleX, mCirClesY, mCirclesX[minLocation], mCirClesY - mRadius);
mPath.lineTo(mCirclesX[minLocation], mCirClesY + mRadius);
mPath.close();
canvas.drawPath(mPath, mPaint);
mPath.reset();
//浮動圓靠近固定圓變大
float f = 1 + (mMinDistance - minDis * 2) / mMinDistance * 0.2f;
log("dis% : " + (mMinDistance - minDis) / mMinDistance + " f = " + f);
canvas.drawCircle(mCirclesX[minLocation], mCirClesY, mRadius * f, mPaint);
}
}
至此蹦误,我們的自定義View便OK了劫拢, 若有幫助,歡迎star
具體代碼見 https://github.com/Jerey-Jobs/BesselLoadingView
老鐵們强胰,來波Star舱沧。
本文作者:Anderson/Jerey_Jobs
博客地址 : 夏敏的博客/Anderson大碼渣/Jerey_Jobs
簡書地址 : Anderson大碼渣
github地址 : Jerey_Jobs