a亚洲精品_精品国产91乱码一区二区三区_亚洲精品在线免费观看视频_欧美日韩亚洲国产综合_久久久久久久久久久成人_在线区

首頁(yè) > 系統(tǒng) > Android > 正文

Android自定義View仿騰訊TIM下拉刷新View

2019-10-21 21:30:26
字體:
來(lái)源:轉(zhuǎn)載
供稿:網(wǎng)友

一 概述

自定義 ViewAndroid 開(kāi)發(fā)里面的一個(gè)大學(xué)問(wèn)。偶然間看到 TIM 郵箱界面的刷新 View 還挺好玩的,于是就自己動(dòng)手實(shí)現(xiàn)了一個(gè),先看看 TIM 里邊的效果圖:

Android,View,騰訊TIM,下拉刷新

二 需求分析

看到上面的動(dòng)圖,大概也知道我們需要實(shí)現(xiàn)的功能:

  • 根據(jù)拖動(dòng)的進(jìn)度來(lái)移動(dòng)小球的位置
  • 小球移動(dòng)過(guò)程的動(dòng)畫(huà)

三 功能實(shí)現(xiàn)

新建一個(gè) RefreshView 類(lèi)繼承自 View ,然后我們?cè)僭?RefreshView 里面新建一個(gè)內(nèi)部實(shí)體類(lèi): Circle

來(lái)看一下 Circle類(lèi)的代碼

#Cirlce.java

 class Circle { int x; int y; int r; int color; public Circle(int x, int y, int r, int color) {  this.x = x;  this.y = y;  this.r = r;  this.color = color; } }

這是一個(gè)實(shí)體類(lèi),里面提供了 x , y , r , color 屬性分別代表圓心坐標(biāo)的 x值,y值,圓的半徑 r 跟顏色。
借助此類(lèi)來(lái)存儲(chǔ)小圓球的相關(guān)屬性。

接下來(lái)就是我們平時(shí)自定義 View 經(jīng)常要重寫(xiě)的三大方法了,先看 onMeasure()

#RefreshView.java

 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.EXACTLY) {  setMeasuredDimension(mWidth, heightSize); } else if (widthMeasureSpec == MeasureSpec.EXACTLY && heightMeasureSpec == MeasureSpec.AT_MOST) {  setMeasuredDimension(widthSize, mHeight); } else if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {  setMeasuredDimension(widthSize, heightSize); } else {  setMeasuredDimension(mWidth, mHeight); } }

為了適配布局文件中的 wrap_content 參數(shù),我們需要重寫(xiě)此方法(此方法不是本文的研究重點(diǎn),不明白的可以百度或者google一下,或者參考《Android開(kāi)發(fā)藝術(shù)探索》里面的相關(guān)章節(jié))。

接著看 onLayout() 方法:

#RefreshView.java

 @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); initContentAttr(getMeasuredWidth(), getMeasuredHeight()); resetCircles(); }

在此方法中調(diào)用了 initContentAttr() 方法來(lái)初始化內(nèi)容大小與 resetCircles() 來(lái)初始化(重置)三個(gè)小球的屬性。分別看下這兩個(gè)方法:

#RefreshView.java

 private void initContentAttr(int width, int height) { mContentWidth = width - getPaddingLeft() - getPaddingRight(); mContentHeight = height - getPaddingTop() - getPaddingBottom(); }

這方法很簡(jiǎn)單,就是進(jìn)行了 padding 的處理,得出真正的布局大小。如果不處理 padding 的話那么用戶(hù)設(shè)置了 padding 將失效。再看 resetCircles():

