編輯:關於Android編程
看標題就知道這篇文章講的主要是view滑動的相關內容。
先看下源碼:
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();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
可以看到scrollBy其實也是調用了scrollTo,區別就是scrollBy是根據相對位置移動,而scrollTo是移動到指定的位置,與原來位置沒什麼關系。
不過這兩個方法移動的是view中的內容,而不是view。舉個例子,如果是textview的話,那麼滑動的是控件中的文字,而textview本身並不會移動。scrollTo很簡單,就不多說了。那來解釋下scrollBy中的變量mScrollX和mScrollY。
mScrollX和mScrollY記錄的是當前view內容所處的位置(移動後所處的位置,一開始為0),這兩個值分別有正值和負值。
mScrollX = view.left - viewcontent.left;
mScrollY = view.top - viewcontent.top;
這裡的view.left指的是該view的左邊框。viewcontent就指的是view內容的左邊框。
看個例子就是移動TextView中的內容:
看到這裡應該明白scrollTo和scrollBy的用法了,不過這種方法有2個缺陷:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4KCgrI57n7v9i8/srHyrnTw3dyYXBfY29udGVudLXEu7CjrMjnufvSxravxNrI3bvhtbzWwsTayN2xu9Xa16GjrL+0sru8+6GjCta00NBzY3JvbGxUb7rNc2Nyb2xsQnm6r8r9tcS7sKOsu+HLsrzk0sa2r6Os08O7p8zl0emyu7rDo6zI57n7u6y2r7XEvuDA67HIvc+087XEu7CjrNPDu6e/ycTcuPzPo837t9a24Lj20KGyvbustq/N6qOstviyu8rH0ruyvb7Nu6zN6qGjxMfV4rj2yrG68lNjcm9sbGVyvs3FycnP08OzocHLo6zL+77NysfAtL3ivvbI57rO0ruyvbK9u6y2r83qxOPP69KqtcS+4MDroaMKCgoKCjxoMiBpZD0="scroller">Scroller
首先需要解釋的是,Scroller並沒有直接操作View的移動,看源代碼就知道Scroller中並沒有一個記錄view的變量。Scroller中許多變量都是int變量,Scroller的工作就是根據你提供的數據(從哪個坐標開始移動,橫向縱向各走多少停下,移動時間總共要花多久)來計算出每一步你應該走在哪個坐標,computeScrollOffset方法來計算你是否需要繼續走,如果返回true,則view還需要繼續滑動,你可以通過getCurrX和getCurrY方法來得到下一步應該移動到哪一步,然後你可以通過scrollTo和scrollBy來移動view;如果返回false,則說明已經走到終點。
現在結合Scroller和scrollTo來展示一個例子:
主要是利用TextView展示如何滑動文字,想要的效果是讓Textview中的文字向右下方滑動50個像素。
首先需要自定義一個TextView。
public class ScrollTextView extends TextView {
public ScrollTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
private Scroller mScroller = new Scroller(getContext());
public void startScroll() {
mScroller.startScroll(0, 0, -50, -50, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
}
上面代碼中startScroll函數中,告訴scroller在1秒內開始從(0,0)開始向右向下移動50個像素。首先解釋下為什麼是-50,因為在前面介紹scrollTo中講到過,mScrollX是等於view.left - viewcontent.left,所以如果你向右平移的話,mScrollX的值為負數(view的左邊框在viewcontent的左邊,減一減當然為負數啦)。
剛才講到scroller其實是把一步ScrollTo分成好多步來走,對於每一步的話都是調用onDraw來重新繪制,所以看起來是在滑動的。每次Draw的時候都會調用computeScroll函數,可以在該函數中,去獲取每一步的坐標,然後調用scrollTo函數,然後去刷新(invalidate函數)。
接下來就是在xml中定義一個ScrollTextView了,需要注意的是,要把大小弄得大一點:
public class MainActivity extends AppCompatActivity {
private ScrollTextView tview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tview = (ScrollTextView) findViewById(R.id.stv);
}
public void onClick(View view) {
tview.startScroll();
}
}
為什麼要把ScrollTextView的大小寫的大點?如果你layout_width和layout_height是wrap_content的話,那你滑動content的話,就看不見了,這就是scrollto的缺點,滑動到view的區域外就看不見了!!!ViewDragHelper就是解決這個的。
Scroller還有一個要說的就是fling,fling的意思是急沖的意思。在Scroller中有兩種模式,一種是SCROLL_MODE,另一個是FLING_MODE。
這是一個工具類,主要是方便我們自定義ViewGroup,他提供一些有用的操作並且跟蹤狀態,允許用戶在父viewgroup中去drag(拖)和reposition(重定位)一些child views。
Android ViewDragHelper完全解析 自定義ViewGroup神器這篇文章講解了ViewDragHelper中是怎麼用的以及API,這裡就不詳述了。
那ViewDragHelper實現的原理是怎麼樣的呢?
在剛剛提供的那篇文章中,有一個demo,我運行了下,給大家看下效果:
我觸碰第一個TextView並將其拖拽至其他地方。但是在上述效果中看到,移動的只有文字,TextView的邊界並沒有移動,還是在原來的左上方位置。
ViewDragHelper是用在ViewGroup中,而不是用在View中的,他的作用是移動ViewGroup中的child view。這裡你需要知道ViewGroup事件傳遞的相關知識,可以看該篇文章View的事件分發。ViewDragHelper主要是攔截ViewGroup的觸摸事件,根據手勢滑動的軌跡來滑動View。ViewDragHelper中也用到了Scroller,但是其繪制的時候,卻不是通過scrollTo方法的,是通過ViewCompat.offsetLeftAndRight和ViewCompat.offsetTopAndBottom方法。ViewDragHelper中類似Scroller的computeScrollOffset的方法是continueSettling,可以通過這個方法來繪制View,看下源碼:
public boolean continueSettling(boolean deferCallbacks) {
if (mDragState == STATE_SETTLING) {
boolean keepGoing = mScroller.computeScrollOffset();
final int x = mScroller.getCurrX();
final int y = mScroller.getCurrY();
final int dx = x - mCapturedView.getLeft();
final int dy = y - mCapturedView.getTop();
if (dx != 0) {
ViewCompat.offsetLeftAndRight(mCapturedView, dx);
}
if (dy != 0) {
ViewCompat.offsetTopAndBottom(mCapturedView, dy);
}
if (dx != 0 || dy != 0) {
mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
}
if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
// Close enough. The interpolator/scroller might think we're still moving
// but the user sure doesn't.
mScroller.abortAnimation();
keepGoing = false;
}
if (!keepGoing) {
if (deferCallbacks) {
mParentView.post(mSetIdleRunnable);
} else {
setDragState(STATE_IDLE);
}
}
}
return mDragState == STATE_SETTLING;
}
使用ViewCompat中的兩個方法就可以重新繪制View的content,而對於childView來說,他們什麼代碼都不用變。這種方法替代了ScrollTo,即使content超過了view的邊界,也依舊可以顯示。
這裡需要注意的是ViewCompat中的兩個函數傳入的值,是針對全頁面的絕對位置,而不是針對View邊界的相對位置。
例如傳進scrollTo(10,10)的話,是將content以view邊界為中心,向左上方偏移10,10。如果傳進ViewCompat.offsetLeftAndRight(view, 100)的話,則是將content往右偏移100像素,這與scrollTo是不一樣的(主要是方向不一樣!!!!)。
另外,如果你執行scrollTo(-10,0);這句代碼兩次,content只會向右平移10像素,但是你如果執行ViewCompat.offsetLeftAndRight(view, 10)兩次,那麼content會向右平移20像素,這個是非常重要的區別。因為scrollTo是將傳入的值10賦值給mScrollX,所以無論你執行多少次,它的content都只會向右平移10,但是ViewCompat不同,他不管你當前位置在哪裡,只要你傳入的值不為0,他就會直接向右移動,所以你執行那個函數n次,那麼就會向右移動10n像素。
那根據這個用法,我們修改剛剛scroller + scrollTo的例子,看下代碼:
public class ScrollTextView extends TextView {
public ScrollTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
private Scroller mScroller = new Scroller(getContext());
public void startScroll() {
mScroller.startScroll(getLeft(), getTop(), 50, 50, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
Log.d("tag", "computeScroll : " + mScroller.getCurrX() + "," + mScroller.getCurrY() + "," + mScroller.getFinalX() + "," + mScroller.getFinalY());
ViewCompat.offsetLeftAndRight(this, mScroller.getCurrX() - getLeft());
ViewCompat.offsetTopAndBottom(this, mScroller.getCurrY() - getTop());
invalidate();
}
}
}
看下修改的地方,第一個主要是調用startScroll傳入的參數不同,這裡傳入的是getLeft、getTop、50,50。先講下後面兩個參數,因為剛剛說過ViewCompat若傳入正值為向右,傳入負值為向左,則按照需求來,這裡應該傳入整數。
在下面computeScroll函數中,調用ViewCompat的兩個方法來進行偏移,接下來先看下打印出的tag日志。
D: computeScroll : 0,237,50,287
D: computeScroll : 1,238,50,287
D: computeScroll : 4,241,50,287
D: computeScroll : 7,244,50,287
D: computeScroll : 10,247,50,287
D: computeScroll : 14,251,50,287
D: computeScroll : 17,254,50,287
D: computeScroll : 22,259,50,287
D: computeScroll : 25,262,50,287
D: computeScroll : 28,265,50,287
D: computeScroll : 31,268,50,287
D: computeScroll : 33,270,50,287
D: computeScroll : 35,272,50,287
D: computeScroll : 37,274,50,287
D: computeScroll : 40,277,50,287
D: computeScroll : 43,280,50,287
D: computeScroll : 44,281,50,287
D: computeScroll : 45,282,50,287
D: computeScroll : 46,283,50,287
D: computeScroll : 47,284,50,287
D: computeScroll : 47,284,50,287
D: computeScroll : 47,284,50,287
D: computeScroll : 48,285,50,287
D: computeScroll : 48,285,50,287
D: computeScroll : 48,285,50,287
D: computeScroll : 48,285,50,287
D: computeScroll : 49,286,50,287
D: computeScroll : 49,286,50,287
D: computeScroll : 49,286,50,287
D: computeScroll : 49,286,50,287
D: computeScroll : 49,286,50,287
D: computeScroll : 49,286,50,287
D: computeScroll : 50,287,50,287
D: computeScroll : 50,287,50,287
D: computeScroll : 50,287,50,287
D: computeScroll : 50,287,50,287
從日志中我們可以看出有的時候scroller算出的每一步其實與上一步有重復,一開始getCurrX還是遞增,但是到後面,就出現一些重復的getCurrX,getCurrY也是一樣的規律,這是為什麼呢,讓我們看看代碼computeOff的代碼:
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,所以處於SCROLL_MODE模式下。在我們調用startScroll的時候,傳入的時間是1000ms,也就是一秒。看代碼可以知道,在SCROLL_MODE模式下,此次滑動結束唯一的條件就是時間到,即使currX和currY已經到達終點也還得繼續。
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
看下這段代碼,首先會根據當前已經流逝的時間算出占整個時間的百分比,然後乘上deltaX(就是我們傳進去的50),來計算出我們當前在x軸方向上應該處於哪一個位置。然後使用math.round四捨五入,所以有的時候每走一步的時候,會發現四捨五入之後,和上一步的結果是一樣,相當於這一步沒動過。所以這解釋了我們剛剛貼出來的日志中存在重復的坐標。但是對於FLING_MODE不同,他結束的條件有兩種,第一種和SCROLL_MODE一樣,時間到了也就會停止,第二種就是如果currX和currY已經到達終點,則也會停止,即使時間沒到也會停止,這和SCROLL_MODE是不一樣的。
好,現在為止應該能理解剛剛的代碼了,那我現在再給出一份代碼,看看大家能不能看出什麼問題來!
public class ScrollTextView extends TextView {
public ScrollTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
private Scroller mScroller = new Scroller(getContext());
public void startScroll() {
mScroller.startScroll(0, 0, 50, 50, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
Log.d("tag", "computeScroll : " + mScroller.getCurrX() + "," + mScroller.getCurrY() + "," + mScroller.getFinalX() + "," + mScroller.getFinalY());
ViewCompat.offsetLeftAndRight(this, mScroller.getCurrX());
ViewCompat.offsetTopAndBottom(this, mScroller.getCurrY());
invalidate();
}
}
}
讀者可以思考下下面這段代碼能不能達到我們想要的效果:將content向右下方移動50像素。
事實上這是達不到我們的效果的,實際上的效果是content向右下方不斷移動,在我的手機上content一直平移至滑出屏幕了,那為什麼會這樣呢?
看下日志就知道了
D: computeScroll : 0,0,50,50
D: computeScroll : 1,1,50,50
D: computeScroll : 2,2,50,50
D: computeScroll : 4,4,50,50
D: computeScroll : 7,7,50,50
D: computeScroll : 10,10,50,50
D: computeScroll : 13,13,50,50
D: computeScroll : 17,17,50,50
D: computeScroll : 21,21,50,50
D: computeScroll : 25,25,50,50
D: computeScroll : 28,28,50,50
D: computeScroll : 31,31,50,50
D: computeScroll : 33,33,50,50
D: computeScroll : 35,35,50,50
D: computeScroll : 37,37,50,50
D: computeScroll : 39,39,50,50
D: computeScroll : 40,40,50,50
D: computeScroll : 41,41,50,50
D: computeScroll : 42,42,50,50
D: computeScroll : 43,43,50,50
D: computeScroll : 44,44,50,50
D: computeScroll : 45,45,50,50
D: computeScroll : 46,46,50,50
D: computeScroll : 46,46,50,50
D: computeScroll : 47,47,50,50
D: computeScroll : 47,47,50,50
D: computeScroll : 47,47,50,50
D: computeScroll : 48,48,50,50
D: computeScroll : 48,48,50,50
D: computeScroll : 49,49,50,50
D: computeScroll : 49,49,50,50
D: computeScroll : 49,49,50,50
D: computeScroll : 49,49,50,50
D: computeScroll : 49,49,50,50
D: computeScroll : 49,49,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
看前面幾行就知道了,在第二行執行時,content向右平移1像素,向下平移1像素,在執行第三行時,content向右平移2,向下平移2像素,到現在為止,content已經向下平移1+2 = 3像素了,向右平移1+2=3像素了,但是content在執行完第三行時,按照預想應該只向右平移2像素,向下平移2像素,但是現實卻是多平移了,所以按照日志來看,當全部運行完,content應該是向右平移了1+2+4+7+10……是遠遠大於50像素的,所以這種方法失敗。那為什麼第一種方法成功了呢?
因為在每次content移動的時候,getLeft其實也是相應的改變了,隨著content在移動,所以當getCurrX改變的時候,getLeft也在改變,所以兩者一相減就是正確需要移動的值。
而且下面那種方法在調試的時候又是正確的,這是因為調試的話,每次執行到computeScrollOffset的話,時間不一樣,打斷點的時候,其實時間已經流逝了,所以在調試狀態下,打印出來的日志就兩行,第一行就是0,第二行就是正確移動的地點,很難調試出來發現其中的問題。
下次如果調試是正確的,但是正常運行是錯誤的話,很可能是跟時間有關,例如並發或者類似今天這種問題。
這篇文章是繼自定義EditText樣式之後的功能強化,對於實際應用項目有很大的參考意見,感興趣的朋友可以移步上一篇,”Android Studion自定義Ed
前言最近在做按鈕的時候遇到在給按鈕設置一張圖片作為背景的同時還要自己定義圓角,最簡單的做法就是直接切張圓角圖作為按鈕就可以了,但是如果不這樣該怎麼辦呢,看代碼:下面來看效
1.概述 PigeonCall,中文名“飛鴿電話”,是一款Android平台的VoIP網絡電話應用,但只工作於局域網,支持給任意局域網內
開始 繼上一次Masterkey漏洞之後,Bluebox在2014年7月30日又公布了一個關於APK簽名的漏洞——FakeID,並打算在今年的Bl