編輯:關於Android編程
其實對於接觸過Android開發的人來說,視圖的滑動並不陌生,因為這一功能特性可以說是隨處可見。
常用的就例如ScrollView、HorizontalScrollView、ListView,還有熟悉的ViewPager等控件,就都支持這一特性。
之所以這一類的控件在Android系統中如此受歡迎,其實也不難想象,最顯而易見的:
手機的屏幕(可視區域)是十分有限的,那麼如何在有限的區域內提供給用戶“無限”的內容,也就是促使滑動視圖誕生的根本原因。
今天就來總結一些對於接觸到Android的視圖滑動相關的知識的一些理解,以便加深印象。
其實在Android中讓視圖滑動的實現方式有很多種,例如在《Android群英傳》一書中,就總結了足足7種方式:
通過View的layout()方法讓View滑動。 通過調用View類的offsetLeftAndRight、offsetTopAndBottom讓View滑動。 通過設置LayoutParams讓View滑動。 通過scrollTo與scrollBy讓View滑動。 通過Scroller類來讓View滑動。 通過屬性動畫來讓View進行滑動。 終極神器ViewDragHelper。
我們這裡不對每種方式都依次進行嘗試,因為萬變不離其宗。所以我們的重點放在理解讓視圖進行滑動的原理上。
最初接觸到Android開發的時候,自己對ViewPager這個控件十分感興趣,因為很多主流的APP的主界面上都采用了這種效果。
那麼,我們何不就通過模仿一個十分簡單的類似ViewPager的效果的自定義控件,來了解視圖滑動的原理呢?
如果不去查閱任何的相關資料,自己去研究如何實現讓視圖滑動。那麼肯定看著最親切的就是它們兩兄弟了。
畢竟名字裡就已經帶著扎眼的“scroll”,而它們確實能夠讓View實現移動的效果。
其實初初接觸之下,肯定都覺得這兩個方法的使用還是十分簡單明了的。
這樣說也沒錯,但其實關於它們也還是有不少值得我們了解的細節的。
我們說scrollTo和scrollBy方法實際上都是實現讓View的位置發生改變,這兩個方法都接受兩個int型的變量(x和y)。
而且就和它們的方法名的定義一樣,它們的區別也很明顯,就在於:
scrollTo是代表View將會移動到坐標點(x,y)的位置。 scrollBy是代表此次View在x軸,和y軸上移動的增量為x和y。
通過一個簡單的Demo,我們可以很形象的體會它們的效果。假設現有如下布局:
那麼我們首先通過如下代碼查看scrollTo的效果:
public class MainActivity extends AppCompatActivity {
private RelativeLayout container;
private Button startScrollBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
container = (RelativeLayout) findViewById(R.id.container);
startScrollBtn = (Button) findViewById(R.id.start_scroll);
startScrollBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
container.scrollTo(100, 100);
}
});
}
}
測試效果如下:
嘿嘿,你可能已經注意到了,我們測試的效果發現是原本居中顯示的button進行了一定的位移,最終到了靠近屏幕左上方的一個位置。
但是,有趣的是:我們在代碼中實際上是通過container,也就是button所在的父視圖RelativeLayout來調用的scrollTo方法。
我們最初很可能會經歷這樣類似的迷茫,原本想調用父視圖的scrollTo方法來讓它發生位移,但卻發現其子控件產生了位移。
為什麼會造成這樣的現象,我們不急著去查明原因。我們先接著看看如果是使用scrollBy,又會是什麼情況。
OK,我們將之前的代碼中scrollTo(100,100)改為scrollBy(100,100),再看看有什麼事情發生:
vcHL1eLA787Sw8fP69KqveK/qs7Sw8e1xNbW1tbSyc7Ko6zX1Mi71rvE3LTT1LTC67Wx1tDIpdXStPCwuMHLoaM8L3A+DQo8aDIgaWQ9"scrollto和scrollby的源碼分析">scrollTo和scrollBy的源碼分析
有了目的其實接下來的工作就很明確了,我們直接先打開scrollTo的源碼看看:
/**
* 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();
}
}
}
其實我們通過該方法的源碼以及代碼注釋發現,我們之前理解的沒有錯,該方法的目的就是:
設置位移目的地的坐標,然後則會調用onScrollChanged方法,從而通知View重繪。
雖然該方法的功能性很明確,但注意一下,我們發現出現了兩個成員變量”mScrollX”與 “mScrollX”,事實上也正是這兩個變量控制著View的位移。
以”mScrollX”為例,我們來看一看源碼中對該變量的說明:
/**
* The offset, in pixels, by which the content of this view is scrolled
* horizontally.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollX;
這個時候,通過注釋我們發現很關鍵的一個信息,所謂的這個關於位移的成員變量,指的是:
by which the content of this view is scrolled,也就是說,指的是該view的內容滾動的距離。
這個時候我們就能理解為什麼,我們在之前的代碼中,通過container調用該方法,但滾動的卻是其內容(即button)。
同時這也是為什麼,我們會看到有些資料和書籍當中會有,“scrollTo與scrollBy移動的不是View自身,而是View的內容”的說法。
好吧,這點我們弄懂了。但是為什麼我們在之前的代碼裡,scrollTo與scrollBy的效果卻沒有任何區別?
帶著這個疑問,我們打開scrollBy的源代碼來瞧上一瞧:
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
原來如此,scrollBy在底層仍然調用了scrollTo,不同的是:它是將傳入的x,y兩個參數分別加上mScrollX與mScrollY來作為最終調用scrollTo時的參數。
於是,我們很清楚的可以理解到,scrollBy是在之前發生過的位移的基礎上,再去移動x,y的距離。
在我們之前的測試代碼中,我們直接調用scrollBy(100,100),這個時候呢,因為mScrollX和mScrollY的默認值為初始的0.所以它與直接調用scrollTo(100,100)所達到的效果沒有任何區別。
解惑的工作完成了嗎?實際上並沒有,因為我們發現,我們傳入的參數為正數,按照對於視圖系的正常理解來說,它應該向屏幕的右下方移動才對,然而事實卻相反。
這樣的結果是在告訴我們,以x軸為例,偏移量大於0則是向左位移;而偏移量小於0才是向右位移這樣的觀點嗎?
這樣的說法是否准確?事實上究竟是怎麼樣的原因造成這樣的現象?我們還得繼續來看。
要解開我們前面所說到的另一個困惑,我們先要了解一個概念,即“什麼是可視區域”?
其實這個概念並不難理解。首先,我們之前也說了,一個手機的屏幕大小是固定的。
作為用戶來說,我們可能很直觀的認為當前屏幕內顯示的內容就是View的內容。
但我們也知道,如果真的如此,那麼一個View的局限性就太大了。所以,在Android中,一個View的內容顯然不可能被限定在這樣的框框裡。
所謂“心有多大,舞台就有多大”,我們可以這樣想象:一個View的內容可以是無限多的,但是呢,因為屏幕大小的限制,當前能夠呈現在屏幕上的內容是有限的。
但是,當前沒有被呈現在屏幕上的內容,我們並不能就說它不存在。現在僅僅是因為我們的“視野”不夠,看不到他們罷了。
所以,當前呈現在手機屏幕上的,我們可以理解為“可視區域”,而當前沒有在手機屏幕上的內容,它們位於“可視區域”之外,但並非不存在。
通過畫一張簡單的圖,可以更容易理解這種關系:
在這裡,圖中藍色的區域是我們的View的所有內容,紅色區域則是顯示在屏幕上的內容。
現在我們來分析一下,為什麼會出現傳入的位移量為正數,button卻向左上方移動的情況。
圖中紅色區域的坐上角的點(即實際Android設備屏幕左上角),該點的坐標是視圖系的原點。
假設我們現在調用了scrollTo(100,100),實際上我們就是讓紅色區域view的左上角從原點,移動到x,y軸坐標均為100的點。
最終就會得到如下圖所示的效果:
也就是說,所謂的“可視區域”經scrooTo方法移動過後,變為了圖中的”黃色區域”。
這個時候,我們發現了一個熟悉的情景。沒錯,button的位置與我們之前測試的效果是一致的。
由此,我們也就搞清楚了Android當中視圖移動的原理了。那麼,我們就不妨趕緊趁熱打鐵,自定義一個View,來模擬一個簡易的ViewPager。
在開始寫代碼之前,我們先明確一下我們大概要做的工作。
首先,很顯然,我們要實現的將是一個ViewGroup。 我們可以向該ViewGroup內添加子視圖,每個子視圖的寬高填滿屏幕。 我們可以左右滑動屏幕來進行子視圖的切換。 滑動距離超過屏幕的1/3的距離,則完成切換;否則取消切換。
那麼,看上去我們要做的工作並不復雜,無非就是:
創建一個自定義視圖類繼承自ViewGroup。 重寫onMeasure完成子視圖的測量。 重寫onLayout方法完成子視圖的擺放。 重寫onTouchEvent方法,處理View的滑動。
所以我們可能首先編寫得到如下的代碼:
package com.tsr.androidscrolltest.widgit;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by Administrator on 2016/7/12.
*/
public class CustomScrollView extends ViewGroup{
private int mScreenWidth;
private int mLastX;
private int mStartX, mEndX;
public CustomScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
mScreenWidth = context.getResources().getDisplayMetrics().widthPixels;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
layoutParams.width = mScreenWidth * count;
setLayoutParams(layoutParams);
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
child.layout(i * mScreenWidth, 0, (i + 1) * mScreenWidth, b);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mStartX = getScrollX();
break;
case MotionEvent.ACTION_MOVE:
int dx = mLastX - x;
if (dx < 0) {
// X軸的偏移量為0,則證明屏幕還未滑動過
if(getScrollX() > 0){
scrollBy(dx,0);
}
} else if (dx > 0) {
if (getScrollX() < getWidth() - mScreenWidth) {
scrollBy(dx, 0);
}
}
mLastX = x;
break;
case MotionEvent.ACTION_UP:
mEndX = getScrollX();
int movedX = mEndX - mStartX;
if (movedX > 0) {
if (movedX < mScreenWidth / 3) {
// 回到原處
scrollBy(-movedX,0);
} else {
scrollBy(mScreenWidth - movedX,0);
}
} else {
if (-movedX < mScreenWidth / 3) {
scrollBy(-movedX,0);
} else {
scrollBy(- mScreenWidth - movedX,0);
}
}
break;
}
postInvalidate();
return true;
}
}
然後在布局文件中使用我們的自定義視圖:
最終的效果如圖所示:
到這裡,其實我們已經基本上簡單的模擬出了viewpager的一個效果。但是,我們肯定很明顯的注意到,
在此時的效果當中,在我們手指松開滑動的時候,剩下的滑動距離是瞬間完成的,這讓人感覺非常突兀,可謂逼死強迫症。
實際上,這樣的結果是可想而知的,因為我們在onTouchEvent當中,當action為ACTION_UP時,
剩下的滑動距離,我們仍然是通過調用scrollBy完成的,該方法本來就是瞬間完成位移的效果的。
那麼,我們就要想辦法了。有沒有什麼方式可以讓我們為這段位移添加上粘性的效果呢?當然有,那就是使用Scroller類。
對於Scroller的使用,實際上很簡單。我們在我們之前的代碼的基礎上修改和加上如下的代碼:
case MotionEvent.ACTION_UP:
mEndX = getScrollX();
int movedX = mEndX - mStartX;
if (movedX > 0) {
if (movedX < mScreenWidth / 3) {
// 回到原處
mScroller.startScroll(getScrollX(), 0, -movedX, 0);
} else {
mScroller.startScroll(getScrollX(), 0, mScreenWidth - movedX, 0);
mCurrentIndex ++;
}
} else {
if (-movedX < mScreenWidth / 3) {
mScroller.startScroll(getScrollX(), 0, -movedX, 0);
} else {
mScroller.startScroll(getScrollX(), 0, -mScreenWidth - movedX, 0);
mCurrentIndex --;
}
}
break;
}
//================================================
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), 0);
}
postInvalidate();
}
我們所做的工作很簡單,就是將手指松開滑動時,剩余指定距離的位移動作改為通過scroller來實現,而不再是scrollBy。
同時,我們重寫了另外一個方法,即computeScroll()。再次運行程序,我們看看新的效果是否爽了不少:
我們發現,與之前相比,松開手指後的滑動有了一個平移的效果。
因為模擬器的緣故,這個效果並不十分明顯,但我們還是可以感覺相比之前體驗好了不少。
既然Scroller這麼好用,我們當然要去探索一下它為什麼能夠提供給我們平滑移動的效果。
首先,我們看見我們是通過調用Scroller的startScroll方法來開啟滑動的,那麼我們就來看一看這個方法的源碼:
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
從源碼中可以看到,該方法其實只是對滾動做了一些初始設置,比如設置滾動模式,開始時間,持續時間等等。但實際上並沒有真正開啟滾動。
那麼,我們肯定會好奇,究竟什麼時候才會開啟滾動呢?我們注意到在之前的代碼中,我們重寫了computeScroll方法。
很顯然,這肯定不會是毫無意義的一個步驟。所以我們就來看看為什麼要重寫這個方法。
很明顯,我們很容易想到,我們得看看這個方法在什麼時候會被調用。隨著辛苦的查找,我們發現它的調用關系是。
在View類的繪制方法”draw()”當中,會調用dispatchDraw()來分發繪制,通知其孩子進行繪制工作。
dispatchDraw在View類中是一個空方法,其具體實現在ViewGroup當中。很好理解,顯然ViewGroup通常才會有孩子。
ViewGroup的dispatchDraw方法當中的代碼很多,但我們只需要知道該方法中會調用到drawChild方法來繪制子試圖。
而API-23版本的源碼當中,該方法的實現已經簡化到了這種地步:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
也就是說,該方法最終又會通過調用child(具體子試圖)自己的draw方法來完成繪制。computeScroll正是在此方法中被調用。
看到這裡你就說了,開頭也是draw,現在又來個draw,繞這麼大一圈干啥呢?我們需要分清的是:
在我們所說的最初的draw()方法,是“public void draw(Canvas canvas) ”該方法。 而在經過dispatchView → drawChild之後,再通過child.draw調用的是另一個重載方法”boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)“。
了解了這一過程,我們再來回憶我們之前的代碼,還記得我們在computeScroll中,調用了另一個方法“computeScrollOffset”。
這個方法用來判斷是否完成了整個滑動,並且提供了getCurrX和getCurrY方法來獲取當前的滑動坐標。
這個方法的源碼如下:
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
從源碼我們可以發現該方法是通過怎麼樣的形式來判斷滑動是否結束和計算當前滑動坐標的。
回憶一下,我們在startScroll當中獲取了一個當前時間賦值給了成員變量mStartTime,即理解為滑動開始時間。
在該方法中,我們再次獲取當前的時間,並減去開始時間,就得到了一個差值。
我們將這個差值與設置的滑動動畫持續時間mDuration進行比較,如果小於持續時間,則證明滑動還要繼續。
然後根據Interpolator來計算出在該時間段裡面移動的距離,賦值給mCurrX, mCurrY,從而計算出當前的滑動坐標。
到了,這裡實際我們已經清楚Scroller的原理了。我們整理一下思路,能夠有一個更為清晰的理解:
首先,通過調用Scroller的startScroll()方法來進行一些滾動的初始化設置。 接著,我們迫使View進行重繪(invalidate()或postInvalidate()). 經過一系列相關的方法調用,最終會觸發computeScroll()方法。 通過重寫觸發computeScroll()方法,首先在其中調用computeScrollOffset()查看滑動是否結束了。 如果沒有結束,則調用scrollTo()完成滾動。然後再次讓View重繪,如此循環,直至完成滑動。
事實上,到了這裡我們已經基本完成了我們想要的一個簡易的ViewPager了。
但是,“不手賤,無八哥”。在我的手賤之下,又發現了一個無聊的問題。
舉例來說,本來如果現在view停留在第一個時,那麼當我們手指向右滑動,原本是不允許view滾動的,因為其左邊沒有別的view。
但是呢,我現在抽瘋似的一直反復的右滑,右滑。有的時候就會出現view向右滑動了一點的情況。
這個時候,屏幕的最左邊就會出現一道寬度很小的白色空隙。我們來分析一下為什麼出現這樣的情況。
我們回顧一下我們之前在ACTION_MOVE當中的的代碼,:
// X軸的偏移量為0,則證明屏幕還未滑動過
if(getScrollX() > 0){
scrollBy(dx,0);
}
} else if (dx > 0) {
if (getScrollX() < getWidth() - mScreenWidth) {
scrollBy(dx, 0);
}
我們看到例如“getScrollX() > 0”這樣的一個判斷,加上這個判斷的初衷正是我們想要的如果當前處在“第一屏”,則手指右滑也不允許發生試圖滾動。
按理說,這個判斷是合理的,但為什麼會出現前面所說的情況呢?我們在腦海裡默默模擬一下我們的操作,
假設手指一直抽瘋似的右滑右滑右滑,那麼就可能出現一種情況,那就是:
手指在一次右滑結束,離開屏幕後;飛快的左移,想進行下一次的右滑操作,但是可能在不經意就會在左移的過程中觸碰到屏幕。
這個時候,我們的程序會認為用戶在執行一次左滑操作,很顯然,這個時候的滑動時允許的。
那麼屏幕中的View就會開始滑動一點點距離,值得注意的是:我們的手指向左滑,實際上View做的是向右移動。
也就是說,這個時候,getScroolX的值就滿足大於0的條件了。而不巧的是,我們的手指這個時候正在抽瘋,
飛速的連貫動作下,這個時候我們又開始了向右滑動手指的操作,那麼,就出現了之前的bug了。
為了解決這個抽瘋導致的問題,我們可以給每一屏的view添加pageIndex。
實際上,這是一個一舉兩得的方式,因為這樣做我們不僅可以通過新的判斷方式避免這種bug,還可以獲取當前屏幕view的index,舉例viewpager的效果又近了一點。
現在,我們為我們的自定義試圖添加如下成員變量:
private int mCurrentIndex = 1, mPagesCount;
然後修改代碼:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = getChildCount();
MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
layoutParams.width = mScreenWidth * count;
setLayoutParams(layoutParams);
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
child.layout(i * mScreenWidth, 0, (i + 1) * mScreenWidth, b);
mPagesCount++;
}
}
case MotionEvent.ACTION_MOVE:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
int dx = mLastX - x;
if (dx < 0) {
// 新的判斷方式
if(mCurrentIndex > 1){
scrollBy(dx,0);
}
} else if (dx > 0) {
if (mCurrentIndex < mPagesCount) {
scrollBy(dx, 0);
}
}
mLastX = x;
break;
case MotionEvent.ACTION_UP:
mEndX = getScrollX();
int movedX = mEndX - mStartX;
if (movedX > 0) {
if (movedX < mScreenWidth / 3) {
// 回到原處
mScroller.startScroll(getScrollX(), 0, -movedX, 0);
} else {
mScroller.startScroll(getScrollX(), 0, mScreenWidth - movedX, 0);
mCurrentIndex ++;
}
} else {
if (-movedX < mScreenWidth / 3) {
mScroller.startScroll(getScrollX(), 0, -movedX, 0);
} else {
mScroller.startScroll(getScrollX(), 0, -mScreenWidth - movedX, 0);
mCurrentIndex --;
}
}
這個時候,就不會再出現之前的bug了。
當你建立Unity 的手機游戲你最可能渴望設置Script Call Optimization為Fast But No Exceptions,只要你相信你能做到。Fast
一、應用的啟動啟動方式通常來說,在安卓中應用的啟動方式分為兩種:冷啟動和熱啟動。 1、冷啟動:當啟動應用時,後台沒有該應用的進程,這時系統會重新創建一個新的進程分配給該
前言已經好長時間沒更新博客了,今天給大家帶來一個橫向滾動的菜單,用的是HorizontalScrollView,但HorizontalScrollView不能在滾動時定位
在開發中發現一個問題:當一個我通過Intent開啟一個前面已經打開的activty的界面時,新打開的activity的狀態會丟失。當時,當我直接按home減將acitvi