下載本文檔
版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領
文檔簡介
【移動應用開發(fā)技術】Android中怎么實現(xiàn)嵌套滾動
Android中怎么實現(xiàn)嵌套滾動,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面在下將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。業(yè)務需求是:VT容器可以滾動;書籍封面可以滾動,并且有視差;當VT容器滾動到頂部時,滾動列表,并且滾動可以銜接。當列表滾動到頂部時,可以滾動書籍封面以及VT容器,并且滾動可以銜接邏輯清楚了,接下來就看如何實現(xiàn)了。在android5以前,對于這種滾動,我們只能選擇自己去攔截事件并處理,但在后面的某個版本,android推出了NestingScroll機制,開發(fā)者的日子就好過多了,并且android提供了一個非常好的容器類:CoordinatorLayout,極大的簡化了開發(fā)者的工作。當然我們也需要投入精力去學習并運用這些新的Api了。當然,我們也要知道如果沒有這些API,我們應當如何去實現(xiàn)這些效果。因此本文會用三種方式去實現(xiàn)這個效果:純事件攔截與派發(fā)方案基于NestingScroll機制的實現(xiàn)方案基于CoordinatorLayout與Behavior方案的實現(xiàn)示例代碼放在Github上,可以clone下來結合文章觀看純事件攔截與派發(fā)方案這是最為原始的方案,當然也靈活性***的了。其它的方案原理上都是系統(tǒng)基于它提供的封裝。使用這種方案時,我們需要解決以下幾個問題:view的滾動(Scroller);view的速度追蹤(VelocityTracker);當VT容器滾動到頂部時,我們如何將事件傳遞給ListView?當ListView滾動到頂部時,VT容器如何攔截到事件?1、2兩點屬于滾動的基礎知識,這里不會做細致的講解。而第3點為何會出現(xiàn)呢?因為android系統(tǒng)在事件派發(fā)時,如果事件被攔截,那么之后的事件都將不會傳遞給子view了。其解決方案也很簡單:在滾動到頂部時主動派發(fā)一次Down事件:if
(mTargetCurrentOffset
+
dy
<=
mTargetEndOffset)
{
moveTargetView(dy);
//
重新dispatch一次down事件,使得列表可以繼續(xù)滾動
int
oldAction
=
ev.getAction();
ev.setAction(MotionEvent.ACTION_DOWN);
dispatchTouchEvent(ev);
ev.setAction(oldAction);
}
else
{
moveTargetView(dy);
}那么第4點是什么問題呢?這里就需要清楚一個坑點了:不是所用的事件都會走入onInterceptTouchEvent。有一種情況是子View主動調用parent.requestDisallowInterceptTouchEvent(true)來告訴系統(tǒng)說:這個事件我要了,父View不要攔截了。這就是所謂的內部攔截法。在ListView的某些時刻它會去調用這個方法。因此一旦事件傳遞給了ListView,外部容器就拿不到這個事件了。因此我們要打破它的內部攔截:@Override
public
void
requestDisallowInterceptTouchEvent(boolean
b)
{
//
去掉默認行為,使得每個事件都會經(jīng)過這個Layout
}方法如上,把requestDisallowInterceptTouchEvent的實現(xiàn)干掉就可以了。主要的技術點已近提出來了。那么下面就看具體實現(xiàn),首先看使用xml:<org.cgspine.nestscroll.one.EventDispatchPlanLayout
android:id="@+id/scrollLayout"
android:layout_marginTop="?attr/actionBarSize"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:header_view="@+id/book_header"
app:target_view="@+id/scroll_view"
app:header_init_offset="30dp"
app:target_init_offset="70dp">
<View
android:id="@id/book_header"
android:layout_width="120dp"
android:layout_height="150dp"
android:background="@color/gray"/>
<org.cgspine.nestscroll.one.EventDispatchTargetLayout
android:id="@id/scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/white">
<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:background="@drawable/list_item_bg_with_border_top_bottom"
android:layout_width="match_parent"
android:layout_height="@dimen/tab_layout_height"
android:fillViewport="true"/>
<android.support.v4.view.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</org.cgspine.nestscroll.one.EventDispatchTargetLayout>
</org.cgspine.nestscroll.one.EventDispatchPlanLayout>EventDispatchTargetLayout實現(xiàn)了自定義接口ITargetView:public
interface
ITargetView
{
boolean
canChildScrollUp();
void
fling(float
vy);
}這是因為與具體業(yè)務抽離,我并不清楚內層盒子是怎樣的(有可能就是ListView了,也有可能是ViewPager包裹ListView)主要的實現(xiàn)在EventDispatchPlanLayout,使用時在xml中指定header_init_offset、target_init_offset等變量就可以了,基本上與業(yè)務邏輯獨立。其重點實現(xiàn)邏輯在onInterceptTouchEvent與onTouchEvent中了。個人不是很建議去動dispatchTouchEvent,雖然所有事件都會經(jīng)過這里,但是這也明顯會增加代碼處理復雜度:public
boolean
onInterceptTouchEvent(MotionEvent
ev)
{
ensureHeaderViewAndScrollView();
final
int
action
=
MotionEventCompat.getActionMasked(ev);
int
pointerIndex;
//
不阻斷事件的快路徑:如果目標view可以往上滾動或者`EventDispatchPlanLayout`不是enabled
if
(!isEnabled()
||
mTarget.canChildScrollUp())
{
Log.d(TAG,
"fast
end
onIntercept:
isEnabled
=
"
+
isEnabled()
+
";
canChildScrollUp
=
"
+
mTarget.canChildScrollUp());
return
false;
}
switch
(action)
{
case
MotionEvent.ACTION_DOWN:
mActivePointerId
=
ev.getPointerId(0);
mIsDragging
=
false;
pointerIndex
=
ev.findPointerIndex(mActivePointerId);
if
(pointerIndex
<
0)
{
return
false;
}
//
在down的時候記錄初始的y值
mInitialDownY
=
ev.getY(pointerIndex);
break;
case
MotionEvent.ACTION_MOVE:
pointerIndex
=
ev.findPointerIndex(mActivePointerId);
if
(pointerIndex
<
0)
{
Log.e(TAG,
"Got
ACTION_MOVE
event
but
have
an
invalid
active
pointer
id.");
return
false;
}
final
float
y
=
ev.getY(pointerIndex);
//
判斷是否dragging
startDragging(y);
break;
case
MotionEventCompat.ACTION_POINTER_UP:
//
雙指邏輯處理
onSecondaryPointerUp(ev);
break;
case
MotionEvent.ACTION_UP:
case
MotionEvent.ACTION_CANCEL:
mIsDragging
=
false;
mActivePointerId
=
INVALID_POINTER;
break;
}
return
mIsDragging;
}代碼邏輯很清晰,應該不用多說。接下來看onTouchEvent的處理邏輯。public
boolean
onTouchEvent(MotionEvent
ev)
{
final
int
action
=
MotionEventCompat.getActionMasked(ev);
int
pointerIndex;
if
(!isEnabled()
||
mTarget.canChildScrollUp())
{
Log.d(TAG,
"fast
end
onTouchEvent:
isEnabled
=
"
+
isEnabled()
+
";
canChildScrollUp
=
"
+
mTarget.canChildScrollUp());
return
false;
}
//
速度追蹤
acquireVelocityTracker(ev);
switch
(action)
{
case
MotionEvent.ACTION_DOWN:
mActivePointerId
=
ev.getPointerId(0);
mIsDragging
=
false;
break;
case
MotionEvent.ACTION_MOVE:
{
pointerIndex
=
ev.findPointerIndex(mActivePointerId);
if
(pointerIndex
<
0)
{
Log.e(TAG,
"Got
ACTION_MOVE
event
but
have
an
invalid
active
pointer
id.");
return
false;
}
final
float
y
=
ev.getY(pointerIndex);
startDragging(y);
if
(mIsDragging)
{
float
dy
=
y
-
mLastMotionY;
if
(dy
>=
0)
{
moveTargetView(dy);
}
else
{
if
(mTargetCurrentOffset
+
dy
<=
mTargetEndOffset)
{
moveTargetView(dy);
//
重新dispatch一次down事件,使得列表可以繼續(xù)滾動
int
oldAction
=
ev.getAction();
ev.setAction(MotionEvent.ACTION_DOWN);
dispatchTouchEvent(ev);
ev.setAction(oldAction);
}
else
{
moveTargetView(dy);
}
}
mLastMotionY
=
y;
}
break;
}
case
MotionEventCompat.ACTION_POINTER_DOWN:
{
pointerIndex
=
MotionEventCompat.getActionIndex(ev);
if
(pointerIndex
<
0)
{
Log.e(TAG,
"Got
ACTION_POINTER_DOWN
event
but
have
an
invalid
action
index.");
return
false;
}
mActivePointerId
=
ev.getPointerId(pointerIndex);
break;
}
case
MotionEventCompat.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case
MotionEvent.ACTION_UP:
{
pointerIndex
=
ev.findPointerIndex(mActivePointerId);
if
(pointerIndex
<
0)
{
Log.e(TAG,
"Got
ACTION_UP
event
but
don't
have
an
active
pointer
id.");
return
false;
}
if
(mIsDragging)
{
mIsDragging
=
false;
//
獲取瞬時速度
mVelocityTputeCurrentVelocity(1000,
mMaxVelocity);
final
float
vy
=
mVelocityTracker.getYVelocity(mActivePointerId);
finishDrag((int)
vy);
}
mActivePointerId
=
INVALID_POINTER;
//釋放速度追蹤
releaseVelocityTracker();
return
false;
}
case
MotionEvent.ACTION_CANCEL:
releaseVelocityTracker();
return
false;
}
return
mIsDragging;
}或許有人會說:為何與onInterceptTouchEvent與有很多重復代碼?這是因為如果事件不打斷,并且子類不處理,就會走進onTouchEvent邏輯,所以這些重復處理是有意義的(其實是抄SwipeRefreshLayout的)。里面主要的邏輯就是兩個:滾動容器TouchUp時滾動到特定位置以及fling傳遞滾動容器的邏輯:private
void
moveTargetViewTo(int
target)
{
target
=
Math.max(target,
mTargetEndOffset);
//
用offsetTopAndBottom來偏移view
ViewCompat.offsetTopAndBottom(mTargetView,
target
-
mTargetCurrentOffset);
mTargetCurrentOffset
=
target;
//
滾動書籍封面view,根據(jù)TargetView進行定位
int
headerTarget;
if
(mTargetCurrentOffset
>=
mTargetInitOffset)
{
headerTarget
=
mHeaderInitOffset;
}
else
if
(mTargetCurrentOffset
<=
mTargetEndOffset)
{
headerTarget
=
mHeaderEndOffset;
}
else
{
float
percent
=
(mTargetCurrentOffset
-
mTargetEndOffset)
*
1.0f
/
mTargetInitOffset
-
mTargetEndOffset;
headerTarget
=
(int)
(mHeaderEndOffset
+
percent
*
(mHeaderInitOffset
-
mHeaderEndOffset));
}
ViewCompat.offsetTopAndBottom(mHeaderView,
headerTarget
-
mHeaderCurrentOffset);
mHeaderCurrentOffset
=
headerTarget;
}TouchUp的滾動邏輯:private
void
finishDrag(int
vy)
{
Log.i(TAG,
"TouchUp:
vy
=
"
+
vy);
if
(vy
>
0)
{
//
向下觸發(fā)fling,需要滾動到Init位置
mNeedScrollToInitPos
=
true;
mScroller.fling(0,
mTargetCurrentOffset,
0,
vy,
0,
0,
mTargetEndOffset,
Integer.MAX_VALUE);
invalidate();
}
else
if
(vy
<
0)
{
//
向上觸發(fā)fling,需要滾動到End位置
mNeedScrollToEndPos
=
true;
mScroller.fling(0,
mTargetCurrentOffset,
0,
vy,
0,
0,
mTargetEndOffset,
Integer.MAX_VALUE);
invalidate();
}
else
{
//
沒有觸發(fā)fling,就近原則
if
(mTargetCurrentOffset
<=
(mTargetEndOffset
+
mTargetInitOffset)
/
2)
{
mNeedScrollToEndPos
=
true;
}
else
{
mNeedScrollToInitPos
=
true;
}
invalidate();
}
}當然這里會打上一些標志位,具體實現(xiàn)是在computeScroll中,這屬于Scroller的功能,這里就不展開了。這樣大體邏輯就講述清楚了,其它細節(jié)就請看官直接看源碼了。基于NestingScroll機制的實現(xiàn)方案NestingScroll機制是在某個版本support包加入的,不過外界極少有文章介紹,所以應該大多數(shù)人并不知道這個機制。NestingScroll主要有兩個接口:NestedScrollingParentNestedScrollingChild當我們需要使用NestingScroll特性時,我們去實現(xiàn)這兩個接口就好了。NestingScroll本質是內部攔截發(fā)然后將相應的接口開給外界。因此實現(xiàn)NestedScrollingChild接口是有難度的,不過像RecyclerView這些控件,官方已經(jīng)幫我們實現(xiàn)好了NestedScrollingChild,要完成我們的需求,我們直接拿來用就好了(ListView就沒辦法使用了,當然你也可以
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經(jīng)權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
- 6. 下載文件中如有侵權或不適當內容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2024藝術家經(jīng)紀代理合同
- 個人住房公積金質押借款合同2024版B版
- 2025年度貨物供應合同及價格調整機制3篇
- 二零二五年度跨區(qū)域環(huán)保產(chǎn)業(yè)聯(lián)盟合作協(xié)議3篇
- 二零二五年度股東間股權轉讓與公司環(huán)境保護協(xié)議3篇
- 二零二五年度集裝箱式活動中心租賃合同范本3篇
- 小學語文課外閱讀教學與教師專業(yè)成長
- 二零二五年度酒店餐飲業(yè)VI設計及服務質量提升合同3篇
- 2024綜合能源管理服務協(xié)議版B版
- 2025年度蔬菜產(chǎn)業(yè)鏈上下游銷售合作協(xié)議2篇
- 陽離子絡合主體
- 兒科課件過敏性紫癜
- 直腸癌臨床路徑
- 綠化養(yǎng)護工作計劃表
- 漢字拼寫游戲
- GB/T 12310-2012感官分析方法成對比較檢驗
- FZ/T 70010-2006針織物平方米干燥重量的測定
- 銀行貸款批復樣本
- 正數(shù)負數(shù)練習題
- QC成果提高內隔墻ALC板材安裝質量
- 韓國文化-課件
評論
0/150
提交評論