在自定義ViewGroup中,使用ViewDragHelper會非常方便.
1.一般在ViewGroup的構造函數中初始化拖拽輔助類
參數
//父類的容器
// sensitivity 敏感度, 越大越敏感.1.0f是默認值 也可以稱之爲敏感閥值
// Callback 事件回調
mhelper = ViewDragHelper.create(forParent, sensitivity, Callback);
2.轉交觸摸事件,由ViewDragHelper來處理我們的touch事件
轉交事件攔截
@Override
public boolean onInterceptTouchEvent(android.view.MotionEvent ev) {
// 由 ViewDragHelper 判斷觸摸事件是否該攔截
return mHelper.shouldInterceptTouchEvent(ev);
}
轉交事件處理
@Override
public boolean onTouchEvent(MotionEvent event) {
// 由 ViewDragHelper 處理事件
try {
mHelper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
3.接收事件的回調 ViewDragHelper.Callback
1.public boolean tryCaptureView(View child, int pointerId) {} -->確定當前手指觸摸的view是否可以被拖動
參數:
// child 被用戶拖拽的子View
// pointerId 多點觸摸的手指id
返回值:
確定當前點擊的view是否可以拖動,如果返回false,則接下來的方法都不會調用,拿不到增量
如果我們不想某個view被拖動,那麼我們可以在這裏判斷,如果參數裏面的view等於不想拖動的view返回false就ok
2.public int getViewHorizontalDragRange(View child) {} -->確定拖拽的範圍.只需要返回一個>0的值,決定了動畫的執行時長,水平方向是否可以被滑開,用於底層的計算,並不能真正的限定水平拖拽的範圍
參數:
//child 被用戶拖拽的子view
返回值:
返回拖拽的範圍 在這裏,一般設置爲滑動的邊界值,這個值可以隨便設置
3.public int clampViewPositionHorizontal(View child, int left, int dx) {} -->修正子View水平方向的位置. 此時還沒有發生真正的移動.
參數:
//child 被用戶拖拽的子view
//left 建議移動到的位置
//dx 與舊位置的差值
返回值:
返回需要移動的位置值,在這裏我們需要做邊界的處理
4.public void onViewPositionChanged(View changedView, int left, int top,int dx, int dy) {} -->當控件位置變化時 調用, 可以做 : 伴隨動畫, 狀態的更新, 事件的回調.
參數:
//changedView 位置發生變化的子孩子
//left 最新的水平方向的位置
//top 最新的垂直方向的位置
//dx 剛剛發生的水平偏移量
//dy 剛剛發生的垂直偏移量
在這裏,我們一般是根據需求去傳遞位置的變化量,以及伴隨動畫, 狀態的更新, 事件的回調
爲了兼容低版本,在此方法裏需要調用重繪的方法 invalidate();
5.public void onViewReleased(View releasedChild, float xvel, float yvel) {} -->決定了鬆手之後要做的事情, 結束的動畫
參數:
//releasedChild 手指鬆開後,被釋放的子view
//xvel 水平方向的速度 正數代表向右滑動,負值代表向左滑動
//yvel 垂直方向的速度 正數代表向下滑動,負值代表向上滑動
在這裏我們一般是處理手指鬆開後的平滑動畫,底部封裝了Scroller(實現步驟和使用Scroller類似)
(1).觸發平滑動畫
mHelper.smoothSlideViewTo(View child, int finalLeft, int finalTop)
參數:
//child 哪一個子view觸發
//水平方向要滑動到的位置
//垂直方向要滑動到的位置
返回值:
boolean類型值,如果當前位置不是指定的最終位置,返回true
(2).開始重繪界面
ViewCompat.postInvalidateOnAnimation(View view); 重繪界面,爲了使滑動流暢不掉幀,
我們利用的v4包裏的ViewCompat來實現
參數:
//view 是執行動畫的父佈局
(3).維持動畫的繼續, 高頻率調用
public void computeScroll() {
if(mHelper.continueSettling(true)){
// 如果當前位置還沒有移動到最終位置. 返回true.需要繼續重繪界面
ViewCompat.postInvalidateOnAnimation(this);
}
4: invalidate&postInvalidate&postInvalidateOnAnimation區別
invalidate:主線程重繪
postInvalidate:可在子線程重繪
postInvalidateOnAnimation
在16之前,跟調用invalidate沒有區別,api16之後,會調用View. postInvalidateOnAnimation新增方法。
所以新增了postInvalidateOnAnimation方法,這個會對invalidate的頻率做調整,減少阻塞message的機率。
5:在自定義view或者ViewGroup中,以下方法調用順序
onFinishInflate()–>onSizeChanged()–>onLayout()
如何知道view已經繪製完成,並且顯示到了界面
protected void onFinishInflate() {}
當view繪製完成,那麼就可以在這裏拿到子view的引用,可以加以邏輯判斷,使代碼的邏輯性更健壯
當view的尺寸發生變化時調用,一般在這裏我們可以去獲取定義的view的寬高
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mWidth = w;
mHeight = h;
}
6:在做屬性動畫的時候,目的就是找到一個百分比(percent),其他就好辦了,可以利用percent和startValue,endValue做動畫,特別是利用系統提供的估值器
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
public Object evaluateColor(float fraction, Object startValue, Object endValue) {
int startInt = (Integer) startValue;
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endInt = (Integer) endValue;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
(int)((startR + (int)(fraction * (endR - startR))) << 16) |
(int)((startG + (int)(fraction * (endG - startG))) << 8) |
(int)((startB + (int)(fraction * (endB - startB))));
}
兩個例子,做個筆記.
/**
* 側滑面板
* @author poplar
*
*/
public class DragLayout extends FrameLayout {
private ViewDragHelper mHelper;
public static enum Status {
Close, Open, Draging
}
private Status status = Status.Close;
public interface OnDragChangeListener {
void onClose();
void onOpen();
void onDraging(float percent);
}
private OnDragChangeListener onDragChangeListener;
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public OnDragChangeListener getOnDragChangeListener() {
return onDragChangeListener;
}
public void setOnDragChangeListener(OnDragChangeListener onDragChangeListener) {
this.onDragChangeListener = onDragChangeListener;
}
public DragLayout(Context context) {
this(context, null);
}
public DragLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// forParent 父類的容器
// sensitivity 敏感度, 越大越敏感.1.0f是默認值
// Callback 事件回調
// 1. 創建 ViewDragHelper 輔助類
mHelper = ViewDragHelper.create(this, 1.0f, callback);
}
// 3. 接受處理的結果.
ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
// 1. 返回值, 決定了child是否可以被拖拽
@Override
public boolean tryCaptureView(View child, int pointerId) {
// child 被用戶拖拽的子View
// pointerId 多點觸摸的手指id
System.out.println("tryCaptureView: ");
return true;
}
// 2. 返回拖拽的範圍. 返回一個 >0 的值, 決定了動畫的執行時長, 水平方向是否可以被滑開
@Override
public int getViewHorizontalDragRange(View child) {
return mRange;
};
// 3. 修正子View水平方向的位置. 此時還沒有發生真正的移動.
// 返回值決定了View將會移動到的位置
public int clampViewPositionHorizontal(View child, int left, int dx) {
// child 被拖拽的子View
// left 建議移動到的位置
// dx 跟舊的位置的差值
// int oldLeft = mMainContent.getLeft();
// System.out.println("clamp: " + " left: " + left + " dx: " + dx + " oldLeft: " + oldLeft);
if(child == mMainContent){
left = fixLeft(left);
}
return left;
}
// 4. 當控件位置變化時 調用, 可以做 : 伴隨動畫, 狀態的更新, 事件的回調.
@Override
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
// left最新的水平方向的位置
// dx 剛剛發生的水平變化量
// System.out.println("onViewPositionChanged: " + " left:" + left + " dx: " + dx);
if(changedView == mLeftContent){
// 如果移動的是左面板
// 1. 放回原來的位置
mLeftContent.layout(0, 0, 0 + mWidth, 0 + mHeight);
// 2. 把左面板發生的變化量dx轉遞給主面板
int newLeft = mMainContent.getLeft() + dx;
// 修正左邊值.
newLeft = fixLeft(newLeft);
mMainContent.layout(newLeft, 0, newLeft + mWidth, 0 + mHeight);
}
dispatchDragEvent();
// 爲了兼容低版本, 手動重繪界面所有內容.
invalidate();
}
//5. 決定了鬆手之後要做的事情, 結束的動畫
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
System.out.println("onViewReleased: xvel: " + xvel);
// releasedChild 被釋放的孩子
// xvel 水平方向的速度 向右爲+, 向左爲-
if(xvel == 0 && mMainContent.getLeft() > mRange * 0.5f){
open();
} else if (xvel > 0) {
open();
} else {
close();
}
}
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
};
/**
* 分發拖拽事件, 伴隨動畫,更新狀態.
*/
protected void dispatchDragEvent() {
// 0.0 -> 1.0
float percent = mMainContent.getLeft() * 1.0f / mRange;
System.out.println("percent: " + percent);
if(onDragChangeListener != null){
onDragChangeListener.onDraging(percent);
}
// 更新狀態
Status lastStatus = status;
status = updateStatus(percent);
if(lastStatus != status && onDragChangeListener != null){
if(status == Status.Close){
onDragChangeListener.onClose();
}else if (status == Status.Open) {
onDragChangeListener.onOpen();
}
}
// 執行動畫
animViews(percent);
}
/**
* 更新狀態
* @param percent 當前動畫執行的百分比
* @return
*/
private Status updateStatus(float percent) {
if(percent == 0){
return Status.Close;
}else if (percent == 1) {
return Status.Open;
}
return Status.Draging;
}
private void animViews(float percent) {
// - 左面板: 縮放動畫, 平移動畫, 透明度動畫
// 縮放動畫 0.0 -> 1.0 >>> 0.0 -> 0.5 >>>0.5 -> 1.0
// percent * 0.5 + 0.5
// mLeftContent.setScaleX(percent * 0.5f + 0.5f);
// mLeftContent.setScaleY(percent * 0.5f + 0.5f);
ViewHelper.setScaleX(mLeftContent, evaluate(percent, 0.5f, 1.0f));
ViewHelper.setScaleY(mLeftContent, evaluate(percent, 0.5f, 1.0f));
// 平移動畫 -mWidth / 2.0f -> 0
ViewHelper.setTranslationX(mLeftContent, evaluate(percent, -mWidth / 2.0f, 0));
// 透明度動畫 0.2f -> 1.0
ViewHelper.setAlpha(mLeftContent, evaluate(percent, 0.2f, 1.0f));
// - 主面板: 縮放動畫 1.0 -> 0.8
ViewHelper.setScaleX(mMainContent, evaluate(percent, 1.0f, 0.8f));
ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f));
// - 背 景: 亮度變化
getBackground().setColorFilter((Integer)evaluateColor(percent, Color.BLACK, Color.TRANSPARENT), PorterDuff.Mode.SRC_OVER);
}
public Object evaluateColor(float fraction, Object startValue, Object endValue) {
int startInt = (Integer) startValue;
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endInt = (Integer) endValue;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
(int)((startR + (int)(fraction * (endR - startR))) << 16) |
(int)((startG + (int)(fraction * (endG - startG))) << 8) |
(int)((startB + (int)(fraction * (endB - startB))));
}
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
/**
* 修正位置
* @param left
* @return
*/
private int fixLeft(int left) {
if(left < 0){
return 0;
}else if (left > mRange) {
return mRange;
}
return left;
}
/**
* 關閉面板
*/
public void close() {
close(true);
}
public void close(boolean isSmooth){
int finalLeft = 0;
if(isSmooth){
// 走平滑動畫
// 1. 觸發一個平滑動畫.
if(mHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){
// 如果當前位置不是指定的最終位置. 返回true
// 需要重繪界面, 一定要傳 子View 所在的容器
ViewCompat.postInvalidateOnAnimation(this);
}
}else {
System.out.println("open");
mMainContent.layout(finalLeft, 0, finalLeft + mWidth, 0 + mHeight);
}
}
/**
* 打開面板
*/
public void open() {
open(true);
}
public void open(boolean isSmooth){
int finalLeft = mRange;
if(isSmooth){
// 走平滑動畫
// 1. 觸發一個平滑動畫.
if(mHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){
// 如果當前位置不是指定的最終位置. 返回true
// 需要重繪界面, 一定要傳 子View 所在的容器
ViewCompat.postInvalidateOnAnimation(this);
}
}else {
System.out.println("open");
mMainContent.layout(finalLeft, 0, finalLeft + mWidth, 0 + mHeight);
}
}
//2. 維持動畫的繼續, 高頻率調用.
@Override
public void computeScroll() {
super.computeScroll();
if(mHelper.continueSettling(true)){
// 如果當前位置還沒有移動到最終位置. 返回true.需要繼續重繪界面
ViewCompat.postInvalidateOnAnimation(this);
}
}
private ViewGroup mLeftContent;
private ViewGroup mMainContent;
private int mHeight;
private int mWidth;
private int mRange;
// 2. 轉交觸摸事件
public boolean onInterceptTouchEvent(android.view.MotionEvent ev) {
// 由 ViewDragHelper 判斷觸摸事件是否該攔截
return mHelper.shouldInterceptTouchEvent(ev);
};
@Override
public boolean onTouchEvent(MotionEvent event) {
// 由 ViewDragHelper 處理事件
try {
mHelper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 當控件尺寸變化的時候調用
mHeight = getMeasuredHeight();
mWidth = getMeasuredWidth();
// 計算拖拽的範圍
mRange = (int) (mWidth * 0.6f);
System.out.println("mWidth: " + mWidth + " mHeight: " + mHeight + " mRange: " + mRange);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// 代碼的健壯性.
// 孩子至少倆
if(getChildCount() < 2){
throw new IllegalStateException("Your viewgroup must have 2 children. 子View至少有兩個!");
}
// 孩子必須是ViewGroup的子類
if(!(getChildAt(0) instanceof ViewGroup) || !(getChildAt(1) instanceof ViewGroup)){
throw new IllegalArgumentException("Child must be an instance of ViewGroup . 孩子必須是ViewGroup的子類");
}
// Github
mLeftContent = (ViewGroup) getChildAt(0);
mMainContent = (ViewGroup) getChildAt(1);
}
}
側拉刪除
/**
* 側拉刪除
* @author poplar
*
*/
public class SwipeLayout extends FrameLayout {
public static enum Status {
Close, Open, Swiping
}
public interface OnSwipeListener{
void onClose(SwipeLayout layout);
void onOpen(SwipeLayout layout);
void onStartOpen(SwipeLayout layout);
void onStartClose(SwipeLayout layout);
}
private Status status = Status.Close;
private OnSwipeListener onSwipeListener;
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
public OnSwipeListener getOnSwipeListener() {
return onSwipeListener;
}
public void setOnSwipeListener(OnSwipeListener onSwipeListener) {
this.onSwipeListener = onSwipeListener;
}
private ViewDragHelper mHelper;
public SwipeLayout(Context context) {
this(context, null);
}
public SwipeLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SwipeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// 1. 創建ViewDragHelper
mHelper = ViewDragHelper.create(this, callback);
}
// 3. 重寫回調的方法
ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return true;
}
public int getViewHorizontalDragRange(View child) {
return mRange;
};
public int clampViewPositionHorizontal(View child, int left, int dx) {
// 返回的值決定了將要移動到的位置.
if(child == mFrontView){
if(left < - mRange){
// 限定左範圍
return - mRange;
}else if (left > 0) {
// 限定右範圍
return 0;
}
}else if (child == mBackView) {
if(left < mWidth - mRange){
// 限定左範圍
return mWidth - mRange;
}else if (left > mWidth) {
// 限定右範圍
return mWidth;
}
}
return left;
};
// 位置發生改變的時候, 把水平方向的偏移量傳遞給另一個佈局
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
if(changedView == mFrontView){
// 拖拽的是前佈局, 把剛剛發生的 偏移量dx 傳遞給 後佈局
mBackView.offsetLeftAndRight(dx);
} else if (changedView == mBackView) {
// 拖拽的是後佈局, 把剛剛發生的 偏移量dx 傳遞給 前佈局
mFrontView.offsetLeftAndRight(dx);
}
dispatchDragEvent();
invalidate();
};
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// 鬆手時候會被調用
// xvel 向右+, 向左-
if(xvel == 0 && mFrontView.getLeft() < - mRange * 0.5f){
open();
}else if(xvel < 0){
open();
}else {
close();
}
};
};
private View mBackView;
private View mFrontView;
private int mRange;
private int mWidth;
private int mHeight;
// 2. 轉交觸摸事件攔截判斷, 處理觸摸事件
public boolean onInterceptTouchEvent(android.view.MotionEvent ev) {
return mHelper.shouldInterceptTouchEvent(ev);
};
/**
* 更新當前的狀態
*/
protected void dispatchDragEvent() {
Status lastStatus = status;
// 獲取最新的狀態
status = updateStatus();
// 狀態改變的時候, 調用監聽裏的方法
if(lastStatus != status && onSwipeListener != null){
if(status == Status.Open){
onSwipeListener.onOpen(this);
}else if (status == Status.Close) {
onSwipeListener.onClose(this);
}else if (status == Status.Swiping) {
if(lastStatus == Status.Close){
onSwipeListener.onStartOpen(this);
}else if (lastStatus == Status.Open) {
onSwipeListener.onStartClose(this);
}
}
}
}
private Status updateStatus() {
int left = mFrontView.getLeft();
if(left == 0){
return Status.Close;
}else if (left == -mRange) {
return Status.Open;
}
return Status.Swiping;
}
@Override
public void computeScroll() {
super.computeScroll();
// 維持平滑動畫繼續
if(mHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(this);
}
}
/**
* 關閉
*/
protected void close() {
close(true);
}
public void close(boolean isSmooth){
int finalLeft = 0;
if(isSmooth){
// 觸發平滑動畫
if(mHelper.smoothSlideViewTo(mFrontView, finalLeft, 0)){
ViewCompat.postInvalidateOnAnimation(this);
}
}else {
layoutContent(false);
}
}
/**
* 打開
*/
protected void open() {
open(true);
}
public void open(boolean isSmooth){
int finalLeft = -mRange;
if(isSmooth){
// 觸發平滑動畫
if(mHelper.smoothSlideViewTo(mFrontView, finalLeft, 0)){
ViewCompat.postInvalidateOnAnimation(this);
}
}else {
layoutContent(false);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
mHelper.processTouchEvent(event);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
super.onLayout(changed, left, top, right, bottom);
// 默認是關閉狀態
layoutContent(false);
}
/**
* 根據當前的開啓狀態擺放內容
* @param isOpen
*/
private void layoutContent(boolean isOpen) {
// 設置前佈局位置
Rect rect = computeFrontRect(isOpen);
mFrontView.layout(rect.left, rect.top, rect.right, rect.bottom);
// 根據前佈局位置設置後佈局位置
Rect backRect = computeBackRectViaFront(rect);
mBackView.layout(backRect.left, backRect.top, backRect.right, backRect.bottom);
// 把任意佈局順序調整到最上
bringChildToFront(mFrontView);
}
/**
* 計算後佈局的矩形區域
* @param rect
* @return
*/
private Rect computeBackRectViaFront(Rect rect) {
int left = rect.right;
return new Rect(left, 0, left + mRange , 0 + mHeight);
}
/**
* 計算前佈局的矩形區域
* @param isOpen
* @return
*/
private Rect computeFrontRect(boolean isOpen) {
int left = 0;
if(isOpen){
left = -mRange;
}
return new Rect(left, 0, left + mWidth, 0 + mHeight);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRange = mBackView.getMeasuredWidth();
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mBackView = getChildAt(0);
mFrontView = getChildAt(1);
}
}