#RefreshView.java

 public static final int STATE_ORIGIN = 0; public static final int STATE_PREPARED = 1; private int mOriginState = STATE_ORIGIN; private void resetCircles() { if (mCircles.isEmpty()) {  int x = mContentWidth / 2;  int y = mContentHeight / 2;  mGap = x - mMinRadius; //初始化相鄰圓心間的最大間距  Circle circleLeft = new Circle(x, y, mMinRadius, 0xffff7f0a);  Circle circleCenter = new Circle(x, y, mMaxRadius, Color.RED);  Circle circleRight = new Circle(x, y, mMinRadius, Color.GREEN);  mCircles.add(LEFT, circleLeft);  mCircles.add(RIGHT, circleRight);  mCircles.add(CENTER, circleCenter); } if (mOriginState == STATE_ORIGIN) {  int x = mContentWidth / 2;  int y = mContentHeight / 2;  for (int i = 0; i < mCircles.size(); i++) {  Circle circle = mCircles.get(i);  circle.x = x;  circle.y = y;  if (i == CENTER) {   circle.r = mMaxRadius;  } else {   circle.r = mMinRadius;  }  } } else {  prepareToStart(); } }

此方法用于初始化和重置小球,方法里面進(jìn)行的兩個(gè)大的 if...else 語(yǔ)句判斷,第一個(gè) if 用于判斷是否應(yīng)該初始化小球,第二個(gè)語(yǔ)句則是用于判斷小球的初始化時(shí)候的形態(tài)。可以在外部調(diào)用 setOriginState() 方法來(lái)指定小球的初始化形態(tài),如不指定,則默認(rèn)為 NOMAL,即三球重合。

#RefreshView.java

 /** * 設(shè)置圓球初始狀態(tài) * {@link #STATE_ORIGIN}為原始狀態(tài)(三個(gè)小球重合), * {@link #STATE_PREPARED}為準(zhǔn)備好可以刷新的狀態(tài),三個(gè)小球間距最大 */ public void setOriginState(int state) { if (state == 0) {  mOriginState = STATE_ORIGIN; } else {  mOriginState = STATE_PREPARED; } }

最后就是最有趣的方法 onDraw() 了:

#RefreshView.java

 @Override protected void onDraw(Canvas canvas) { for (Circle circle : mCircles) {  mPaint.setColor(circle.color);  canvas.drawCircle(circle.x + getPaddingLeft(), circle.y + getPaddingTop(), circle.r, mPaint); } }

這方法很簡(jiǎn)單,就是將 mCircles 列表里面的圓畫(huà)出來(lái)而已(里面進(jìn)行了 padding 的處理)。

三大方法都講完了,可是這只是畫(huà)出了幾個(gè)小圓球而已,我們需求分析里的需求還沒(méi)實(shí)現(xiàn)呢,上面的方法已經(jīng)把 View 的基礎(chǔ)搭起來(lái)了,要實(shí)現(xiàn)這個(gè)也就不難了。接下來(lái)就是大家期待的需求實(shí)現(xiàn)了:

根據(jù)拖動(dòng)的進(jìn)度來(lái)移動(dòng)小球的位置

實(shí)現(xiàn)代碼如下:

#RefreshView.java

 public void drag(float fraction) { if (mOriginState == STATE_PREPARED) {  return; } if (mAnimator != null && mAnimator.isRunning()) {  return; } if (fraction > 1) {  return; } mCircles.get(LEFT).x = (int) (mMinRadius + mGap * (1f - fraction)); mCircles.get(RIGHT).x = (int) (mContentWidth / 2 + mGap * fraction); postInvalidate(); }

在方法里面進(jìn)行三次判斷,如果初始狀態(tài)是 STATE_PREPARED (三小球距離最大,沒(méi)必要再變動(dòng)了)、動(dòng)畫(huà)正在進(jìn)行或者進(jìn)度大于1 都不進(jìn)行移動(dòng)。然后修改小球的屬性,再重繪。

小球移動(dòng)過(guò)程的動(dòng)畫(huà)

這個(gè)是這個(gè)自定義 View 最難的部分了,需要一些數(shù)學(xué)的小運(yùn)算,有點(diǎn)繁瑣。

我們先來(lái)理清實(shí)現(xiàn)動(dòng)畫(huà)的邏輯,看了開(kāi)篇的gif,應(yīng)該可以了解到,剛準(zhǔn)備開(kāi)始動(dòng)畫(huà)時(shí),左邊的小球應(yīng)該是處于最左端,中間的小球處于中間,右邊的處于最右端。我們一個(gè)個(gè)小球來(lái)分析。

  • 左邊小球:動(dòng)畫(huà)開(kāi)始后,左邊的小球向右移動(dòng),并且逐漸變大,直到小球運(yùn)動(dòng)到中點(diǎn),過(guò)了中點(diǎn)后小球繼續(xù)往右移動(dòng),不過(guò)卻逐漸變小,到了終點(diǎn)后小球?qū)⑾Вㄏн^(guò)程為先縮小再消失,下同),接著又從左邊出現(xiàn)(出現(xiàn)過(guò)程也是從小到大的漸變,下同),然后重復(fù)上述過(guò)程。
  • 中間小球:中間的小球先向右移動(dòng),逐漸縮小,然后消失,后來(lái)再?gòu)淖筮叧霈F(xiàn),最后移動(dòng)到中間,其間逐漸變大。后面就是重復(fù)的上述動(dòng)作。
  • 右邊小球:右邊的小球則是先消失,再?gòu)淖筮叧霈F(xiàn),接著移動(dòng)到中間,其間逐漸變大,然后再?gòu)闹悬c(diǎn)移動(dòng)到末端,其間逐漸縮小。

