編輯:關於Android編程
其實對於側滑菜單,在博主剛開始學android接觸到的時候,博主是非常感興趣的,也非常想知道它是如何實現的,在技術的不斷上升之後,我也可以自己封裝側滑菜單了.雖然網上有太多的成品等著我去用,但是博主本著學習和分享的態度還是決定寫下這篇博客,也不介意再重復的造一次輪子,但是我相信,每一次重復的造輪子,都是對我技術最好的檢驗!好了下面就帶大家來封裝這個神奇的側滑菜單吧
可以看到實現的效果還是挺棒的,第二種效果和第一種之間就僅僅存在一點區別,就是右邊的視圖會有一個縮放的效果
如果感興趣的話,就跟我一起把這個輪子造出來吧!
其實大家腦子裡面應該能想到,菜單和主界面這是一個怎麼樣的擺放,但是博主還是給出一張自己畫的分析圖,菜單在左邊為例:
圖中黑色為手機屏幕
紅色是菜單視圖
藍色是主界面視圖
這是菜單沒有滑動出來的時候
這是菜單滑動出來的時候
圖畫的不好,大家將就著看一下,其實就是菜單從左邊慢慢的滑出的一個過程
所以其實視圖的擺放還是挺簡單的
1.菜單和主界面緊挨著
2.菜單在左邊,主界面在右邊,主界面的大小就是屏幕的大小(其實是側滑菜單控件的大小,但是為了更好的講解,這裡策劃菜單的大小和屏幕是一樣的,所以這裡就說主界面和屏幕一樣大的,實際大小是你在布局文件中指定側滑控件的大小哦)
/** * Created by cxj on 2016/5/15. * * @author 小金子 * 這個是一個自定義的層疊式的側滑菜單 * 請遵循一下使用原則: * 1.如果是兩個孩子的情況 * 第一個孩子是菜單,默認是左邊 * 第二個是主界面 * 3.不是兩個孩子都會報錯 */ public class ScaleSlideMenu extends ViewGroup { public ScaleSlideMenu(Context context) { this(context, null); } public ScaleSlideMenu(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ScaleSlideMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } /** * 初始化 * * @param context */ private void init(Context context) { this.context = context; scroller = new Scroller(context); vt = VelocityTracker.obtain(); screenWidth = ScreenUtils.getScreenWidth(context); } }構造函數之後用this,這樣子只需要在三個參數的構造函數中調用初始化的方法
onMeasure方法測量孩子和自身
onLayout方法排列孩子的位置
這裡不對這兩個方法做深入的講解,所以不太懂的人請自行百度先了解了解
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //檢查孩子的個數 checkChildCount(); //寬度的計算模式 int widthMode = MeasureSpec.getMode(widthMeasureSpec); //推薦的寬度的值 int widthSize = MeasureSpec.getSize(widthMeasureSpec); //高度的計算模式 int heightMode = MeasureSpec.getMode(heightMeasureSpec); //推薦的高度的值 int heightSize = MeasureSpec.getSize(heightMeasureSpec); //菜單 View menuView = getChildAt(0); //生成菜單的寬度的計算模式和大小,菜單的寬度大小受一個百分比限制 int menuWidthSpec = MeasureSpec.makeMeasureSpec((int) (widthSize * menuPercent), MeasureSpec.AT_MOST); //生成菜單的高度的計算模式和大小 int menuHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST); //讓菜單的試圖部分去測量自己 menuView.measure(menuWidthSpec, menuHeightSpec); //主界面 View mainView = getChildAt(1); //生成主界面的寬度的計算模式和大小 int mainWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST); //生成主界面的高度的計算模式和大小 int mainHeightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST); //讓主界面的試圖部分去測量自己 mainView.measure(mainWidthSpec, mainHeightSpec); //側滑控件的大小呢就直接采用父容器推薦的,所以 //ViewGroup.LayoutParams.WRAP_CONTENT 和 ViewGroup.LayoutParams.MATCH_PARENT 的情況下大小是一致的,這裡做一個說明 //作用是保存自身的大小,沒有這句話控件不顯示,也就是看不到啦 setMeasuredDimension(widthSize, heightSize); //保存自身的寬度和高度,其他地方有用 mWidth = getWidth(); mHeight = getHeight(); }注釋寫的很纖細了,這裡不在全部陳述,只說明一點
這裡菜單的寬度是由一個比例值算出來的,是整個策劃菜單寬度的百分之幾,這個變量可以從代碼中知道是menuPercent,這裡就給使用的人提供了方便,可以修改這個值,來實現自己想要的菜單大小
有關的變量和常量這裡貼出
/** * 最小的菜單寬度占用百分比 */ public static final float MIN_MENUPERCENT = 0.5f; /** * 最大的菜單寬度的占用百分比 */ public static final float MAX_MENUPERCENT = 0.8f; /** * 菜單寬度占屏幕的比例,默認是最小的 */ private float menuPercent = MAX_MENUPERCENT;
/** * 檢查孩子個數 */ private void checkChildCount() { int childCount = getChildCount(); if (childCount != 2) { throw new RuntimeException("the childCount must be 2"); } }這個方法來檢查孩子的個數,如果不是兩個,直接讓程序掛掉就行了
那麼整個測量的方法就介紹完了,其實挺簡單的,而且很多都是套路,你懂得,就是代碼幾乎都一樣嘛
此方法就是計算出每一個孩子的位置信息,然後通過View.layout(l,t,r,b)方法來保存或者設置每一個孩子的位置信息
所以在保存位置信息之前是不是需要計算每一個孩子的位置信息先吶?
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //計算所有孩子的位置信息 computePosition(); //獲取孩子的個數 int childCount = getChildCount(); int size = rectEntities.size(); for (int i = 0; i < size && i < childCount; i++) { View v = getChildAt(i); RectEntity rectEntity = rectEntities.get(i); v.layout(rectEntity.leftX, rectEntity.leftY, rectEntity.rightX, rectEntity.rightY); } }
/** * 存儲孩子的位置信息,由方法{@link ScaleSlideMenu#computePosition()}計算而來 */ private ListrectEntities = new ArrayList (); /** * 計算所有孩子的位置信息 * * @return */ private void computePosition() { rectEntities.clear(); //獲取孩子的個數 int childCount = getChildCount(); if (childCount == 2) { //如果是兩個孩子的情況 //第一個孩子是一個菜單 View menuView = getChildAt(0); //闖進第一個孩子的位置參數 RectEntity menuRect = new RectEntity(); if (menuGravity == MENU_GRAVITY_LEFT) { //如果菜單是在左邊的 menuRect.rightX = 0; menuRect.leftX = menuRect.rightX - menuView.getMeasuredWidth(); } else { menuRect.leftX = mWidth; menuRect.rightX = menuRect.leftX + menuView.getMeasuredWidth(); } menuRect.leftY = 0; menuRect.rightY = menuRect.leftY + menuView.getMeasuredHeight(); //第二個孩子是一個主界面 View mainView = getChildAt(1); //闖進第一個孩子的位置參數 RectEntity mainRect = new RectEntity(); mainRect.leftX = 0; mainRect.rightX = mainRect.leftX + mainView.getMeasuredWidth(); mainRect.leftY = 0; mainRect.rightY = mainRect.leftY + mainView.getMeasuredHeight(); //添加位置信息到集合 rectEntities.add(menuRect); rectEntities.add(mainRect); } else { //如果是三個孩子的情況 } }
此方法涉及到一個對象RectEntity,其實就是Java的面向對象思想,把一個孩子的位置信息封裝到一個類中
這裡有四個變量分別是矩形左上角的橫縱坐標和右下角的橫縱坐標,可能有人有疑問,這兩個點就能確定一個矩形了?
顯然不能,但是這裡的矩形隱含了一個條件,就是矩形是水品放置的,所以左上角的坐標和右下角的坐標已經足夠了
/** * 一個實體類,描述一個矩形的左上角的點坐標和右下角的點的坐標 * * @author cxj QQ:347837667 * @date 2015年12月22日 * */ public class RectEntity { // 左上角橫坐標 public int leftX; // 左上角縱坐標 public int leftY; // 右下角橫坐標 public int rightX; // 右下角縱坐標 public int rightY; }
到這裡為止,其實就已經可以看到主界面的試圖了,不過還不能有任何的效果和滑動,這個下面馬上帶你實現
這裡先給出我隨便弄得布局文件
這裡可以看到策劃菜單這個是填充父容器的,沒有任何內邊距和外邊距,那麼其實就是屏幕的大小啦
菜單xml
public class MainActivity extends Activity { /** * 側滑控件 */ private ScaleSlideMenu sm = null; /** * 主界面左上角的小菜單圖標 */ private ImageView iv_menu = null; /** * 顯示菜單數據的 */ private ListView lv = null; /** * listview要用到的數據 */ private Listdata = new ArrayList (); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //尋找控件 sm = (ScaleSlideMenu) findViewById(R.id.sm); iv_menu = (ImageView) findViewById(R.id.menu); lv = (ListView) findViewById(R.id.lv); //准備菜單的數據 for (int i = 0; i < 20; i++) { data.add("菜單條目" + i); } lv.setAdapter(new CommonAdapter (this, data, android.R.layout.simple_list_item_1) { @Override public void convert(CommonViewHolder h, String item, int position) { h.setText(android.R.id.text1, item); } }); } public void clickView(View view) { Toast.makeText(this, "點我按鈕了", Toast.LENGTH_LONG).show(); } }
代碼到這裡,你運行以後應該就是下面這樣子了
不具備任何的效果和滑動
要有滑動效果,肯定是手指滑動吧?所以肯定是重寫onTouchEvent方法吧?所以,go
private int currentX; private int currentY; private int finalX; private int finalY; @Override public boolean onTouchEvent(MotionEvent e) { //獲取事件的類型(動作) int action = e.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: //按下 //保存按下的時候的坐標 currentX = (int) e.getX(); currentY = (int) e.getY(); break; case MotionEvent.ACTION_MOVE: //移動 //保存移動之後的新的坐標點 finalX = (int) e.getX(); finalY = (int) e.getY(); //如果新舊坐標不一致 if (finalX != currentX || finalY != currentY) { //計算水平距離和垂直距離 int dx = finalX - currentX; int dy = finalY - currentY; //讓整個試圖跟著手指移動起來 scrollBy(-dx, 0); //移動之後舊的坐標進行更新 currentX = (int) e.getX(); currentY = (int) e.getY(); } break; case MotionEvent.ACTION_UP://抬起 break; } return true; }
加上這段代碼之後,你的策劃菜單就可以滾動啦!目前的效果
可以看到我們的試圖是可以滾動了
然後先實現平滑的打開菜單和關閉菜單,這裡提供給用戶兩個方法,分別用來打開和關閉菜單
/** * 打開菜單 */ public void openMenu() { if (menuGravity == MENU_GRAVITY_LEFT) { //拿到菜單的位置參數 RectEntity menuRect = rectEntities.get(0); smoothTo(menuRect.leftX); } else { //拿到菜單的位置參數 RectEntity menuRect = rectEntities.get(0); smoothTo(menuRect.rightX - menuRect.leftX); } preIsMenuOpen = isMenuOpen; isMenuOpen = true; } /** * 關閉菜單 */ public void closeMenu() { smoothTo(0); preIsMenuOpen = isMenuOpen; isMenuOpen = false; }
/** * 平滑的移動到指定位置 */ private void smoothTo(int finalX) { isScrolling = true; scroller.startScroll(getScrollX(), 0, finalX - getScrollX(), 0, defalutDuring); removeRepeatData();//消除重復數據 scrollTo(scroller.getCurrX(), 0); }在這裡可以看到Scroller類被使用到了,它幫我們產生從起始點到重點過程中的軌跡點
但是數據並不是一次性全部產生的,而是每次調用scroller.computeScrollOffset();方法後才會產生下一個新的數據
比如(1,1)到(10,10),產生的一些列數據大概如下:
(1,1),(2,2),(3,3),(4,4)......(10,10)
當然了,它產生的數據可比我舉例的多得多,甚至很多都是重復的數據,也就是所謂的比較密集吧,所以這裡為了消除重復的那些數據,編寫了一個removeRepeatData()的方法來消除重復的數據,讓它新舊數據之間相差至少為1
/** * 用於消除滑動的時候出現的重復數據 * 因為平滑的滑動的時候產生的數據很多都是重復的 * 都是所以這裡如果遇到事重復的就拿下一個,直到不重復為止 * 1.當前的值{@link View#getScrollX()}不等於{@link Scroller#getCurrX()} * 2. */ private void removeRepeatData() { //獲取當前的滾動的值 int scrollX = getScrollX(); //如果當前的值不是最後一個值,並且當前的值等於scroller中的當前值,那麼獲取下一個值 while (scrollX != scroller.getFinalX() && scrollX == scroller.getCurrX()) { scroller.computeScrollOffset(); } }
scrollTo(int x, int y)方法是View中的方法,可以讓試圖滾動到某一點,而我們要使用試圖平滑的滾動,你肯定不能直接滾動到目標點,你肯定需要讓Scroller類產生軌跡,然後反復的調用scrollTo(int x, int y)方法滾動到每一個點,從而實現效果上的平滑滾動
所以方法smoothTo中的scrollTo(scroller.getCurrX(), 0);語句就是讓試圖滾動到了第一個點
在View中當滾動完成後會調用computeScroll()方法,所以我們就可以在這個方法中讓視圖繼續滾動到下一個點
@Override public void computeScroll() { //當View完成滾動的時候調用 //如果軌跡中還有沒有滾完的點 if (scroller.computeScrollOffset()) { removeRepeatData();//消除重復數據 scrollTo(scroller.getCurrX(), 0); } }上述實現平滑的滾動的方法是每一個能平滑滾動的控件的一個基本的實現方法,所以閱讀的你,需要掌握!
然後看現在的效果圖
到這裡為止實現了平滑的打開菜單,關閉菜單同理,不再詳細解釋
但是拖動的時候只是拖動到哪裡就是哪裡,不會自動判斷當前的位置是應該打開菜單還是關閉菜單
所以下一步就是在手指抬起的時候判斷當前的位置是應該打開菜單還是關閉菜單
判斷之後讓試圖平滑的滑動到位置
但是還有其他細節的處理,所以這裡就直接給出了全部的觸摸的事件的處理代碼,還請見諒,博主會做一個詳細的解釋
@Override public boolean onTouchEvent(MotionEvent e) { //獲取事件的類型(動作) int action = e.getAction(); //如果按下的,進行有效點的判讀,只有接近邊緣小於某一個百分比,才可以滑出菜單 if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { //拿到按下的時候的橫左邊 float x = e.getX(); if (menuGravity == MENU_GRAVITY_LEFT) { //如果菜單在左邊 if (x / ((Number) screenWidth).floatValue() < slidePercent) { //如果距離邊界的百分比小於設置的百分比 isMyEvent = true; } } else { if ((mWidth - x) / ((Number) screenWidth).floatValue() < slidePercent) { //如果距離邊界的百分比小於設置的百分比 isMyEvent = true; } } } //如果不需要側滑出菜單,並且菜單是關閉狀態 if (!isMyEvent && !isMenuOpen && !isScrolling) { return false; } vt.addMovement(e); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: //按下 isMove = false; //保存按下的時候的坐標 currentX = (int) e.getX(); currentY = (int) e.getY(); scroller.setFinalX(currentX); scroller.abortAnimation(); break; case MotionEvent.ACTION_MOVE: //移動 //保存移動之後的新的坐標點 finalX = (int) e.getX(); finalY = (int) e.getY(); //如果新舊坐標不一致 if (finalX != currentX || finalY != currentY) { //計算水平距離和垂直距離 int dx = finalX - currentX; int dy = finalY - currentY; //讓整個試圖跟著手指移動起來 scrollBy(-dx, 0); scaleMainView(); //移動之後舊的坐標進行更新 currentX = (int) e.getX(); currentY = (int) e.getY(); isMove = true; } break; case MotionEvent.ACTION_UP://抬起 if (isMove) { //計算速度 vt.computeCurrentVelocity(1000, Integer.MAX_VALUE); //水平方向的速度 float xVelocity = vt.getXVelocity(); //如果速度的絕對值大於200,我們就認為試圖有一個拋出的感覺 //如果速度達到了 if (xVelocity > 200) { if (menuGravity == MENU_GRAVITY_LEFT) { openMenu(); } else { closeMenu(); } } else if (xVelocity < -200) { if (menuGravity == MENU_GRAVITY_LEFT) { closeMenu(); } else { openMenu(); } } else { judgeShouldSmoothToLeftOrRight(); } } else { closeMenu(); } vt.clear(); isMove = false; isMyEvent = false; break; } return true; }
首先在觸摸事件的最開始有一些代碼是一個判斷,在判斷什麼呢?你想一下,我們不一定在主界面的任何地方滑動都希望滑出菜單,所以這裡就有一個限制,如果你手指剛剛觸碰到屏幕的時候,如果你距離屏幕兩邊的距離和屏幕寬度的壁紙小於指定的百分比,策劃菜單則認為你要滑出菜單,所以你可以看到有一個變量isMyEvent,這個變量就是記錄判斷之後是不是認為要滑出菜單,如果不要並且菜單是關閉狀態,並且試圖不在滑動,則直接返回false,表示我不需要這個事件,你們愛誰處理就誰處理去吧~~~
然後往下看,有一句vt.addMovement(e);
還記得開頭前言的下面我有說過這個vt這個類的作用,是一個可以計算出用戶在觸摸的時候產生的速度的一個類,用法很簡單,就是調用vt.addMovement(e);,在抬起的時候你就可以獲取速度啦
在移動的動作事件裡面有這麼一句話:scaleMainView();這個最後解釋
最後看抬起事件中的代碼,通過判斷isMove變量得知,手指在屏幕中是否滑動了,如果滑動了則計算出因滑動產生的速度,如果速度如注釋所說,就有一個拋出的感覺,那麼根據速度的方向和菜單的位置來選擇應該是打開還是關閉菜單
拋出的這段的效果來一張
可以看到效果棒棒哒~~~~
最後來揭露一下為什麼這裡的縮放效果我沒介紹,但是上述的效果中都已經實現了,這是因為真的就是一行代碼
介紹一個方法:
/** * 縮放主界面 */ private void scaleMainView() { if (slideMode == SCALE_SLIDE_MODE) { //如果模式是縮放的模式 float percent = ((Number) Math.abs(getScrollX())).floatValue() / ((Number) Math.abs(rectEntities.get(0).leftX)).floatValue(); getChildAt(1).setScaleX(1f - 0.4f * (percent)); getChildAt(1).setScaleY(1f - 0.4f * (percent)); } }
這個方法的注釋寫出了這個方法的功能,就是縮放主界面,而方法中是根據什麼進行縮放的呢?
就是根據當前的視圖滾動的偏移量和菜單的寬度的計算出百分比
然後使用這個百分比設置主界面View的縮放比例,這裡利用數學上的知識做了一個限制,可以看到
percent的值區間是[0-1],所以整個1-0.4*percent的區域就是[0.6-1],所以不會出現縮放太誇張的現象!
所以這個方法在視圖滾動的時候調用即可實現縮放的效果!
第一個需要調用這個方法地方就是平滑滾動的時候
@Override public void computeScroll() { //當View完成滾動的時候調用 //如果軌跡中還有沒有滾完的點 if (scroller.computeScrollOffset()) { removeRepeatData();//消除重復數據 scrollTo(scroller.getCurrX(), 0); scaleMainView(); } }
第二個需要調用的地方就是我們的手指去滑動的時候
case MotionEvent.ACTION_MOVE: //移動 //保存移動之後的新的坐標點 finalX = (int) e.getX(); finalY = (int) e.getY(); //如果新舊坐標不一致 if (finalX != currentX || finalY != currentY) { //計算水平距離和垂直距離 int dx = finalX - currentX; int dy = finalY - currentY; //讓整個試圖跟著手指移動起來 scrollBy(-dx, 0); scaleMainView(); //移動之後舊的坐標進行更新 currentX = (int) e.getX(); currentY = (int) e.getY(); isMove = true; } break;
好了,到這裡就基本上完工了,最後的最後需要處理的一點事情就是在我們的菜單打開的時候,菜單部分的試圖的事件不攔截,但是主界面的事件要絕對的攔截!
所以需要重寫onInterceptTouchEvent(MotionEvent ev)方法
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: //按下 //獲取到按下的時候的相對於屏幕的橫坐標x int x = (int) ev.getX(); //攔截菜單打開的情況下主界面的事件 if (isMenuOpen) { //菜單的位置參數對象 RectEntity menuRect = rectEntities.get(0); if (menuGravity == MENU_GRAVITY_LEFT) { //如果菜單在左邊 if (x > Math.abs(menuRect.leftX)) { //滿足說明了點擊的是主界面的區域 return true; } } else { //菜單在右邊 if (x < (mWidth - (menuRect.rightX - menuRect.leftX))) { return true; } } } } return super.onInterceptTouchEvent(ev); }
https://github.com/xiaojinzi123/xiaojinzi-openSource-view/tree/master/xiaojinzi/view/scaleSlideMenu
首先聲明本文是基於GitHub上baoyongzhang的SwipeMenuListView修改而來,該項目地址:https://github.com/baoyongzh
我們都知道Logcat是我們Android開發調試最常用的一個工具,但是Android Studio默認的Logcat調試的顏色是一樣的,我們不好區分verbose、de
接到一個新的任務,對現有項目進行代碼混淆。之前對混淆有過一些了解,但是不夠詳細和完整,知道有些東西混淆起來還是比較棘手的。不過幸好目前的項目不是太復雜(針對混淆這塊來說)
本文實例為大家分享了ActionBar下拉式導航的實現代碼,供大家參考,具體內容如下利用Actionbar同樣可以很輕松的實現下拉式的導航方式,若想實現這種效果:1)ac