溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

android自定義布局中的平滑移動(dòng)

發(fā)布時(shí)間:2020-08-24 02:57:15 來(lái)源:網(wǎng)絡(luò) 閱讀:515 作者:無(wú)心小書(shū)童 欄目:移動(dòng)開(kāi)發(fā)

在android應(yīng)用程序的開(kāi)發(fā)過(guò)程中,相信我們很多人都想把應(yīng)用的交互做的比較絢麗,比如讓界面切換平滑的滾動(dòng),還有熱度灰常高的偽3D等界面效果,通常情況下,系統(tǒng)提供的應(yīng)用在特效這方面只能為我們提供簡(jiǎn)單的動(dòng)畫(huà)接口,所以要想實(shí)現(xiàn)比較酷炫的效果還是要自己去開(kāi)發(fā)布局控件(即所謂的自定義View、ViewGroup)。小弟也經(jīng)常做一些自定義的控件,最近工作比較清閑,所以便將自己對(duì)自定義布局控件的一些心得寫(xiě)出來(lái),權(quán)當(dāng)是自己的學(xué)習(xí)筆記了,各位高手看到了可以忽略android自定義布局中的平滑移動(dòng)

。下面就我最近工作中遇到的一個(gè)自定義控件開(kāi)發(fā)做一些簡(jiǎn)單的介紹,其實(shí)那個(gè)地方原本可以用ScrollView解決很大一部分問(wèn)題的,但有一些效果確實(shí)需要對(duì)控件進(jìn)行重新定義,在繼承ScrollView開(kāi)發(fā)中仍然會(huì)遇到一些ScrollView自身的限制,所以就仿照ScrollView自己做了一個(gè)控件。在其中遇到了一些問(wèn)題自然就是像ScrollView中拖動(dòng)的效果(比如快速拖動(dòng)在手指離開(kāi)屏幕時(shí)控件依舊會(huì)由于慣性繼續(xù)滑動(dòng)一段距離后才會(huì)停止運(yùn)動(dòng)),所以就對(duì)這個(gè)東東做了一下仔細(xì)的研究,雖然以前也做過(guò)類似的開(kāi)發(fā),這次由于時(shí)間比較充裕,所以將開(kāi)發(fā)中遇到的一些問(wèn)題都一一記錄了下來(lái)。下面開(kāi)始正題:


自定義布局控件自然是要繼承某個(gè)View或ViewGroup

由于是根據(jù)項(xiàng)目的開(kāi)發(fā)來(lái)寫(xiě)的這篇博客,所以我就以自定義布局控件(ViewGroup)來(lái)做介紹了。

開(kāi)發(fā)一個(gè)自定義的ViewGroup自然是要繼承ViewGroup類了,在繼承這個(gè)類之后必須要重寫(xiě)的方法就是

onLayout(boolean changed, int l, int t, int r, int b)

另外至少要有一個(gè)構(gòu)造方法,我個(gè)人習(xí)慣重寫(xiě)那個(gè)有兩個(gè)參數(shù)的構(gòu)造方法(XXX(Context context, AttributeSet attrs)),因?yàn)橛辛诉@個(gè)構(gòu)造方法就可以在xml布局文件里使用這個(gè)類了。

如果想要對(duì)這個(gè)布局控件以及其子控件的尺寸進(jìn)行精確的控制那就要重寫(xiě)下面這個(gè)方法了

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

這個(gè)方法從字面理解就是估算控件的尺寸大小了,關(guān)于這個(gè)方法的詳細(xì)說(shuō)明引用一下另一位童鞋的文章http://www.eoeandroid.com/thread-102385-1-1.html,這里就不詳細(xì)介紹了


下面開(kāi)始介紹關(guān)于如何讓自定義的控件進(jìn)行平滑的移動(dòng),并能夠根據(jù)手勢(shì)的情況產(chǎn)生慣性滑動(dòng)的效果

先介紹一下開(kāi)發(fā)這種滑動(dòng)效果需要用到的各種工具類:

android.view.VelocityTracker
android.view.Scroller
android.view.ViewConfiguration