理清小球的移動(dòng)過(guò)程對(duì)代碼的實(shí)現(xiàn)很有幫助,我們可以分析出:

1)每個(gè)小球?qū)τ谧鴺?biāo)系的移動(dòng)特點(diǎn)是一樣的。

2)每個(gè)小球?qū)τ趧?dòng)畫(huà)的進(jìn)度的移動(dòng)特點(diǎn)是不一樣的。

聽(tīng)起來(lái)好像有點(diǎn)拗口,我們用人話來(lái)解釋一下:

1)每個(gè)小球?qū)τ谧鴺?biāo)系的移動(dòng)特點(diǎn)是一樣的:左邊的小球在坐標(biāo)的最左邊是先出現(xiàn),然后再向右移動(dòng),那么中間和右邊的小球呢?其實(shí)是同樣的,它們?cè)谧鴺?biāo)軸最左邊的時(shí)候都是先出現(xiàn),再向右移動(dòng),無(wú)論哪個(gè)小球,它們?cè)谧鴺?biāo)軸的同一點(diǎn)上的動(dòng)作和形態(tài)應(yīng)該是一致的。

2)每個(gè)小球?qū)τ趧?dòng)畫(huà)的進(jìn)度的移動(dòng)特點(diǎn)是不一樣的:左邊的小球在動(dòng)畫(huà)剛開(kāi)始時(shí)是處于最左端,而中間的小球卻在中間位置,右邊的則在最右端。當(dāng)動(dòng)畫(huà)開(kāi)始后,比如進(jìn)行了一半,這時(shí)候左邊的小球應(yīng)該移動(dòng)到了中點(diǎn)附近,而中間的確是在末端(消失),右邊的小球就會(huì)出現(xiàn)在中間附近。

按照上面分析的邏輯,我把動(dòng)畫(huà)的總進(jìn)度分為6份,為什么是6份呢?通過(guò)上面的動(dòng)畫(huà)分析,知道小球應(yīng)該經(jīng)歷一下過(guò)程(不分時(shí)間先后):

  • 出現(xiàn) (從無(wú)漸變到初始大?。?/li>
  • 從最左端移動(dòng)到中點(diǎn)(期間變大)
  • 從中點(diǎn)移動(dòng)到末端(期間縮小)
  • 消失 (從初始大小漸變到消失)

為了讓小球之間的間隔保持一個(gè)優(yōu)美的狀態(tài)(動(dòng)畫(huà)開(kāi)始后小球間不會(huì)重疊,相鄰小球的間隔基本一致),就把1、4出現(xiàn)和消失階段分別設(shè)為 1/6 的動(dòng)畫(huà)周期,中間2、3兩個(gè)階段分別占用 1/3 個(gè)動(dòng)畫(huà)周期。

Android,View,騰訊TIM,下拉刷新

這樣一來(lái),出現(xiàn)跟消失占用了 1/3 動(dòng)畫(huà)進(jìn)度,其他兩個(gè)部分分別占用了 1/3 動(dòng)畫(huà)進(jìn)度。舉個(gè)例子:剛開(kāi)始動(dòng)畫(huà)時(shí),設(shè)最左邊的小球?yàn)?1,中間的小球?yàn)?2,最右端的小球?yàn)?3 。

當(dāng) 小球1 移動(dòng)到中點(diǎn)時(shí),這時(shí)動(dòng)畫(huà)進(jìn)行了 1/3 ,那么此時(shí)的 小球2 就應(yīng)該移動(dòng)到末端,小球3 則剛好經(jīng)歷消失和出現(xiàn)過(guò)程,于是應(yīng)該出現(xiàn)于坐標(biāo)軸的起點(diǎn)。

