一、登陸效果展現html
二、關注效果展現java
畫圖首先是onDraw方法(我會把圓代碼寫上,一步一步剖析):首先在view中定義個屬性:private RectF rectf = new RectF();//能夠理解爲,裝載控件按鈕的區域面試
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;canvas
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { height = measuredHeight(heightMeasureSpec); //這裏是測量控件大小 width = measureWidth(widthMeasureSpec); //咱們常常能夠看到咱們設置控件wrap_content,match_content或者固定值 setMeasuredDimension(width, height); }
下面以measureWidth爲例:微信
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) {}), //繪製文字的路徑(文字過長時,文字來回滾動須要用到)架構
private Path textPath = new Path():app
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="文案內容"。在自定義控件裏獲取以下:ide
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; } }
爲了點擊事件的直觀,也能夠把處理防止重複點擊事件封裝在裏面佈局
//這是我自定義登陸點擊的接口 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(); } } } }); }
矩形到正方形(爲了簡化,我把源碼一些其餘屬性去掉了,這樣方便理解)post
//其中 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(); } });
矩形到圓角矩形。就是從一個沒有圓角的變成徹底圓角的矩形,固然我展現的時候只有第三個圖,最後一個按鈕才明顯了。
其餘的我直接設置成了圓角按鈕,由於我把圓角作成了一個屬性。
還記得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 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裏參數的類型啓動不一樣效果:
一、啓動擴散全屏動畫 public void loginSuccess(Animator.AnimatorListener endListener) {} 二、啓動打勾動畫 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; }
由於項目是把以前的功能寫成了控件,因此有不少地方不完善。但願有建議的大牛和小夥伴,提示提示我,讓我完善的更好。謝謝