編輯:關於Android編程
在android開發中,滑動對一個app來說,是非常重要的,流暢的滑動操作,能夠給用戶帶來用好的體驗,那麼本次就來講講android中實現滑動有哪些方式。其實滑動一個View,本質上是移動一個View,改變其當前所屬的位置,要實現View的滑動,就必須監聽用戶觸摸的事件,且獲取事件傳入的坐標值,從而動畫的改變位置而實現滑動。
*layout方法
*offsetLetfAndRight()與offsetTopAndBottom()
*LayoutParams
*scrollTo與scrollBy
*Scroller
*屬性動畫
*ViewDragHelper
首先要知道android的坐標系與我們平常學習的坐標系是不一樣的,在android中是將左上方作為坐標原點,向右為x抽正方向,向下為y抽正方向,像在觸摸事件中,getRawX(),getRawY()獲取到的就是Android坐標中的坐標.
android開發中除了上面的這種坐標以外,還有一種坐標,叫視圖坐標系,他的原點不在是屏幕左上方,而是以父布局坐上角為坐標原點,像在觸摸事件中,getX(),getY()獲取到的就是視圖坐標中的坐標.
觸摸事件MotionEvent在用戶交互中,有非常重要的作用,因此必須要掌握他,我們先來看看Motievent中封裝的一些常用的觸摸事件常亮:
//單點觸摸按下動作 public static final int ACTION_DOWN = 0; //單點觸摸離開動作 public static final int ACTION_UP = 1; //觸摸點移動動作 public static final int ACTION_MOVE = 2; //觸摸動作取消 public static final int ACTION_CANCEL = 3; //觸摸動作超出邊界 public static final int ACTION_OUTSIDE = 4; //多點觸摸按下動作 public static final int ACTION_POINTER_DOWN = 5; //多點觸摸離開動作 public static final int ACTION_POINTER_UP = 6;
以上是比較常用的一些觸摸事件,通常情況下,我們會在OnTouchEvent(MotionEvent event)方法中通過event.getAction()方法來獲取觸摸事件的類型,其代碼模式如下:
@Overridepublic boolean onTouchEvent(MotionEvent event){ //獲取當前輸入點的坐標,(視圖坐標) float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //處理輸入按下事件 break; case MotionEvent.ACTION_MOVE: //處理輸入的移動事件 break; case MotionEvent.ACTION_UP: //處理輸入的離開事件 break; } return true; //注意,這裡必須返回true,否則只能響應按下事件}
以上只是一個空殼的架構,遇到的具體的場景,也有可能會新增多其他事件,或是用不到這麼多事件等等,要根據實際情況來處理。在介紹如何實現滑動之前先來看看android中給我們提供了那些常用的獲取坐標值,相對距離等的方法,主要是有以下兩個類別:
View 提供的獲取坐標方法
getTop(): 獲取到的是View自身的頂邊到其父布局頂邊的距離
getBottom(): 獲取到的是View自身的底邊到其父布局頂邊的距離
getLeft(): 獲取到的是View自身的左邊到其父布局左邊的距離
getRight(): 獲取到的是View自身的右邊到其父布局左邊的距離
MotionEvent提供的方法
getX(): 獲取點擊事件距離控件左邊的距離,即視圖坐標
getY(): 獲取點擊事件距離控件頂邊的距離,即視圖坐標
getRawX(): 獲取點擊事件距離整個屏幕左邊的距離,即絕對坐標
getRawY(): 獲取點擊事件距離整個屏幕頂邊的距離,即絕對坐標
介紹上面一些基本的知識點後,下面我們就來進入正題了,android中實現滑動的其中方法:
其實不管是哪種滑動,他們的基本思路是不變的,都是:當觸摸View時,系統記下當前的觸摸坐標;當手指移動時,系統記下移動後的觸摸點坐標,從而獲得相對前一個點的偏移量,通過偏移量來修改View的坐標,並不斷的更新,重復此動作,即可實現滑動的過程。
首先我們先來定義一個View,並置於LinearLayout中,我們的目的是要實現View隨著我們手指的滑動而滑動,布局代碼如下:
我們知道,在進行View繪制時,會調用layout()方法來設置View的顯示位置,而layout方法是通過left,top,right,bottom這四個參數來確定View的位置的,所以我們可以通過修改這四個參數的值,從而修改View的位置。首先我們在onTouchEvent方法中獲取觸摸點的坐標:
float x = event.getX();float y = event.getY();
接著在ACTION_DOWN的時候記下觸摸點的坐標值:
case MotionEvent.ACTION_DOWN: //記錄按下觸摸點的位置 mLastX = x; mLastY = y; break;
最後在ACTION_MOVE的時候計算出偏移量,且將偏移量作用到layout方法中:
case MotionEvent.ACTION_MOVE: //計算偏移量(此次坐標值-上次觸摸點坐標值) int offSetX = (int) (x - mLastX); int offSetY = (int) (y - mLastY); //在當前left,right,top.bottom的基礎上加上偏移量 layout(getLeft() + offSetX, getTop() + offSetY, getRight() + offSetX, getBottom() + offSetY ); break;
這樣每次在手指移動的時候,都會調用layout方法重新更新布局,從而達到移動的效果,完整代碼如下:
package com.liaojh.scrolldemo;import android.content.Context;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;/** * @author LiaoJH * @DATE 15/11/7 * @VERSION 1.0 * @DESC TODO */public class DragView extends View{ private float mLastX; private float mLastY; public DragView(Context context) { this(context, null); }public DragView(Context context, AttributeSet attrs){ this(context, attrs, 0);}public DragView(Context context, AttributeSet attrs, int defStyleAttr){ super(context, attrs, defStyleAttr);}@Overridepublic boolean onTouchEvent(MotionEvent event){ //獲取當前輸入點的坐標,(視圖坐標) float x = event.getX(); float y = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //記錄按下觸摸點的位置 mLastX = x; mLastY = y; break; case MotionEvent.ACTION_MOVE: //計算偏移量(此次坐標值-上次觸摸點坐標值) int offSetX = (int) (x - mLastX); int offSetY = (int) (y - mLastY); //在當前left,right,top.bottom的基礎上加上偏移量 layout(getLeft() + offSetX, getTop() + offSetY, getRight() + offSetX, getBottom() + offSetY ); break; } return true;}}
當然也可以使用getRawX(),getRawY()來獲取絕對坐標,然後使用絕對坐標來更新View的位置,但要注意,在每次執行完ACTION_MOVE的邏輯之後,一定要重新設置初始坐標,這樣才能准確獲取偏移量,否則每次的偏移量都會加上View的父控件到屏幕頂邊的距離,從而不是真正的偏移量了。
@Overridepublic boolean onTouchEvent(MotionEvent event){ //獲取當前輸入點的坐標,(絕對坐標) float rawX = event.getRawX(); float rawY = event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //記錄按下觸摸點的位置 mLastX = rawX; mLastY = rawY; break; case MotionEvent.ACTION_MOVE: //計算偏移量(此次坐標值-上次觸摸點坐標值) int offSetX = (int) (rawX - mLastX); int offSetY = (int) (rawY - mLastY); //在當前left,right,top.bottom的基礎上加上偏移量 layout(getLeft() + offSetX, getTop() + offSetY, getRight() + offSetX, getBottom() + offSetY ); //重新設置初始位置的值 mLastX = rawX; mLastY = rawY; break; } return true;}
這個方法相當於系統提供了一個對左右,上下移動的API的封裝,在計算出偏移量之後,只需使用如下代碼設置即可:
offsetLeftAndRight(offSetX); offsetTopAndBottom(offSetY);
偏移量的計算與上面一致,只是換了layout方法而已。
LayoutParams保存了一個View的布局參數,因此可以在程序中通過動態的改變布局的位置參數,也可以達到滑動的效果,代碼如下:
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) getLayoutParams(); lp.leftMargin = getLeft() + offSetX; lp.topMargin = getTop() + offSetY; setLayoutParams(lp);
使用此方式時需要特別注意:通過getLayoutParams()獲取LayoutParams時,需要根據View所在的父布局的類型來設置不同的類型,比如這裡,View所在的父布局是LinearLayout,所以可以強轉成LinearLayout.LayoutParams。
在通過改變LayoutParams來改變View的位置時,通常改變的是這個View的Margin屬性,其實除了LayoutParams之外,我們有時候還可以使用ViewGroup.MarginLayoutParams來改變View的位置,代碼如下:
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();lp.leftMargin = getLeft() + offSetX;lp.topMargin = getTop() + offSetY;setLayoutParams(lp);//使用這種方式的好處就是不用考慮父布局類型
在一個View中,系統提供了scrollTo與scrollBy兩種方式來改變一個View的位置,其中scrollTo(x,y)表示移動到一個具體的坐標點(x,y),而scrollBy(x,y)表示移動的增量。與前面幾種計算偏移量相同,使用scrollBy來移動View,代碼如下:
scrollBy(offSetX,offSetY);
然後我們拖動View,發現View並沒有移動,這是為雜呢?其實,方法沒有錯,view也的確移動了,只是他移動的不是我們想要的東西。scrollTo,scrollBy方法移動的是view的content,即讓view的內容移動,如果是在ViewGroup中使用scrollTo,scrollBy方法,那麼移動的將是所有的子View,而如果在View中使用的話,就是view的內容,所以我們需要改一下我們之前的代碼:
((View)getParent()).scrollBy(offSetX, offSetY);
這次是可以滑動了,但是我們發現,滑動的效果跟我們想象的不一樣,完全相反了,這又是為什麼呢?其實這是因為android中對於移動參考系選擇的不同從而實現這樣的效果,而我們想要實現我們滑動的效果,只需將偏移量設置為負值即可,代碼如下:
((View) getParent()).scrollBy(-offSetX, -offSetY);
同樣的在使用絕對坐標時,使用scrollTo也可以達到這樣的效果。
如果讓一個View向右移動200的距離,使用上面的方式,大家應該發現了一個問題,就是移動都是瞬間完成的,沒有那種慢慢平滑的感覺,所以呢,android就給我們提供了一個類,叫scroller類,使用該類就可以實現像動畫一樣平滑的效果。
其實它實現的原理跟前面的scrooTo,scrollBy方法實現view的滑動原理類似,它是將ACTION_MOVE移動的一段位移劃分成N段小的偏移量,然後再每一個偏移量裡面使用scrollBy方法來實現view的瞬間移動,這樣在整體的效果上就實現了平滑的效果,說白了就是利用人眼的視覺暫留特性。
下面我們就來實現這麼一個例子,移動view到某個位置,松開手指,view都吸附到左邊位置,一般來說,使用Scroller實現滑動,需經過以下幾個步驟:
初始化Scroller
//初始化Scroller,使用默認的滑動時長與插值器mScroller = new Scroller(context);
重寫computeScroll()方法
該方法是Scroller類的核心,系統會在繪制View的時候調用draw()方法中調用該方法,這個方法本質上是使用scrollTo方法,通過Scroller類可以獲取到當前的滾動值,這樣我們就可以實現平滑一定的效果了,一般模板代碼如下:
@Overridepublic void computeScroll(){ super.computeScroll(); //判斷Scroller是否執行完成 if (mScroller.computeScrollOffset()) { ((View)getParent()).scrollTo( mScroller.getCurrX(), mScroller.getCurrY() ); //調用invalidate()computeScroll()方法 invalidate(); }}
Scroller類提供中的方法:
computeScrollOffset(): 判斷是否完成了真個滑動getCurrX(): 獲取在x抽方向上當前滑動的距離getCurrY(): 獲取在y抽方向上當前滑動的距離
startScroll開啟滑動
最後在需要使用平滑移動的事件中,使用Scroller類的startScroll()方法來開啟滑動過程,startScroller()方法有兩個重載的方法:
– public void startScroll(int startX, int startY, int dx, int dy)
– public void startScroll(int startX, int startY, int dx, int dy, int duration)
可以看到他們的區別只是多了duration這個參數,而這個是滑動的時長,如果沒有使用默認時長,默認是250毫秒,而其他四個坐標則表示起始坐標與偏移量,可以通過getScrollX(),getScrollY()來獲取父視圖中content所滑動到的點的距離,不過要注意這個值的正負,它與scrollBy,scrollTo中說的是一樣的。經過上面這三步,我們就可以實現Scroller的平滑一定了。
繼續上面的例子,我們可以在onTouchEvent方法中監聽ACTION_UP事件動作,調用startScroll方法,其代碼如下:
case MotionEvent.ACTION_UP: //第三步 //當手指離開時,執行滑動過程 ViewGroup viewGroup = (ViewGroup) getParent(); mScroller.startScroll( viewGroup.getScrollX(), viewGroup.getScrollY(), -viewGroup.getScrollX(), 0, 800 ); //刷新布局,從而調用computeScroll方法 invalidate(); break;
使用屬性動畫同樣可以控制一個View的滑動,下面使用屬相動畫來實現上邊的效果(關於屬相動畫,請關注其他的博文),代碼如下:
case MotionEvent.ACTION_UP: ViewGroup viewGroup = (ViewGroup) getParent(); //屬性動畫執行滑動 ObjectAnimator.ofFloat(this, "translationX", viewGroup.getScrollX()).setDuration(500) .start(); break;
一看這個類的名字,我們就知道他是與拖拽有關的,猜的沒錯,通過這個類我們基本可以實現各種不同的滑動,拖放效果,他是非常強大的一個類,但是它也是最為復雜的,但是不要慌,只要你不斷的練習,就可以數量的掌握它的使用技巧。下面我們使用這個類來時實現類似於QQ滑動側邊欄的效果,相信廣大朋友們多與這個現象是很熟悉的吧。
先來看看使用的步驟是如何的:
初始化ViewDragHelper
ViewDragHelper這個類通常是定義在一個ViewGroup的內部,並通過靜態方法進行初始化,代碼如下:
//初始化ViewDragHelper
viewDragHelper = ViewDragHelper.create(this,callback);
它的第一個參數是要監聽的View,通常是一個ViewGroup,第二個參數是一個Callback回調,它是整個ViewDragHelper的邏輯核心,後面進行具體介紹。
攔截事件
重寫攔截事件onInterceptTouchEvent與onTouchEvent方法,將事件傳遞交給ViewDragHelper進行處理,代碼如下:
@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev){ //2. 將事件交給ViewDragHelper return viewDragHelper.shouldInterceptTouchEvent(ev);}@Overridepublic boolean onTouchEvent(MotionEvent event){ //2. 將觸摸事件傳遞給ViewDragHelper,不可少 viewDragHelper.processTouchEvent(event); return true;}
處理computeScroll()方法
前面我們在使用Scroller類的時候,重寫過該方法,在這裡我們也需要重寫該方法,因為ViewDragHelper內部也是使用Scroller類來實現的,代碼如下:
//3. 重寫computeScroll@Overridepublic void computeScroll(){ //持續平滑動畫 (高頻率調用) if (viewDragHelper.continueSettling(true)) // 如果返回true, 動畫還需要繼續執行 ViewCompat.postInvalidateOnAnimation(this);}
處理回調Callback
通過如下代碼創建一個Callback:
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback(){ @Override //此方法中可以指定在創建ViewDragHelper時,參數ViewParent中的那些子View可以被移動 //根據返回結果決定當前child是否可以拖拽 // child 當前被拖拽的View // pointerId 區分多點觸摸的id public boolean tryCaptureView(View child, int pointerId) { //如果當前觸摸的view是mMainView時開始檢測 return mMainView == child; } @Override //水平方向的滑動 // 根據建議值 修正將要移動到的(橫向)位置 (重要) // 此時沒有發生真正的移動 public int clampViewPositionHorizontal(View child, int left, int dx) { //返回要滑動的距離,默認返回0,既不滑動 //參數參考clampViewPositionVertical f (child == mMainView) { if (left > 300) { left = 300; } if (left < 0) { left = 0; } } return left; } @Override //垂直方向的滑動 // 根據建議值 修正將要移動到的(縱向)位置 (重要) // 此時沒有發生真正的移動 public int clampViewPositionVertical(View child, int top, int dy) { //top : 垂直向上child滑動的距離, //dy: 表示比較前一次的增量,通常只需返回top即可,如果需要精確計算padding等屬性的話,就需要對left進行處理 return super.clampViewPositionVertical(child, top, dy); //0 }};
到這裡就可以拖拽mMainView移動了。
下面我們繼續來優化這個代碼,還記得之前我們使用Scroller時,當手指離開屏幕後,子view會吸附到左邊位置,當時我們監聽ACTION_UP,然後調用startScroll來實現的,這裡我們使用ViewDragHelper來實現。
在ViewDragHelper.Callback中,系統提供了這麼一個方法—onViewReleased(),我們可以通過重寫這個方法,來實現之前的操作,當然這個方法內部也是通過Scroller來實現的,這也是為什麼我們要重寫computeScroll方法的原因,實現代碼如下:
@Override //拖動結束時調用 public void onViewReleased(View releasedChild, float xvel, float yvel) { if (mMainView.getLeft() < 150) { // 觸發一個平滑動畫,關閉菜單,相當於Scroll的startScroll方法 if (viewDragHelper.smoothSlideViewTo(mMainView, 0, 0)) { // 返回true代表還沒有移動到指定位置, 需要刷新界面. // 參數傳this(child所在的ViewGroup) ViewCompat.postInvalidateOnAnimation(DragLayout.this); } } else { //打開菜單 if (viewDragHelper.smoothSlideViewTo(mMainView, 300, 0)) ; { ViewCompat.postInvalidateOnAnimation(DragLayout.this); } } super.onViewReleased(releasedChild, xvel, yvel); }
當滑動的距離小於150時,mMainView回到原來的位置,當大於150時,滑動到300的位置,相當於打開了mMenuView,而且滑動的時候是很平滑的。此外還有一些方法:
@Override public void onViewCaptured(View capturedChild, int activePointerId) { // 當capturedChild被捕獲時,調用. super.onViewCaptured(capturedChild, activePointerId); } @Override public int getViewHorizontalDragRange(View child) { // 返回拖拽的范圍, 不對拖拽進行真正的限制. 僅僅決定了動畫執行速度 return 300; } @Override //當View位置改變的時候, 處理要做的事情 (更新狀態, 伴隨動畫, 重繪界面) // 此時,View已經發生了位置的改變 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { // changedView 改變位置的View // left 新的左邊值 // dx 水平方向變化量 super.onViewPositionChanged(changedView, left, top, dx, dy); }
說明:裡面還有很多關於處理各種事件方法的定義,如:
onViewCaptured():用戶觸摸到view後回調
onViewDragStateChanged(state):這個事件在拖拽狀態改變時回調,比如:idle,dragging等狀態
onViewPositionChanged():這個是在位置改變的時候回調,常用於滑動時伴隨動畫的實現效果等
對於裡面的方法,如果不知道什麼意思,則可以打印log,看看參數的意思。
這裡介紹的就是android實現滑動的七種方法,至於使用哪一種好,就要結合具體的項目需求場景了,畢竟硬生生的實現這個效果,而不管用戶的使用體驗式不切實際的,這裡面個人覺得比較重要的是Scroller類的使用。屬性動畫以及ViewDragHelper類,特別是最後一個,也是最難最復雜的,但也是甩的最多的。
超詳細解析定位坐標—LatLng定位中用得最多的是坐標(也就是經緯度),那麼我們首先搞清楚什麼是坐標:LatLng 類:地理坐標基本數據結構。 描述
(一)LinearLayout常用屬性1. orientation —–布局組件中的排列方式,有水平(horizontal),垂直(vertica
本文主要介紹三級緩存的原理解析與實現方式。以前一直覺得三級緩存圖片加載是一個很難理解的東西,但是自己看了一下午再試著寫了一遍之後感覺還是只要沉下心思考還時很容易熟悉掌握的
今天終於有點時間,來寫了一下: 為RecyclerView實現下拉刷新和上拉加載更多。今天會在前面的兩篇文章的基礎上:RecyclerView系列之(1):為Recycl