iT邦幫忙

0

Android 滑動 - scrollTo()、scrollBy () 以及 Scroller [1] 原始碼閱讀

前言

因為最近開始試著把一些閱讀到的東西轉化吸收一下,並且也訓練一下書寫的技能,所以上來紀錄一下。

另外也看許多人說 source code 有機會有時間還是要加減看看,所以想說藉這個機會有順路看到的就整理整理上來,當做自己的紀錄也讓有需要的人了解或是討論,要是我有哪邊寫錯或是不完整的也歡迎補充交流喔~~


在實際應用和說明前先簡單看一下 scrollTo 以及 scrollBy 的原始碼。

/**
	* The offset, in pixels, by which the content of this view is scrolled
	* horizontally.
	* Please use {@link View#getScrollX()} and {@link View#setScrollX(int)} instead of
	* accessing these directly.
	* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
protected int mScrollX;
/**
	* The offset, in pixels, by which the content of this view is scrolled
	* vertically.
	* Please use {@link View#getScrollY()} and {@link View#setScrollY(int)} instead of
	* accessing these directly.
	* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
protected int mScrollY;

/**
	* Return the scrolled left position of this view. This is the left edge of
	* the displayed part of your view. You do not need to draw any pixels
	* farther left, since those are outside of the frame of your view on
	* screen.
	*
	* @return The left edge of the displayed part of your view, in pixels.
	*/
@InspectableProperty
public final int getScrollX() {
	return mScrollX;
}

/**
	* Return the scrolled top position of this view. This is the top edge of
	* the displayed part of your view. You do not need to draw any pixels above
	* it, since those are outside of the frame of your view on screen.
	*
	* @return The top edge of the displayed part of your view, in pixels.
	*/
@InspectableProperty
public final int getScrollY() {
	return mScrollY;
}

在這邊我們可以注意到 mScrollX 和 mScrollY 兩個都是偏移量,而非實際座標(相對於原點(0, 0)偏移的量),並且取用兩者的時候應該要使用 getter 和 setter 使用。

再來是 scrollTo 還有 scrollBy 的部分,

/**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }

    /**
     * Move the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the amount of pixels to scroll by horizontally
     * @param y the amount of pixels to scroll by vertically
     */
    public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

從他們兩者的原始碼我們可以看出來其實兩者是差不多的,差別是:

  • scrollTo:如果"偏移量"發生改變,就會改變 mScrollX 和 mScrollY 賦予它們新的值,以改變當前位置。簡單來說就是將一個 view 從舊的位置移到新的位置。
  • scrollBy:我們可以從他傳入 srcollTo 的部分發現,他是基於原本的偏移基礎上再發生偏移。換句話說就是我們想要把這個 view 移動多遠,那我們就將這個移動距離填入即可。

大致知道 scrollTo 以及 scrollBy 的關係後,我們再延伸閱讀一下,如果單看右圖左邊的部分在 Android 手機上坐標系的關係圖,如果我們今天打算使用 scroll(0, 20) 會發生甚麼是情呢,向下移動20單位嗎?不!事實上他會向上移動20單位。這是為甚麼呢?

https://ithelp.ithome.com.tw/upload/images/20200611/20125739eUaBik3Jdd.png

那這個部分就需要我們稍微看一下原始碼的部分才能了解了,在前面的 scrollTo() 的尾巴我們可以看到他最後呼叫了postInvalidateOnAnimation(),那這代表著未來這個方法會去通知 View 進行重繪。所以我們來看一下 draw() 的原始碼,那這個部分就只附上比較重要的部分:

/**
	* Manually render this view (and all of its children) to the given Canvas.
	* The view must have already done a full layout before this function is
	* called.  When implementing a view, implement
	* {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
	* If you do need to override this method, call the superclass version.
	*
	* @param canvas The Canvas to which the View is rendered.
*/
@CallSuper
public void draw(Canvas canvas) {
	final int privateFlags = mPrivateFlags;
	mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
	
	/*
		* Draw traversal performs several drawing steps which must be executed
		* in the appropriate order:
		*
		*      1. Draw the background
		*      2. If necessary, save the canvas' layers to prepare for fading
		*      3. Draw view's content
		*      4. Draw children
		*      5. If necessary, draw the fading edges and restore layers
		*      6. Draw decorations (scrollbars for instance)
		*/
	...

	// Step 6, draw decorations (foreground, scrollbars)
	onDrawForeground(canvas);
	
	...
}

