您好,登錄后才能下訂單哦!
在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í)筆記了,各位高手看到了可以忽略
。下面就我最近工作中遇到的一個(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代碼
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代碼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的初始化以及資源釋放的方法
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代碼public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN
&& event.getEdgeFlags() != 0) {
return false;
}
obtainVelocityTracker(event);
final int action = event.getAction();
final float x = event.getX();
final float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
LogUtil.log(TAG, "ACTION_DOWN#currentScrollY:" + getScrollY()
+ ", mLastMotionY:" + mLastMotionY,
LogUtil.LOG_E);
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
final int deltaY = (int) (mLastMotionY - y);
mLastMotionY = y;
if (deltaY < 0) {
if (getScrollY() > 0) {
scrollBy(0, deltaY);
}
} else if (deltaY > 0) {
mIsInEdge = getScrollY() <= childTotalHeight - height;
if (mIsInEdge) {
scrollBy(0, deltaY);
}
}
break;
case MotionEvent.ACTION_UP:
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int initialVelocity = (int) velocityTracker.getYVelocity();
if ((Math.abs(initialVelocity) > mMinimumVelocity)
&& getChildCount() > 0) {
fling(-initialVelocity);
}
releaseVelocityTracker();
break;
}
return true;
}
復(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代碼public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int scrollX = getScrollX();
int scrollY = getScrollY();
int oldX = scrollX;
int oldY = scrollY;
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
scrollX = x;
scrollY = y;
scrollY = scrollY + 10;
scrollTo(scrollX, scrollY);
postInvalidate();
}
}
復(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代碼public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
invalidate();
}
}
}
復(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è)方法很笨,但是很容易出效果的
免責(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)容。