一、登陸效果展現php
二、關注效果展現畫圖首先是onDraw方法(我會把圓代碼寫上,一步一步剖析): 首先在view中定義個屬性:private RectF rectf = new RectF();//能夠理解爲,裝載控件按鈕的區域java
rectf.left = current_left;
rectf.top = 0; //(這2點肯定空間區域左上角,current_left,是爲了後面動畫矩形變成等邊矩形準備的,這裏你能夠當作0)
rectf.right = width - current_left;
rectf.bottom = height; //(經過改變current_left大小,更新繪製,就會實現了動畫效果)
//畫圓角矩形
//參數1:區域
//參數2,3:圓角矩形的圓角,其實就是矩形圓角的半徑
//參數4:畫筆
canvas.drawRoundRect(rectf, circleAngle, circleAngle, paint);
複製代碼
上面是畫圓角,那width和height怎麼來呢固然是經過onMeasure;git
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
height = measuredHeight(heightMeasureSpec); //這裏是測量控件大小
width = measureWidth(widthMeasureSpec); //咱們常常能夠看到咱們設置控件wrap_content,match_content或者固定值
setMeasuredDimension(width, height);
}
複製代碼
下面以measureWidth爲例:github
private int measureWidth(int widthMeasureSpec) {
int result;
int specMode = MeasureSpec.getMode(widthMeasureSpec);
int specSize = MeasureSpec.getSize(widthMeasureSpec);
//這裏是精準模式,好比match_content,或者是你控件裏寫明瞭控件大小
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
//這裏是wrap_content模式,其實這裏就是給一個默認值
//下面這段註銷代碼是最開始若是用戶不設置大小,給他一個默認固定值。這裏以字體長度來決定更合理
//result = (int) getContext().getResources().getDimension(R.dimen.dp_150);
//這裏是我設置的長度,固然你寫自定義控件能夠設置你想要的邏輯,根據你的實際狀況
result = buttonString.length() * textSize + height * 5 / 3;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
複製代碼
這裏我是用本身的方式實現:當文字長度超過控件長度時,文字須要來回滾動。因此自定義控件由於你須要什麼樣的功能能夠本身去實現(固然這個方法也是在onDraw裏,爲何這麼個順序講,目的但願我但願你能按部就班的理解,若是你以爲onDraw方代碼太雜,你能夠用個方法獨立出去,你能夠跟做者同樣用private void drawText(Canvas canvas) {}), //繪製文字的路徑(文字過長時,文字來回滾動須要用到)canvas
private Path textPath = new Path():微信
textRect.left = 0;
textRect.top = 0;
textRect.right = width;
textRect.bottom = height; //這裏肯定文字繪製區域,其實就是控件區域
Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
//這裏是獲取文字繪製的y軸位置,能夠理解上下居中
int baseline = (textRect.bottom + textRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
//這裏判斷文字長度是否大於控件長度,固然我控件2邊須要留文字的間距,因此不是大於width,這麼說只是更好的理解
//這裏是當文字內容大於控件長度,啓動回滾效果。建議先看下面else裏的正常狀況
if ((buttonString.length() * textSize) > (width - height * 5 / 3)) {
textPath.reset();
//由於要留2遍間距,以heigh/3爲間距
textPath.moveTo(height / 3, baseline);
textPath.lineTo(width - height / 3, baseline);
//這裏的意思是文字從哪裏開始寫,能夠是居中,這裏是右邊
textPaint.setTextAlign(Paint.Align.RIGHT);
//這裏是以路徑繪製文字,scrollSize能夠理解爲文字在x軸上的便宜量,同時,個人混動效果就是經過改變scrollSize
//刷新繪製來實現
canvas.drawTextOnPath(buttonString, textPath, scrollSize, 0, textPaint);
if (isShowLongText) {
//這裏是繪製遮擋物,由於繪製路徑沒有間距這方法,因此繪製遮擋物相似於間距方式
canvas.drawRect(new Rect(width - height / 2 - textSize / 3, 0, width - height / 2, height),paintOval);
canvas.drawRect(new Rect(height / 2, 0, height / 2 + textSize / 3, height), paintOval);
//這裏有個bug 有個小點-5 因畫筆粗細產生
canvas.drawArc(new RectF(width - height, 0, width - 5, height), -90, 180, true, paintOval);
canvas.drawArc(new RectF(0, 0, height, height), 90, 180, true, paintOval);
}
if (animator_text_scroll == null) {
//這裏是計算混到最右邊和最左邊的距離範圍
animator_text_scroll = ValueAnimator.ofInt(buttonString.length() * textSize - width + height * 2 / 3,-textSize);
//這裏是動畫的時間,scrollSpeed能夠理解爲每一個文字滾動控件外所需的時間,能夠作成控件屬性提供出去
animator_text_scroll.setDuration(buttonString.length() * scrollSpeed);
//設置動畫的模式,這裏是來回滾動
animator_text_scroll.setRepeatMode(ValueAnimator.REVERSE);
//設置插值器,讓整個動畫流暢
animator_text_scroll.setInterpolator(new LinearInterpolator());
//這裏是滾動次數,-1無限滾動
animator_text_scroll.setRepeatCount(-1);
animator_text_scroll.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//改變文字路徑x軸的偏移量
scrollSize = (int) animation.getAnimatedValue();
postInvalidate();
}
});
animator_text_scroll.start();
}
} else {
//這裏是正常狀況,isShowLongText,是我在啓動控件動畫的時候,是否啓動 文字有漸變效果的標識,
//若是是長文字,啓動漸變效果的話,若是控件變小,文字內容在當前控件外,會顯得很難看,因此根據這個標識,關閉,這裏你能夠先忽略(同時由於根據路徑繪製text不能有間距效果,這個標識仍是判斷是否在控件2遍繪製遮擋物,這是做者的解決方式,若是你有更好的方式能夠在下方留言)
isShowLongText = false;
/** * 簡單的繪製文字,沒有考慮文字長度超過控件長度 * */
//這裏是居中顯示
textPaint.setTextAlign(Paint.Align.CENTER);
//參數1:文字
//參數2,3:繪製文字的中心點
//參數4:畫筆
canvas.drawText(buttonString, textRect.centerX(), baseline, textPaint);
}
複製代碼
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SmartLoadingView">
<attr name="textStr" format="string" />
<attr name="errorStr" format="string" />
<attr name="cannotclickBg" format="color" />
<attr name="errorBg" format="color" />
<attr name="normalBg" format="color" />
<attr name="cornerRaius" format="dimension" />
<attr name="textColor" format="color" />
<attr name="textSize" format="dimension" />
<attr name="scrollSpeed" format="integer" />
</declare-styleable>
</resources>
複製代碼
這裏以,文案爲例, textStr。好比你再佈局種用到app:txtStr="文案內容"。在自定義控件裏獲取以下:app
public SmartLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//自定義控件的3參方法的attrs就是咱們設置自定義屬性的關鍵
//好比咱們再attrs.xml裏自定義了咱們的屬性,
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.SmartLoadingView);
//這裏是獲取用戶有沒有設置整個屬性
//這裏是從用戶那裏獲取有沒有設置文案
String title = typedArray.getString(R.styleable.SmartLoadingView_textStr);
if (TextUtils.isEmpty(title)){
//若是獲取來的屬性是空,那麼能夠默認一個屬性
//(做者忘記設置了!由於已經發布後期優化,老尷尬了)
buttonString ="默認文案";
}else{
//若是有設置文案
buttonString = title;
}
}
複製代碼
爲了點擊事件的直觀,也能夠把處理防止重複點擊事件封裝在裏面ide
//這是我自定義登陸點擊的接口
public interface LoginClickListener {
void click();
}
public void setLoginClickListener(final LoginClickListener loginClickListener) {
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (loginClickListener != null) {
//防止重複點擊
if (!isAnimRuning) {
start();
loginClickListener.click();
}
}
}
});
}
複製代碼
矩形到正方形(爲了簡化,我把源碼一些其餘屬性去掉了,這樣方便理解)佈局
//其中 default_all_distance = (w - h) / 2;除以2是由於2遍都往中間縮短
private void set_rect_to_circle_animation() {
//這是一個屬性動畫,current_left 會在duration時間內,從0到default_all_distance勻速變化
//想添加多樣化的話 還能夠加入插值器。
animator_rect_to_square = ValueAnimator.ofInt(0, default_all_distance);
animator_rect_to_square.setDuration(duration);
animator_rect_to_square.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//這裏的current_left跟onDraw相關,還記得嗎
//onDraw裏的控件區域
//控件左邊區域 rectf.left = current_left;
//控件右邊區域 rectf.right = width - current_left;
current_left = (int) animation.getAnimatedValue();
//刷新繪製
invalidate();
}
});
複製代碼
矩形到圓角矩形。就是從一個沒有圓角的變成徹底圓角的矩形,固然我展現的時候只有第三個圖,最後一個按鈕才明顯了。post
其餘的我直接設置成了圓角按鈕,由於我把圓角作成了一個屬性。
還記得onDraw裏的canvas.drawRoundRect(rectf, circleAngle, circleAngle, paint);circleAngle就是圓角的半徑
能夠想象一下若是全是圓角,那麼circleAngle會是多少,固然是height/2;沒錯吧,因此
由於我把圓角作成了屬性obtainCircleAngle是從xml文件獲取的屬性,若是不設置,則爲0,就沒有任何圓角效果
animator_rect_to_angle = ValueAnimator.ofInt(obtainCircleAngle, height / 2);
animator_rect_to_angle.setDuration(duration);
animator_rect_to_angle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//這裏試想下若是是一個正方形,恰好是圓形的圓角,那就是一個圓
circleAngle = (int) animation.getAnimatedValue();
//刷新繪畫
invalidate();
}
});
複製代碼
2個屬性動畫作好後,用 private AnimatorSet animatorSet = new AnimatorSet();把屬性動畫加進去,能夠設置2個動畫同時進行,仍是前後順序 這裏是同時進行所用用with
animatorSet
.play(animator_rect_to_square).with(animator_rect_to_angle);
複製代碼
這裏就是畫圓弧,只是不斷改變,圓弧的起始點和終點,最終呈現loading狀態,也是在onDraw裏
//繪製加載進度
if (isLoading) {
//參數1:繪製圓弧區域
//參數2,3:繪製圓弧起始點和終點
canvas.drawArc(new RectF(width / 2 - height / 2 + height / 4, height / 4, width / 2 + height / 2 - height / 4, height / 2 + height / 2 - height / 4), startAngle, progAngle, false, okPaint);
//這裏是我經過實踐,實現最佳loading動畫
//固然這裏有不少方式,由於我自定義這個view想把全部東西都放在這個類裏面,你也能夠有你的方式
//若是有更好的方式,歡迎留言,告知我一下
startAngle += 6;
if (progAngle >= 270) {
progAngle -= 2;
isAdd = false;
} else if (progAngle <= 45) {
progAngle += 6;
isAdd = true;
} else {
if (isAdd) {
progAngle += 6;
} else {
progAngle -= 2;
}
}
//刷新繪製,這裏不用擔憂有那麼多刷新繪製,會不會影響性能
//
postInvalidate();
}
複製代碼
那麼這裏首先要把loading動畫取消,那麼直接改變isLoading=false;不會只它同時啓動打勾動畫;打勾動畫的動畫,這裏比較麻煩,也是我在別人自定義動畫裏學習的,經過PathMeasure,實現路徑動畫
/** * 路徑--用來獲取對勾的路徑 */
private Path path = new Path();
/** * 取路徑的長度 */
private PathMeasure pathMeasure;
複製代碼
//初始化打勾動畫路徑;
private void initOk() {
//對勾的路徑
path.moveTo(default_all_distance + height / 8 * 3, height / 2);
path.lineTo(default_all_distance + height / 2, height / 5 * 3);
path.lineTo(default_all_distance + height / 3 * 2, height / 5 * 2);
pathMeasure = new PathMeasure(path, true);
}
複製代碼
//初始化打勾動畫
private void set_draw_ok_animation() {
animator_draw_ok = ValueAnimator.ofFloat(1, 0);
animator_draw_ok.setDuration(duration);
animator_draw_ok.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
startDrawOk = true;
isLoading = false;
float value = (Float) animation.getAnimatedValue();
effect = new DashPathEffect(new float[]{pathMeasure.getLength(), pathMeasure.getLength()}, value * pathMeasure.getLength());
okPaint.setPathEffect(effect);
invalidate();
}
});
}
//啓動打勾動畫只須要調用
animator_draw_ok.start();
複製代碼
onDraw裏繪製打勾動畫
//繪製打勾,這是onDraw的,startDrawOk是判斷是否開啓打勾動畫的標識
if (startDrawOk) {
canvas.drawPath(path, okPaint);
}
複製代碼
以前6.1提到了矩形到圓角矩形和矩形到正方形的動畫,
那麼這裏只是前面2個動畫反過來,再加上聯網失敗的文案,和聯網失敗的背景圖即刻
這裏我經過loginSuccess裏參數的類型啓動不一樣效果:
1、啓動擴散全屏動畫
public void loginSuccess(Animator.AnimatorListener endListener) {}
2、啓動打勾動畫
public void loginSuccess(AnimationOKListener animationOKListener) {}
複製代碼
啓動擴散全屏是本文的重點,裏面還涉及到了一個自定義view
CirclBigView,這個控件是全屏的,並且是從一個小圓不斷改變半徑變成大圓的動畫,那麼有人會問,全屏確定很差啊,會影響佈局,
可是這裏,我把它放在了activity的視圖層:
ViewGroup activityDecorView = (ViewGroup) ((Activity) getContext()).getWindow().getDecorView();
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
activityDecorView.addView(circlBigView, layoutParams);
複製代碼
這個靈感也是前不久在學習微信,拖拽退出的思路里發現的。所有代碼以下:
public void toBigCircle(Animator.AnimatorListener endListener) {
//把縮小到圓的半徑,告訴circlBigView
circlBigView.setRadius(this.getMeasuredHeight() / 2);
//把當前背景顏色告訴circlBigView
circlBigView.setColorBg(normal_color);
int[] location = new int[2];
//測量當前控件所在的屏幕座標x,y
this.getLocationOnScreen(location);
//把當前座標告訴circlBigView,同時circlBigView會計算當前點,到屏幕4個點的最大距離,便是當前控件要擴散到的半徑
//具體建議讀者看完本博客後,去下載玩耍下。
circlBigView.setXY(location[0] + this.getMeasuredWidth() / 2, location[1]);
if (circlBigView.getParent() == null) {
ViewGroup activityDecorView = (ViewGroup) ((Activity) getContext()).getWindow().getDecorView();
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
activityDecorView.addView(circlBigView, layoutParams);
}
circlBigView.startShowAni(endListener);
isAnimRuning = false;
}
複製代碼