首先我們可以從中間的註解看到繪製滾動條的部分 (scrollbars for instance) 是在第六步進行的 ,因此我們注意一下 Step 6的地方,他去呼叫了onDrawForeground,那我們再跟過去會發現在這個方法裡面他會去呼叫 onDrawScrollBars(canvas),這個函示會分別去繪製水平方向以及垂直方向的 ScrollBar。

/**
	* Draw any foreground content for this view.
	*
	* <p>Foreground content may consist of scroll bars, a {@link #setForeground foreground}
	* drawable or other view-specific decorations. The foreground is drawn on top of the
	* primary view content.</p>
	*
	* @param canvas canvas to draw into
	*/
public void onDrawForeground(Canvas canvas) {
	onDrawScrollIndicators(canvas);
	onDrawScrollBars(canvas);
	
	final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
	if (foreground != null) {
		...
	}
}

而在 onDrawScrollBars(canvas) 中我們簡單的觀察一下會發現,在這個 function 中最後都會去調用一個函式 invalidate(bounds)

/**
	* <p>Request the drawing of the horizontal and the vertical scrollbar. The
	* scrollbars are painted only if they have been awakened first.</p>
	*
	* @param canvas the canvas on which to draw the scrollbars
	*
	* @see #awakenScrollBars(int)
	*/
  protected final void onDrawScrollBars(Canvas canvas) {
    // scrollbars are drawn only when the animation is running
    final ScrollabilityCache cache = mScrollCache;

    if (cache != null) {
			...
      final boolean drawHorizontalScrollBar = isHorizontalScrollBarEnabled();
      final boolean drawVerticalScrollBar = isVerticalScrollBarEnabled()
              && !isVerticalScrollBarHidden();

      // Fork out the scroll bar drawing for round wearable devices.
      if (mRoundScrollbarRenderer != null) {
        if (drawVerticalScrollBar) {
          ...
          if (invalidate) {
            invalidate();
          }
        }
        // Do not draw horizontal scroll bars for round wearable devices.
      } else if (drawVerticalScrollBar || drawHorizontalScrollBar) {
        ...
        if (drawHorizontalScrollBar) {
          ...
          if (invalidate) {
            invalidate(bounds);
          }
        }

        if (drawVerticalScrollBar) {
          ...
          if (invalidate) {
            invalidate(bounds);
          }
        }
	    }
	  }
	}

這邊因為這個function比較長,所以我刪減了部分內容留下我們現在比較需要關注的點。到這邊相信也有感覺到離我們想知道的事情很近了,在這邊順便補充一下,我們看到 invalidate 中傳入的 bound 的型別式 Rect,也就是常用來繪製矩形之類的型態。下面附上他的屬性。

Rect(int left, int top, int right, int bottom)

看到傳入了上下左右的數值有沒有突然覺得下一步就是我們想知道的地方啊,最後我們來看一下 invalidate(bounds) 中的一個部分。

/**
	* Mark the area defined by dirty as needing to be drawn. If the view is
	* visible, {@link #onDraw(android.graphics.Canvas)} will be called at some
	* point in the future.
	* <p>
	* This must be called from a UI thread. To call from a non-UI thread, call
	* {@link #postInvalidate()}.
	* <p>
	* <b>WARNING:</b> In API 19 and below, this method may be destructive to
	* {@code dirty}.
	*
	* @param dirty the rectangle representing the bounds of the dirty region
	*
	* @deprecated The switch to hardware accelerated rendering in API 14 reduced
	* the importance of the dirty rectangle. In API 21 the given rectangle is
	* ignored entirely in favor of an internally-calculated area instead.
	* Because of this, clients are encouraged to just call {@link #invalidate()}.
	*/
	@Deprecated
	public void invalidate(Rect dirty) {
		final int scrollX = mScrollX;
		final int scrollY = mScrollY;
		invalidateInternal(dirty.left - scrollX, dirty.top - scrollY,
				dirty.right - scrollX, dirty.bottom - scrollY, true, false);
	}

看到這個原始碼有沒有一點點小悸動阿,是不是發現了甚麼!那就是在他的尾巴有一個 dirty.left - scrollX, dirty.top - scrollY, dirty.right - scrollX, dirty.bottom - scrollY ,相信你看到這邊應該也知道為甚麼我們輸入的數值會跟我們一開始預期的相反了吧~


尚未有邦友留言

立即登入留言