由此可以看到又恢復(fù)到了剛開(kāi)始時(shí)候的情況(一個(gè)小球在最左,一個(gè)在中,一個(gè)在最右),只不過(guò)是顏色不同了而已。以此類(lèi)推,無(wú)限循環(huán),就可以形成優(yōu)美的動(dòng)畫(huà)了。

分析出這些有什么用呢?我發(fā)現(xiàn)用坐標(biāo)來(lái)確定小球的移動(dòng)實(shí)現(xiàn)起來(lái)會(huì)有點(diǎn)小問(wèn)題,所以就用動(dòng)畫(huà)的進(jìn)度來(lái)實(shí)現(xiàn),下面看具體實(shí)現(xiàn)。

需要實(shí)現(xiàn)小球的無(wú)限運(yùn)動(dòng),最實(shí)用的就是用動(dòng)畫(huà)來(lái)實(shí)現(xiàn),這里我用了屬性動(dòng)畫(huà)。先初始化 Animotor 類(lèi):

#RefreshView.java

 private void initAnimator() { ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(1500); animator.setRepeatCount(-1); animator.setRepeatMode(ValueAnimator.RESTART); animator.setInterpolator(new LinearInterpolator()); animator.addListener(new Animator.AnimatorListener() {  @Override  public void onAnimationStart(Animator animation) {  prepareToStart(); //確保View達(dá)到可以刷新的狀態(tài)  }  @Override  public void onAnimationEnd(Animator animation) {  }  @Override  public void onAnimationCancel(Animator animation) {  }  @Override  public void onAnimationRepeat(Animator animation) {  } }); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  @Override  public void onAnimationUpdate(ValueAnimator animation) {  for (Circle circle : mCircles) {   updateCircle(circle, mCircles.indexOf(circle), animation.getAnimatedFraction());  }  postInvalidate();  } }); mAnimator = animator; }

可以看到,這是一個(gè)無(wú)限循環(huán)的動(dòng)畫(huà),如果不手動(dòng)停止,它就會(huì)一直循環(huán)下去。對(duì)于 mAnimator ,還添加了一個(gè)監(jiān)聽(tīng)器,當(dāng)開(kāi)始動(dòng)畫(huà)是就調(diào)用 prepareToStart() 方法,這個(gè)方法看起來(lái)是不是有點(diǎn)眼熟,沒(méi)錯(cuò),它就是我們上面 resetCircles() 里面判斷小球形態(tài)為 STATE_PREPARED 是調(diào)用過(guò),此方法將確保小球達(dá)到刷新的臨界點(diǎn)。我們主要看看 UpdateLisener 中的 onAnimationUpdate() 方法里面的 updateCircle() 方法:

#RefreshView

 private void updateCircle(Circle circle, int index, float fraction) { float progress = fraction; //真實(shí)進(jìn)度 float virtualFraction; //每個(gè)小球內(nèi)部的虛擬進(jìn)度 switch (index) {  case LEFT:  if (fraction < 5f / 6f) {   progress = progress + 1f / 6f;  } else {   progress = progress - 5f / 6f;  }  break;  case CENTER:  if (fraction < 0.5f) {   progress = progress + 0.5f;  } else {   progress = progress - 0.5f;  }  break;  case RIGHT:  if (fraction < 1f / 6f) {   progress += 5f / 6f;  } else {   progress -= 1f / 6f;  }  break; } if (progress <= 1f / 6f) {  virtualFraction = progress * 6;  appear(circle, virtualFraction);  return; } if (progress >= 5f / 6f) {  virtualFraction = (progress - 5f / 6f) * 6;  disappear(circle, virtualFraction);  return; } virtualFraction = (progress - 1f / 6f) * 3f / 2f; move(circle, virtualFraction); }

我用了一個(gè) virtualFraction 來(lái)表示每個(gè)小球的虛擬進(jìn)度(相當(dāng)于上面坐標(biāo)圖中的下值,即坐標(biāo)百分比),例如當(dāng)動(dòng)畫(huà)的總進(jìn)度為 0 時(shí),左小球的虛擬進(jìn)度就應(yīng)該是 1/6+0 (默認(rèn)已經(jīng)經(jīng)過(guò)了出現(xiàn)過(guò)程,消耗了 1/6),中間小球的虛擬進(jìn)度為 1/6+1/3+0 = 1/2 (默認(rèn)經(jīng)歷了出現(xiàn),移動(dòng)到中間過(guò)程),最右邊小球的虛擬進(jìn)度為 1/6+1/3+1/3+0 = 5/6 。然后動(dòng)畫(huà)的總進(jìn)度到 1/3 時(shí),左小球的虛擬進(jìn)度就為 1/2 (中間位置)......

下面再看下 move() 、appear()、disapear() 方法:

#RefreshView

 private void appear(Circle circle, float fraction) { circle.r = (int) (mMinRadius * fraction); circle.x = mMinRadius; } private void disappear(Circle circle, float fraction) { circle.r = (int) (mMinRadius * (1 - fraction)); } private void move(Circle circle, float fraction) { int difference = mMaxRadius - mMinRadius; if (fraction < 0.5) {  circle.r = (int) (mMinRadius + difference * fraction * 2); } else {  circle.r = (int) (mMaxRadius - difference * (fraction - 0.5) * 2); } circle.x = (int) (mMinRadius + mGap * 2 * fraction); }

這個(gè)三個(gè)方法都很簡(jiǎn)單,根據(jù)坐標(biāo)的占比來(lái)計(jì)算出小球的坐標(biāo)跟大小。

以上就是整個(gè) RefershView 的實(shí)現(xiàn)了,如果需要看源碼的可以拉到文末。

四 使用及效果

看下怎么使用:

#MainActivity

 @Override protected void onCreate(Bundle savedInstanceState) {  super.onCreate(savedInstanceState);  setContentView(R.layout.activity_main);  mRefreshView = findViewById(R.id.refresh_view);//  mRefreshView.setOriginState(RefreshView.STATE_PREPARED);  Button start = findViewById(R.id.start);  Button stop = findViewById(R.id.stop);  SeekBar seekBar = findViewById(R.id.seek_bar);  seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {   @Override   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {    mRefreshView.drag(progress / 100f);   }   @Override   public void onStartTrackingTouch(SeekBar seekBar) {   }   @Override   public void onStopTrackingTouch(SeekBar seekBar) {   }  });  start.setOnClickListener(this);  stop.setOnClickListener(this); } @Override public void onClick(View v) {  switch (v.getId()) {   case R.id.start:    mRefreshView.start();    break;   case R.id.stop:    mRefreshView.stop();    break;  } }

效果圖:

Android,View,騰訊TIM,下拉刷新

由于錄制軟件的問(wèn)題,綠色的小球顯示效果不太好,在手機(jī)或虛擬機(jī)上顯示是正常的。再看個(gè)項(xiàng)目里的實(shí)際運(yùn)用效果:

Android,View,騰訊TIM,下拉刷新

錄屏軟件對(duì)綠色好像過(guò)敏,將就看一下吧。

此文到此就結(jié)束了,感謝閱讀,喜歡的動(dòng)動(dòng)小手點(diǎn)個(gè)贊。

Demo 地址:https://github.com/gminibird/RefreshViewTest

總結(jié)

以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)VEVB武林網(wǎng)的支持。


注:相關(guān)教程知識(shí)閱讀請(qǐng)移步到Android開(kāi)發(fā)頻道。
發(fā)表評(píng)論 共有條評(píng)論
用戶(hù)名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 午夜成人在线视频 | 国内a∨免费播放 | 91视频免费版污 | 天天干 夜夜操 | 久久久蜜桃 | 日韩高清在线播放 | 久久亚洲二区 | 北条麻妃国产九九九精品小说 | 四虎欧美 | 欧美日日干 | 欧美日韩精品一区二区三区 | 日韩精品一区二区三区老鸭窝 | 国产电影精品久久 | 色噜噜色狠狠 | 中文字幕国产 | 人人超碰免费 | 国产在线国偷精品产拍免费yy | 日本一区二区免费在线 | 四虎首页 | 日韩一区免费观看 | 男女国产视频 | 国产精品美女视频一区二区三区 | 日本小视频网站 | 欧美在线亚洲 | 国产一区二区三区高清 | 偷拍亚洲精品 | 高清国产一区二区三区四区五区 | 欧美精品在线观看 | 精品久久久一区二区 | 99亚洲视频 | 黑人巨大精品欧美一区二区免费 | 精品久久久久久久 | 欧美精品久久久久久久久 | 美女日日日 | 国产精品毛片一区二区在线看 | 国产精品视频 | 日韩成人高清电影 | 日日操av| 日本免费视频 | 久久久久久久久久穴 | 一区二区视频 |