VelocityTracker從字面意思理解那就是速度追蹤器了,在滑動(dòng)效果的開(kāi)發(fā)中通常都是要使用該類計(jì)算出當(dāng)前手勢(shì)的初始速度(不知道我這么理解是否正確,對(duì)應(yīng)的方法是velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity))并通過(guò)getXVelocity或getYVelocity方法得到對(duì)應(yīng)的速度值initialVelocity,并將獲得的速度值傳遞給Scroller類的fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) 方法進(jìn)行控件滾動(dòng)時(shí)各種位置坐標(biāo)數(shù)值的計(jì)算,API中對(duì)fling 方法的解釋是基于一個(gè)fling手勢(shì)開(kāi)始滑動(dòng)動(dòng)作,滑動(dòng)的距離將由所獲得的初始速度initialVelocity來(lái)決定。關(guān)于ViewConfiguration 的使用主要使用了該類的下面三個(gè)方法:

configuration.getScaledTouchSlop() //獲得能夠進(jìn)行手勢(shì)滑動(dòng)的距離
configuration.getScaledMinimumFlingVelocity()//獲得允許執(zhí)行一個(gè)fling手勢(shì)動(dòng)作的最小速度值
configuration.getScaledMaximumFlingVelocity()//獲得允許執(zhí)行一個(gè)fling手勢(shì)動(dòng)作的最大速度值

需要重寫(xiě)的方法至少要包含下面幾個(gè)方法:

onTouchEvent(MotionEvent event)//有手勢(shì)操作必然少不了這個(gè)方法了

computeScroll()//必要時(shí)由父控件調(diào)用請(qǐng)求或通知其一個(gè)子節(jié)點(diǎn)需要更新它的mScrollX和mScrollY的值。典型的例子就是在一個(gè)子節(jié)點(diǎn)正在使用Scroller進(jìn)行滑動(dòng)動(dòng)畫(huà)時(shí)將會(huì)被執(zhí)行。所以,從該方法的注釋來(lái)看,繼承這個(gè)方法的話一般都會(huì)有Scroller對(duì)象出現(xiàn)。


在往下就是介紹比較具體的開(kāi)發(fā)思路

首先我們要初始化一些變量,其中的多數(shù)代碼已經(jīng)在上面做出介紹了

Java代碼

  1. void init(Context context) {
                    mScroller = new Scroller(getContext());
                    setFocusable(true);
                    setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
                    setWillNotDraw(false);
                    final ViewConfiguration configuration = ViewConfiguration.get(context);
                    mTouchSlop = configuration.getScaledTouchSlop();
                    mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
                    mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
            }

復(fù)制代碼

然后我們申明一個(gè)用來(lái)處理滑動(dòng)操作的方法fling(int velocityY),代碼如下:

Java代碼
  1. public void fling(int velocityY) {
            if (getChildCount() > 0) {
                    mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
                                    maxScrollEdge);
                    final boolean movingDown = velocityY > 0;
                    awakenScrollBars(mScroller.getDuration());
                    invalidate();
            }
    }

復(fù)制代碼

在這個(gè)方法里只是使用Scroller的fling方法開(kāi)始執(zhí)行fling手勢(shì)動(dòng)作了,關(guān)于其中的各種參數(shù)就不一一解釋了。

awakenScrollBars(int startDelay)方法根據(jù)我對(duì)注釋的理解就是在這里給出動(dòng)畫(huà)開(kāi)始的延時(shí),當(dāng)參數(shù)startDelay為0時(shí)動(dòng)畫(huà)將立刻開(kāi)始,其實(shí)就是一個(gè)延遲的作用


下面是對(duì)VelocityTracker的初始化以及資源釋放的方法

Java代碼
  1. private void obtainVelocityTracker(MotionEvent event) {
            if (mVelocityTracker == null) {
                    mVelocityTracker = VelocityTracker.obtain();
            }
            mVelocityTracker.addMovement(event);
    }
    private void releaseVelocityTracker() {
            if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
            }
    }

復(fù)制代碼

onTouchEvent(MotionEvent event)方法的重寫(xiě)

