利用假期把以前做的東西總結整理一下,先從簡單的開始吧。
實現的效果是這樣的:
做了個截屏動畫,比例有點不對了,湊合着看吧。
整個窗口有3部分組成,中間的主界面是個列表,左邊的滑出界面是個菜單,右邊的滑出界面是編輯框等。左邊菜單是半屏的,右邊的是全屏的。
最后其實我只用到了左邊的滑出窗口,但還是把左右的算法都做全了。
並且設計了一個方法,讓activity可以指定兩個滑出窗口的寬度。
源碼: http://files.cnblogs.com/inkheart0124/PushSlider.zip
一、自定義PushSlider類 繼承ViewGroup 並實現手勢listener
PushSlider.java
public class PushSlider extends ViewGroup implements OnGestureListener
public static final int SLIDER_PAGE_LEFT = 0; public static final int SLIDER_PAGE_MIDDLE = 1; public static final int SLIDER_PAGE_RIGHT = 2;
三個Child View的index,其中SLIDER_PAGE_MIDDLE是定寬的(全屏寬度),SLIDER_PAGE_LEFT和SLIDER_PAGE_RIGHT可以設置View的寬度。
1 public PushSlider(Context context, AttributeSet attrs) { 2 super(context, attrs); 3 setHapticFeedbackEnabled(false); 4 mContext = context; 5 mForbidden = false; 6 mGDetector = new GestureDetector(mContext,this); 7 focusedIndex = SLIDER_PAGE_MIDDLE; 8 mMoveFlag = MOVE_FLAG_ALLOW_LEFT|MOVE_FLAG_ALLOW_RIGHT; 9 mDensity = getResources().getDisplayMetrics().density; 10 mScroller = new Scroller(mContext, new AccelerateInterpolator()); 11 mChildWidth = new int[3]; 12 13 }
初始化數據:
5行,mForbidden = false; 初始狀態默認允許滑動,activity可以通過mPushView.pushForbid(true)方法禁止滑動。
6行,mGDetector = new GestureDetector(mContext,this); 創建手勢監聽對象。
7行,初始focus的view。
9行,獲取像素密度,后面要用它來計算位置。
10行,Scroller是一個實現View group平滑滾動的一個helper類。mScroller和mGDetector就是后面手勢滑動的主角。
11行,數組mChildWidth[] 記錄每個子view的寬度
重寫onMeasure和onLayout方法:
1 @Override 2 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ 3 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 4 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 5 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 6 if(mChild == null){ 7 initChildren(widthSize,heightSize); 8 } 9 else{ 10 int newSpec = MeasureSpec.makeMeasureSpec(mChildWidth[focusedIndex], MeasureSpec.EXACTLY); 11 mChild[focusedIndex].measure(newSpec, heightMeasureSpec); 12 } 13 }
入參是父view的尺寸,第7行第一次執行onMeasure時會根據父view的寬和高 (這里等於屏的寬和高) 來初始化子view。
1 private void initChildren(int width, int height){ 2 if(mChild == null){ 3 mChild = new View[3]; 4 mChildWidth[SLIDER_PAGE_FIX]=width; 5 } 6 7 for(int i=SLIDER_PAGE_LEFT; i<=SLIDER_PAGE_RIGHT; i++){ 8 mChild[i] = getChildAt(i); 9 if(mChild[i] != null){ 10 mChild[i].setVisibility(View.VISIBLE); 11 int childWidth = mChildWidth[i]; 12 if(childWidth > width){ 13 childWidth = width; 14 mChildWidth[i] = width; 15 } 16 int widthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY); 17 int heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 18 mChild[i].measure(widthMeasureSpec, heightMeasureSpec); 19 } 20 } 21 changeFocus(); 22 }
initChildren方法 初始化數組mChild,記錄每個子view的對象。
4行,定寬的那個子view直接設置成全屏寬度。定寬的是索引為SLIDER_PAGE_MIDDLE的子view。
7~19行,getChildAt(i)獲得ViewGroup中的每個子View,根據每個子view的寬度設置調用它們的measure方法。
因此,主activity必須在onCreate方法中執行pushSlider的setPageWidth方法,設置左右兩個子view的寬度。
mPushView.setPageWidth(PushSlider.SLIDER_PAGE_LEFT, screenWidth/2);
mPushView.setPageWidth(PushSlider.SLIDER_PAGE_RIGHT, screenWidth);
public void setPageWidth(int index, int width){ if(index == SLIDER_PAGE_FIX) return; mChildWidth[index]=width; }
onLayout
1 @Override 2 protected void onLayout(boolean changed, int l, int t, int r, int b) { 3 4 if(changed){ 5 6 if(mChild[focusedIndex] != null){ 7 int w = mChildWidth[focusedIndex]; 8 if(w == 0){ 9 w = r-l; 10 } 11 mChild[focusedIndex].layout(l,t,l+w,b); 12 } 13 14 for(int next=SLIDER_PAGE_LEFT; next<=SLIDER_PAGE_RIGHT; next++){ 15 if(next == focusedIndex){ 16 continue; 17 } 18 if(mChild[next] != null){ 19 int w1 = mChildWidth[next]; 20 int x = 0; 21 if((next== SLIDER_PAGE_LEFT)&&(focusedIndex == SLIDER_PAGE_MIDDLE)){ 22 x = l- w1; 23 }else if((next== SLIDER_PAGE_LEFT)&&(focusedIndex == SLIDER_PAGE_RIGHT)){ 24 x = l- mChildWidth[SLIDER_PAGE_MIDDLE] - w1; 25 }else if((next== SLIDER_PAGE_MIDDLE)&&(focusedIndex == SLIDER_PAGE_LEFT)){ 26 x = l+ mChildWidth[SLIDER_PAGE_LEFT]; 27 }else if((next== SLIDER_PAGE_MIDDLE)&&(focusedIndex == SLIDER_PAGE_RIGHT)){ 28 x = l-w1; 29 }else if((next== SLIDER_PAGE_RIGHT)&&(focusedIndex == SLIDER_PAGE_MIDDLE)){ 30 x = l+mChildWidth[SLIDER_PAGE_MIDDLE]; 31 } 32 else if((next== SLIDER_PAGE_RIGHT)&&(focusedIndex == SLIDER_PAGE_LEFT)){ 33 x = l+mChildWidth[SLIDER_PAGE_LEFT]+mChildWidth[SLIDER_PAGE_MIDDLE]; 34 } 35 mChild[next].layout(x,t,x+w1,b); 36 } 37 } 38 39 mLeftPosX = -mChildWidth[SLIDER_PAGE_LEFT]; 40 if(mChild[SLIDER_PAGE_RIGHT] != null) 41 mRightPosX = mChildWidth[SLIDER_PAGE_MIDDLE]; 42 else 43 mRightPosX = 0; 44 }else{ 45 46 if(mChild[focusedIndex] != null){ 47 int w = mChildWidth[focusedIndex]; 48 mChild[focusedIndex].layout(l,t,l+w,b); 49 } 50 } 51 }
onLayout 沒什么特別的,就是把每個子view布局一下,調用子view的layout(l,t,r,b)方法。
以我的手機屏寬720pixels,中間的view從l=0到r=719,左邊的就是-359到0,右邊的720到1439。
=================================================================
這兩個函數看起來很簡單,無非是算位置煩一點。但是在這里我犯了一個錯誤,導致后面花了很多時間找原因。
我的menifest里是這樣的
<activity
android:name=".MainActivity"
android:label="@string/activity_car"
android:screenOrientation="portrait"
android:configChanges="orientation|keyboardHidden"
>
只支持豎屏。當時的考慮是,對pushSlider來說,onMeasure只需在第一次執行時measure每個子view就可以了,因為子view的尺寸是不會變化的。而onLayout也只需要在changed == true的時候layout子view,其位置相對於父view來說也是固定的。(scroller滾動的是整個父view相對屏的位置)
但是,我的middle view中布局了一個ListView,於是悲劇了。我發現list動態修改item后不會刷新,調用mAdapter.notifyDataSetChanged()后也不刷新。
各種猜測以及試驗以及百度了很久,最后發現ListView的條目發生改變后,父view的onMeasure和onLayout會被調用,這里必須調用list所在view的measure和layout方法,否則就會導致上面的不刷新問題。
=================================================================
這樣三個子view的布局就ok了。在開始滑動之前,還需要定義個監聽接口,讓activity中能監聽到滑動后子view的焦點切換情況。
private OnPageChangedListener mPageChangedListener = null; public interface OnPageChangedListener{ void onPageChanged(View v, int whichHandle); } public void setOnPageChangedListener(OnPageChangedListener listener){ mPageChangedListener = listener; }
activity需要調用mPushView.setOnPageChangedListener(this);注冊監聽,並實現void onPageChanged(View v, int whichHandle),兩個傳參分別是獲得焦點的子view和其索引值。
手勢滑動部分請參考 <Android 開發實踐 ViewGroup 實現左右滑出窗口(二)http://www.cnblogs.com/inkheart0124/p/3534165.html>
