編輯:關於Android編程
如上圖簡單呈現出兩個方塊後,提出一個需求:
1.拖動方塊時,方塊(即子View)可以跟隨手指移動。
2.一個方塊移動時,另一個方塊可以跟隨移動。
3.將方塊移動到左邊區域(右邊區域)後放開(即手指離開屏幕),它會自動移動到左邊界(右邊界)。
4.移動的時候給方塊加點動畫(duang~duang~duang~) 。
在自定義控件中,View繪制的一個重寫方法layout(),用來設置顯示的位置。所以,可以通過修改View的坐標值來改變view在父View的位置,以此可以達到移動的效果!但是缺點是只能移動指定的View:
//通過layout方法來改變位置 view.layout(l,t,r,b);
非常方便的封裝方法,只需提供水平、垂直方向上的偏移量,展示效果與layout()方法相同。
view.offsetLeftAndRight(offset);//同時改變left和right view.offsetTopAndBottom(offset);//同時改變top和bottom
此類保存了一個View的布局參數,可通過LayoutParams動態改變一個布局的位置參數,以此動態地修改布局,達到View位置移動的效果!但是在獲取getLayoutParams()時,要根據該子View對應的父View布局來決定自身的LayoutParams 。所以一切的前提是:必須要有一個父View,否則無法獲取LayoutParams !
//必須獲取父View的LayoutParams LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)getLayoutParams(); layoutParams.leftMargin = getLeft() + dx; layoutParams.topMargin = getTop() + dy; setLayoutParams(layoutParams);
通過改變scrollX和scrollY來移動,但是可以移動所有的子View。scrollTo(x,y)表示移動到一個具體的坐標點(x,y),而scrollBy(x,y)表示移動的增量為dx,dy。
scrollTo(x,y); scrollBy(xOffset,yOffset);
注意:這裡使用scrollBy(xOffset,yOffset);,你會發現並沒有效果,因為以上兩個方法移動的是View的content。若在ViewGroup中使用,移動的是所有子View;若在View中使用,移動的是View的內容(比如TextView)。
所以,不可在view中使用以上方法!應該在View所在的ViewGroup中使用:
((View)getParent()).scrollBy(offsetX, offsetY);
【視圖坐標系】:
可是即使這樣,你會發現view移動的效果與設想方向相反!這是Android試圖移動原因,若參數為正值,content將向坐標軸負方向移動;參數為負值,content將向坐標軸正方向移動。所以要實現隨手指移動而滑動的效果,應將偏移量設置為負值即可:
((View)getParent()).scrollBy(-offsetX, -offsetY);
5. canvas
通過改變Canvas繪制的位置來移動View的內容,用的少:
canvas.drawBitmap(bitmap, left, top, paint)
總結
但是要完成最開始的提的需求,不管使用哪一種方法,都需要通過onTouchEvent方法來捕捉手勢,自己手動計算移動距離,再改變子View的布局,不免有些麻煩,所以在這裡引出正文,介紹一個強大的類來處理移動:ViewDragHelper
ViewDragHelper介紹:
1. 產生: ViewDragHelper在高版本的v4包(android4.4以上的v4)中,於Google在2013年開發者大會提出的
2. 作用:它主要用於處理ViewGroup中對子View的拖拽處理。
3. 使用:它主要封裝了對View的觸摸位置,觸摸速度,移動距離等的檢測和Scroller,通過接口回調的方式通知我們。所以我們需要做的只是用接收來的數據指定這些子View是否需要移動,移動多少等。
4. 本質:是一個對觸摸事件的解析類。
ViewDragHelper實現
1. ViewDragHelper實例創建
/**
* Factory method to create a new ViewDragHelper.
*
* @param forParent Parent view to monitor
* @param sensitivity Multiplier for how sensitive the helper should be about detecting
*the start of a drag. Larger values are more sensitive. 1.0f is normal.
* @param cb Callback to provide information and receive events
* @return a new ViewDragHelper instance
*/
viewDragHelper = ViewDragHelper.create(forParent, sensitivity, cb);
create()就是創建ViewDragHelper實例的方法,代碼中的注釋是create()中參數的解釋,來查看:
(1)forParent :“用來監視的父View”。傳入參數父View,即可監視該父View中的所有子View。
(2)sensitivity:“檢測時的敏感度;值越大越敏感,1是正常范圍”。比如說手指在滑動屏幕時速度特別快,敏感度越大時,此時速度快也可以檢測到,反之亦然。
(3)Callback :“提供信息和接受的事件”。最重要的參數!可以從這個回調提供的信息獲取到View滑動的距離、速度等。
2. 自定義View繼承FrameLayout
這裡來個小提示:之前實現的布局中自定義DragLayout是繼承於ViewGroup,並且實現重寫了onMeasure()方法,如下:
DragLayout.java
public class DragLayout extends ViewGroup{
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//方法一:對子View的測量需求
/*獲取子View的寬度100dp 的兩種方法:
int size = (int) getResources().getDimension(R.dimen.width);
int size = readView.getLayoutParams().width;*/
int measureSpec = MeasureSpec.makeMeasureSpec(redView.getLayoutParams().width, MeasureSpec.EXACTLY);//具體指定寬高,為精確模式
redView.measure(measureSpec,measureSpec);//當父控件測量完子控件,才可以填(0,0)
blueView.measure(measureSpec,measureSpec);
/* //方法二:如果說沒有特殊的對子View的測量需求,可用如下方法
measureChild(redView,widthMeasureSpec,heightMeasureSpec);
measureChild(blueView,widthMeasureSpec,heightMeasureSpec);*/
}
}
但是現在使DragLayout 類繼承於FrameLayout即可!
public class DragLayout extends FrameLayout{
...
}
因為在自定義ViewGroup的時候,如果對子View的測量沒有特殊的需求,那麼可以繼承系統已有的布局(比如FrameLayout、RelativeLayout),目的是為了讓已有的布局幫我們實現onMeasure()。
所以在繼承之後,我們無需實現onMeasure()方法,以上代碼全部不需要(這裡選擇繼承FrameLayout幀布局,原因是在Android源碼中其實現最簡單),所以重寫繼承的onLayout()方法其實是重寫幀布局中的onLayout(),如果也注釋的話,你會發現藍色小方塊覆蓋紅色,一起擺放在左上角(其實就是幀布局的擺放規則)
3. callback回調創建
private ViewDragHelper.Callback callback = new Callback() {
//必須要實現的方法
@Override
public boolean tryCaptureView(View child, int pointerId) {
return false;
}
};
4. 觸摸、攔截事件
以上部分ViewDragHelper的創建部分已完成,可是還沒結束。比如大家熟悉的一個類:GestureDetector手勢識別器,想要它生效,必須傳一個觸摸事件,這樣GestureDetector類才可以解析當前手勢。道理相同,之前在介紹ViewDragHelper已提到,它只是一個對觸摸事件的解析類,需要傳一個觸摸事件,才會生效。
//處理是否攔截
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//由viewDragHelper 來判斷是否應該攔截此事件
boolean result = viewDragHelper.shouldInterceptTouchEvent(ev);
return result;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//將觸摸事件傳給viewDragHelper來解析處理
viewDragHelper.processTouchEvent(event);
//消費掉此事件,自己來處理
return true;
}
以上則viewDragHelper可以監視並解析我們的手勢了,而且會把信息通過回調傳遞給callback。
5. 處理computeScroll()
該方法是Scroller類的核心,系統在繪制View的時候在draw()中調用此方法,實際與scrollTo()相同。
@Override
public void computeScroll() {
super.computeScroll();
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
invalidate();
}
如上,Scroller類提供computeScrollOffset()方法來判斷是否完成了整個滑動,同時getCurrX()和getCurrY()來獲得當前滑動坐標。
重點是invalidate()方法,因為只能在computeScroll()方法中獲取模擬過程中的scrollX 和 scrollY,但computeScroll()方法是不會自動調用的,只能通過invalidate() —> draw() —>computeScroll()來間接調用computeScroll()方法!模擬過程結束,if判斷中computeScrollOffset()方法返回false,中斷循環,完成整個平滑移動過程!
但是!!!我們並不采取以上方法,之前介紹過ViewDragHelper已經封裝好了Scroller,用另外一種:
@Override
public void computeScroll() {
super.computeScroll();
if(viewDragHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(DragLayout.this);
}
}
}
continueSettling()方法判斷是否結束,同Scroller的方法相似,主要是postInvalidateOnAnimation(),此方法不像Scroller的scrollTo,還需要傳值,其實此方法體內已經封裝好移動的方法,它會自動去測量當前位置進行移動,所以我們只需調用即可!(在手指抬起時回調的方法中也會用到它,後面介紹)
6. 實現callback回調中的方法
之前在創建callback時,默認只實現了tryCaptureView()方法 ,完成需求僅僅不夠,還需要其它方法,依次介紹:
(1) tryCaptureView()
此方法用於判斷是否捕獲當前child的觸摸事件,可以指定ViewDragHelper移動哪一個子View。此例中,需要移動兩個方塊,則判斷當前View是否是自己想移動的,返回boolean值。
/**用於判斷是否捕獲當前child的觸摸事件
* @param child 當前觸摸的子View
* @return true:捕獲並解析 false:不處理
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == blueView || child == redView;
}
(2) onViewCaptured()
此方法在View被開始捕獲和解析時回調,即當tryCaptureView()中的返回值為true的時候,此方法才會被調用。
例如tryCaptureView()方法中只捕獲紅色方塊,當移動紅方快時,該方法會回調,移動藍色方塊時則不會!
/** 當View被開始捕獲和解析的回調(用處不大)
* @param capturedChild 當前被捕獲的子View
*/
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
Log.e("tag","onViewCaptures");
}
(3) clampViewPositionHorizontal() 和 clampViewPositionVertical()
這兩個為具體滑動方法,分別對應水平和垂直方向上的移動。要想子View移動,此方法必須重寫實現!
而方法的返回值則是指定View在水平(left)或垂直(top)方向上變成的值,參數中的dx、dy則是代表相較於上一次位置的增量。
/** 控制child在水平方向的移動
* @param child
* @param left ViewDragHelper會將當前child的left值改變成返回的值
* @param dx 相較於上一次child在水平方向上移動的
* @return
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
/**控制child在垂直方向的移動
* @param child
* @param top ViewDragHelper會將當前child的top值改變成返回的值
* @param dy 相較於上一次child在水平方向上移動的
* @return
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
};
顯示效果:
通過以上GIF動圖和日志打印可以看出,僅將返回值設置成方法中的參數,方塊就可以任意移動了。也證實了方法中提供的參數而dx或dy是每一次移動的距離,left或top 是指定View移動到的位置,這是計算好了的,相當於
left = child.getLeft() + dx。
若想要它不移動,則:
return left - dx;
將它計算好後的距離減去相較於上次移動的距離即可,此時的View就不會移動。所以根據你的需求,可以任意改變此方法的返回值來移動View。
(4) getViewHorizontalDragRange() 和 getViewVerticalDragRange()
看到以上GIF動圖,你會發現我在移動方塊時,它可以超過邊界,沒有任何限制!有些不合理,想限制它的移動范圍,這兩個方法就可以獲取View的拖拽范圍,將它的返回值設為:父控件的寬/高 - 子控件的寬/高,即控件可以移動的范圍。
//獲取View水平方向的拖拽范圍
@Override
public int getViewHorizontalDragRange(View child) {
return getMeasuredWidth() - child.getMeasuredWidth();
}
// 獲取View垂直方向的拖拽范圍
@Override
public int getViewVerticalDragRange(View child) {
return getMeasuredHeight() - child.getMeasuredHeight();
}
可是以上實現後,你會發現拖拽方塊還是可以超出邊界,此方法並沒有起作用!是否代表此方法完全無用?這返回的值有何用?
不是,它目前確實並不可以限制邊界,但此方法返回的值會用在:比如說手指抬起時,View緩慢移動的動畫時間的計算會用到此值,最好不要返回0(返回也不會錯)!
但是我們還想要達到限制View拖拽邊界的效果,這時在第三點介紹的clampViewPositionHorizontal() 和 clampViewPositionVertical()發揮效果了,該方法是通過返回值來改變View移動的位置,這時可以在方法中加判斷是否有越界:
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
if(left <0){
//限制左邊界
left = 0;
}else if (left > (getMeasuredWidth() - child.getMeasuredWidth())){
//限制右邊界
left = getMeasuredWidth() - child.getMeasuredWidth();
}
return left;
}
顯示效果:
(5)onViewPositionChanged()
目前為止,需求已經完成可以任意拖拽View了,接下來完成拖拽View時,另一塊跟隨移動。這時介紹一個新的方法:onViewPositionChanged(),該方法在child(需要捕捉的View)位置改變時執行,參數left(top)跟之前介紹方法中含義相同,為child最新的left(top)位置,而dx(dy)是child相較於上一次移動時水平(垂直)方向上改變的距離。
了解之後,就知道這個方法很強大了,在方法體中判斷具體View,再根據方法提供的參數設置另一View的位置,如下:
/**當child位置改變時執行
* @param changedView 位置改變的子View
* @param left child最新的left位置
* @param top child最新的top位置
* @param dx 相較於上一次水平移動的距離
* @param dy 相較於上一次垂直移動的距離
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
if(changedView == blueView){
//拖動藍色方塊時,紅色也跟隨移動
redView.layout(redView.getLeft()+dx , redView.getTop()+dy ,
redView.getRight()+dx , redView.getBottom()+dy);
}else if(changedView == redView){
//拖動紅色方塊時,藍色也跟隨移動
blueView.layout(blueView.getLeft()+dx , blueView.getTop()+dy ,
blueView.getRight()+dx , blueView.getBottom()+dy);
}
}
顯示效果:
(6)onViewReleased()
完成目前需求第三個:手指在左邊(右邊)區域離開屏幕後,方塊自動移動到左邊界(右邊界)。接下來介紹最後一個方法onViewReleased(),手指抬起的時候執行該方法。
這裡有兩個新參數:
xvel: x方向移動的速度,若是正值,則代表向右移動,若是負值則向左移動;
yvel: y方向移動的速度,若是正值則向下移動,若是負值則向上移動。
/**手指抬起的時候執行該方法
* @param releasedChild 當前抬起的View
* @param xvel x方向移動的速度:正值:向右移動 負值:向左移動
* @param yvel y方向移動的速度:正值:向下移動 負值:向上移動
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
int centerLeft = getMeasuredWidth()/2 - releasedChild.getMeasuredWidth()/2;
if(releasedChild.getLeft() < centerLeft){
//在左半邊,應該向左緩慢移動,不用scroller,ViewDragHelper已封裝好
viewDragHelper.smoothSlideViewTo(releasedChild,0,releasedChild.getTop());
//仍需要刷新!
ViewCompat.postInvalidateOnAnimation(DragLayout.this);
// scroller.startScroll();
// invalidate();
}else {
//在右半邊,向右緩慢移動
viewDragHelper.smoothSlideViewTo(releasedChild,getMeasuredWidth() - releasedChild.getMeasuredWidth(),releasedChild.getTop());
ViewCompat.postInvalidateOnAnimation(DragLayout.this);
}
}
};
顯示效果:
7. 執行伴隨動畫
還剩下最後一個需求,在方塊移動時加些動畫,說到動畫引入一個概念:百分比(即子View左側占子View可移動寬度的比例)。在移動子View的時候,比如從左到右,那麼百分比則是0~1。做個實驗,在回調onViewPositionChanged()加入兩行:
//1.計算view移動的百分比
float fraction = changedView.getLeft() * 1f / (getMeasuredWidth() - changedView.getMeasuredWidth());
Log.e("tag","fraction:"+fraction);
//2.執行一系列的伴隨動畫
executeAnim(fraction);
結果:
證實了我們以上的推論,所以現在可以通過傳參數百分比來完成我們想要的動畫效果:
/**
* 執行伴隨動畫
* @param fraction
*/
private void executeAnim(float fraction){
//fraction: 0 - 1
//縮放
// ViewHelper.setScaleX(redView, 1+0.5f*fraction);
// ViewHelper.setScaleY(redView, 1+0.5f*fraction);
//旋轉
// ViewHelper.setRotation(redView,360*fraction);//圍繞z軸轉
ViewHelper.setRotationX(redView,360*fraction);//圍繞x軸轉
// ViewHelper.setRotationY(redView,360*fraction);//圍繞y軸轉
ViewHelper.setRotationX(blueView,360*fraction);//圍繞z軸轉
//平移
// ViewHelper.setTranslationX(redView,80*fraction);
//透明
// ViewHelper.setAlpha(redView, 1-fraction);
}
最終成品:
以上了解後,ViewDragHelper的學習到此為止,接下來利用它做一個側滑什麼的更是不在話下,包括現在網上的彷QQ側滑面板都是利用ViewDragHelper完成的,所以工欲善其事,必先利其器呀~
關於View移動總結的,參照了徐宜生老師的《Android群英傳》,講解了許多View相關知識,重新加深理解了,還是很有幫助的。
之前的項目裡要做一個異步更新UI的功能,但是結果出現了ANR,所以想寫個demo來測試到底是哪個地方出現了問題,結果發現原來的思路是沒有問題,郁悶~~ 現在這個demo
自定義開關控件 Android自定義控件一般有三種方式 1、繼承Android固有的控件,在Android原生控件的基礎上,進行添加功能和邏輯。 2、繼承Vie
public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter
SQLite數據庫框架ORMLite與GreenDao的簡單比較 而且鴻洋老師也寫了兩篇關於ORMLite的文章Android ORMLite 框架的入門用法