Java代碼
  1. public boolean onTouchEvent(MotionEvent event) {

  2.                 if (event.getAction() == MotionEvent.ACTION_DOWN

  3.                                 && event.getEdgeFlags() != 0) {

  4.                         return false;

  5.                 }


  6.                 obtainVelocityTracker(event);


  7.                 final int action = event.getAction();

  8.                 final float x = event.getX();

  9.                 final float y = event.getY();


  10.                 switch (action) {

  11.                 case MotionEvent.ACTION_DOWN:

  12.                         LogUtil.log(TAG, "ACTION_DOWN#currentScrollY:" + getScrollY()

  13.                                         + ", mLastMotionY:" + mLastMotionY,

  14.                                         LogUtil.LOG_E);

  15.                         if (!mScroller.isFinished()) {

  16.                                 mScroller.abortAnimation();

  17.                         }

  18.                         mLastMotionY = y;

  19.                         break;


  20.                 case MotionEvent.ACTION_MOVE:

  21.                         final int deltaY = (int) (mLastMotionY - y);

  22.                         mLastMotionY = y;

  23.                         if (deltaY < 0) {

  24.                                 if (getScrollY() > 0) {

  25.                                         scrollBy(0, deltaY);

  26.                                 } 

  27.                         } else if (deltaY > 0) {

  28.                                 mIsInEdge = getScrollY() <= childTotalHeight - height;

  29.                                 if (mIsInEdge) {

  30.                                         scrollBy(0, deltaY);

  31.                                 }

  32.                         }

  33.                         break;


  34.                 case MotionEvent.ACTION_UP:

  35.                         final VelocityTracker velocityTracker = mVelocityTracker;

  36.                         velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);

  37.                         int initialVelocity = (int) velocityTracker.getYVelocity();


  38.                         if ((Math.abs(initialVelocity) > mMinimumVelocity)

  39.                                         && getChildCount() > 0) {

  40.                                 fling(-initialVelocity);

  41.                         }


  42.                         releaseVelocityTracker();

  43.                         break;

  44.                 }


  45.                 return true;

  46.         }

復(fù)制代碼

在onTouchEvent方法中,當(dāng)手勢(shì)執(zhí)行到ACTION_UP時(shí)獲得當(dāng)時(shí)手勢(shì)的速度值然后判斷這個(gè)速度值是否大于可滑動(dòng)的最小速度,如果符合條件那么就執(zhí)行fling(int velocityY)方法,通過(guò)fling方法中的日志發(fā)現(xiàn),在執(zhí)行了invalidate()方法之后,程序便會(huì)執(zhí)行computeScroll()方法,在computeScroll()方法中執(zhí)行scrollTo方法主要是因?yàn)閙ScrollX、mScrollY這兩個(gè)變量的修飾符為portected,無(wú)法在擴(kuò)展類里面無(wú)法對(duì)這兩個(gè)變量直接進(jìn)行操作,那么就需要使用scrollTo方法對(duì)這兩個(gè)變量進(jìn)行操作,以刷新當(dāng)前的UI控件,下面附上computeScroll()方法的代碼

Java代碼
  1. public void computeScroll() {

  2.         if (mScroller.computeScrollOffset()) {

  3.                 int scrollX = getScrollX();

  4.                 int scrollY = getScrollY();

  5.                 int oldX = scrollX;

  6.                 int oldY = scrollY;

  7.                 int x = mScroller.getCurrX();

  8.                 int y = mScroller.getCurrY();

  9.                 scrollX = x;

  10.                 scrollY = y;

  11.                 scrollY = scrollY + 10;

  12.                 scrollTo(scrollX, scrollY);

  13.                 postInvalidate();

  14.         }

  15. }

復(fù)制代碼

其中的mScroller.computeScrollOffset()是用來(lái)判斷動(dòng)畫(huà)是否完成,如果沒(méi)有完成返回true繼續(xù)執(zhí)行界面刷新的操作,各種位置信息將被重新計(jì)算用以重新繪制最新?tīng)顟B(tài)的界面。關(guān)于scrollTo方法,我們需要看一下該方法的代碼(來(lái)自View中):

Java代碼
  1. public void scrollTo(int x, int y) {

  2.         if (mScrollX != x || mScrollY != y) {

  3.             int oldX = mScrollX;

  4.             int oldY = mScrollY;

  5.             mScrollX = x;

  6.             mScrollY = y;

  7.             onScrollChanged(mScrollX, mScrollY, oldX, oldY);

  8.             if (!awakenScrollBars()) {

  9.                 invalidate();

  10.             }

  11.         }

  12.     }

復(fù)制代碼

我們可以看到,當(dāng)傳遞進(jìn)來(lái)的x、y的值與控件當(dāng)前的mScrollX、mScrollY的值不相同時(shí)對(duì)界面進(jìn)行重新計(jì)算,根據(jù)日志打印的情況來(lái)看似乎awakenScrollBars()返回的總是true, 這樣的話每執(zhí)行一次computeScroll()方法,就需要執(zhí)行一次postInvalidate()方法來(lái)刷新界面,而postInvalidate()方法會(huì)通過(guò)內(nèi)部線程重新調(diào)用invalidate()已達(dá)到界面刷新的效果,產(chǎn)生手勢(shì)離開(kāi)屏幕之后的慣性滑動(dòng)效果。


可能上面說(shuō)的比較凌亂,在這里總結(jié)一下,大概的思路如下:

首先我們通過(guò)VelocityTracker、ViewConfiguration類得到一些慣性滑動(dòng)所必須的變量,比如手勢(shì)離開(kāi)屏幕時(shí)的初始速度,允許進(jìn)行手勢(shì)操作的最小距離以及允許手勢(shì)操作的速度邊界值;

第二,創(chuàng)建Scroller的對(duì)象,使用它的fling方法供我們控制界面滑動(dòng)使用;

第三,重寫(xiě)onTouchEvent方法,當(dāng)我們用手指在屏幕上來(lái)回滑動(dòng)時(shí)此時(shí)執(zhí)行的是scrollBy方法來(lái)刷新界面,當(dāng)手指離開(kāi)屏幕,此時(shí)就要開(kāi)始執(zhí)行ACTION_UP后面的操作了;

通過(guò)對(duì)手指離開(kāi)屏幕時(shí)的速度進(jìn)行判斷是否能夠進(jìn)行慣性滑動(dòng)操作,

如果能夠執(zhí)行那么就使用Scroller類的fling方法啟動(dòng)滑動(dòng)動(dòng)畫(huà),

這時(shí)需要調(diào)用一下invalidate()方法來(lái)間接的調(diào)用computeScroll方法,

在computeScroll方法中對(duì)Scroller的動(dòng)畫(huà)是否執(zhí)行完成做了判斷,

如果動(dòng)畫(huà)沒(méi)有完成(mScroller.computeScrollOffset() == true)那么就使用scrollTo方法對(duì)mScrollX、mScrollY的值進(jìn)行重新計(jì)算刷新界面,

調(diào)用postInvalidate()方法重新繪制界面,

postInvalidate()方法會(huì)調(diào)用invalidate()方法,

invalidate()方法又會(huì)調(diào)用computeScroll方法,

就這樣周而復(fù)始的相互調(diào)用,直到mScroller.computeScrollOffset() 返回false才會(huì)停止界面的重繪動(dòng)作


總結(jié),滑動(dòng)效果來(lái)看,它依然是在不停的計(jì)算控件的位置刷新屏幕,不停的繪制新的圖片替換舊的圖片,當(dāng)然每次刷新的速度很快,從而給人一種是在快速滑動(dòng)的感覺(jué),寫(xiě)到這里我發(fā)現(xiàn),現(xiàn)在所謂的動(dòng)畫(huà)總是逃脫不了電影的那種模式,每秒播放多少幀的圖片來(lái)達(dá)到連續(xù)播放的效果欺騙人的眼睛。

而且,關(guān)于android一些酷炫效果的開(kāi)發(fā),還是要自己多動(dòng)手,熟悉View、ViewGroup中每個(gè)繪制方法、位置計(jì)算方法的調(diào)用方式以及順序,那么至少是在2D動(dòng)畫(huà)開(kāi)發(fā)中,也就是一種方式,逃脫不了不停重新繪制的這個(gè)圈。

關(guān)于熟悉View、ViewGroup中每個(gè)繪制方法、位置計(jì)算方法的調(diào)用方式以及順序的問(wèn)題,我建議最好自己寫(xiě)一個(gè)簡(jiǎn)單的自定義View或ViewGroup的擴(kuò)展類,重載那些繪制、位置計(jì)算的方法打個(gè)日志出來(lái)一看自然就明白了,雖然這個(gè)方法很笨,但是很容易出效果的


向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI