Android 座標系
將屏幕的左上角的頂點作爲Android座標系的原點,從這個點向右是 x 軸正方向,向下是 y 軸正方向。
getRawX()、getRawY()獲得的座標是Android座標系上的座標。
視圖座標系
描述子視圖在父視圖的位置關係,視圖座標系同樣是從原點向右是 x 軸正方向,向下是 y 軸正方向。,原點不再是屏幕的左上角,而是父佈局的左上角爲座標原點。
getX()、getY()所獲得的座標是視圖座標系中的座標。
觸控事件 —— MotionEvent (P91)
getTop():獲取到的是View自身的頂邊到父佈局的距離。
getLeft():
getRight():。
getBottom():獲取到的是View自身的底邊到父佈局的距離。
getX():獲取點擊事件距離控件左邊的距離,即視圖座標。
getRawX():獲取點擊事件距離整個屏幕的左邊的距離,即絕對座標。
getScrollX():View 左邊緣和 View 內容左邊緣在水平方向上的距離。
控件滑動
public class DragView extends View {
private static final String TAG = "DragView";
private Scroller mScroller;
public DragView(Context context) {
this(context, null);
}
public DragView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DragView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initData(context);
}
private void initData(Context context) {
mScroller = new Scroller(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getViewMeasuredWidth(widthMeasureSpec), getViewMeasuredWidth(heightMeasureSpec));
}
private int getViewMeasuredHeight(int heightMeasureSpec) {
return 0;
}
private int getViewMeasuredWidth(int widthMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int result = 0;
switch (widthMode){
case MeasureSpec.EXACTLY:
Log.i(TAG, "EXACTLY: ");
break;
case MeasureSpec.AT_MOST: //wrap_content
Log.i(TAG, "AT_MOST: ");
result = 200;
result = Math.min(result, widthSize);
break;
}
return result;
}
private int lastX;
private int lastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
/**
* 通過layout方法使得View達到滑動效果
* getX() 獲取的是點擊事件距離控件左邊的距離,即視圖座標
* getRawX() 獲取的是點擊事件距離整個屏幕左邊的距離,即絕對距離
*/
//textGetXY(event);
//textGetRawXY(event);
/**
* 通過使用offsetLeftAndRight(offsetX)使得View達到平移的效果
*/
//textOffset(event);
/**
* 使用LayoutParams使得view達到平滑效果
*/
//textLayoutParams(event);
/**
* 使用scrollBy使得view達到平滑效果
*/
//textScrollToOrBy(event);
textScrollSlowly(event);
return true;
}
private void textScrollSlowly(MotionEvent event) {
int startX = (int) event.getRawX();
int startY = (int) event.getRawY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = startX;
lastY = startY;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = startX - lastX;
int offsetY = startY - lastY;
((View)getParent()).scrollBy(-offsetX, -offsetY);
lastX = startX;
lastY = startY;
break;
case MotionEvent.ACTION_UP:
View viewGroup = (View) getParent();
/**
* getScrollX(): 總是等於View左邊緣和View內容左邊緣在水平方向上的距離
* 要注意正負情況和ScrollBy()、ScrollTo()是一樣的
*
* viewGroup.getScrollX()獲得的是當前viewGroup左邊界和ViewGroup內容左邊緣在水平方向上的距離,
* 也就是當前view左邊界和ViewGroup左邊界的距離,如果view沒有滑動前在(0,0),那麼現在就是滑動後的座標的相反數
*
* -viewGroup.getScrollX(), -viewGroup.getScrollY() 讓滑動後的view再回原來位置
*/
mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(), -viewGroup.getScrollX(), -viewGroup.getScrollY());
invalidate();
break;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
private void textScrollToOrBy(MotionEvent event) {
int startX = (int) event.getRawX();
int startY = (int) event.getRawY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = startX;
lastY = startY;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = startX - lastX;
int offsetY = startY - lastY;
/**
* 並不能移動,因爲scrollTo、scrollBy移動的是view的內容
*/
//scrollBy(offsetX, offsetY);
/**
* 移動了ViewGroup中的所有子view
*
* scrollBy :視圖移動的知識,相當於:view控件固定在手機屏幕上,而屏幕下是一個巨大的畫布,也就是我們想要展示的視圖,
* scrollBy僅僅是移動了view控件和手機屏幕,而view中的文本是畫在畫布上的,沒有隨着控件和手機屏
* 幕滑動。 因此,要用-offsetX, -offsetY
*/
((View)getParent()).scrollBy(-offsetX, -offsetY);
lastX = startX;
lastY = startY;
break;
case MotionEvent.ACTION_UP:
break;
}
}
private void textLayoutParams(MotionEvent event) {
int startX = (int) event.getRawX();
int startY = (int) event.getRawY();
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = startX;
lastY = startY;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = startX - lastX;
int offsetY = startY - lastY;
/**
* 在這裏使用getLeft()使得view滑動紊亂 layoutParams.leftMargin才行
*/
layoutParams.leftMargin = layoutParams.leftMargin + offsetX;
layoutParams.topMargin = layoutParams.topMargin + offsetY;
setLayoutParams(layoutParams);
lastX = startX;
lastY = startY;
break;
case MotionEvent.ACTION_UP:
break;
}
}
/**
* offsetLeftAndRight(offsetX)、offsetTopAndBottom(offsetY)
* 相當於對左右、上下平移的一個封裝
* @param event
*/
private void textOffset(MotionEvent event) {
int startX = (int) event.getRawX();
int startY = (int) event.getRawY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = startX;
lastY = startY;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = startX - lastX;
int offsetY = startY - lastY;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
lastX = startX;
lastY = startY;
break;
case MotionEvent.ACTION_UP:
break;
}
}
/**
* 使用getRawX()的時候,每次執行完ACTION_MOVE需要對lastX重新賦值,爲什麼?
* 因爲getRawX()獲取的是點擊事件距離整個屏幕左邊的距離,即絕對距離,在控件滑動的時候,lastX時刻發生變化
* @param event
*/
private void textGetRawXY(MotionEvent event) {
int startX = (int) event.getRawX();
int startY = (int) event.getRawY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = startX;
lastY = startY;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = startX - lastX;
int offsetY = startY - lastY;
layout(getLeft()+offsetX, getTop()+offsetY, getRight()+offsetX, getBottom()+offsetY);
lastX = startX;
lastY = startY;
break;
case MotionEvent.ACTION_UP:
break;
}
}
/**
* 使用getX()的時候不需要每次執行完ACTION_MOVE重新賦值,爲什麼?
* 原因:由於ACTION_DOWN中的lastX是當前按壓點到當前控件左邊界的距離,
* 而當手指移動一個像素,控件也應該移動一個像素,所以lastX應該是不變的,所以不需要重新賦值。
*/
private void textGetXY(MotionEvent event) {
int startX = (int) event.getX();
int startY = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = startX;
lastY = startY;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = startX - lastX;
int offsetY = startY - lastY;
layout(getLeft()+offsetX, getTop()+offsetY, getRight()+offsetX, getBottom()+offsetY);
break;
case MotionEvent.ACTION_UP:
break;
}
}
}
ViewDragView–實現類似QQ側邊欄的效果
public class DragViewGroup extends FrameLayout {
private static final String TAG = "DragViewGroup";
private ViewDragHelper mViewDragHelper;
private View mMainView,mMenuView;
private int mWidth;
public DragViewGroup(@NonNull Context context) {
this(context, null);
}
public DragViewGroup(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DragViewGroup(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
initData();
}
private void initData() {
/**
* ViewDragHelper的構造方法是私有的
*/
mViewDragHelper = ViewDragHelper.create(this, mCallback);
}
/**
* 當佈局的xml文件加載完成後調用該方法
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getMeasuredWidth();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
/**
* 把觸摸事件傳遞給ViewDragHelper,這一步必不可少
*/
mViewDragHelper.processTouchEvent(event);
return true;
}
private ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
/**
* 剛觸摸屏幕的時候就調用該方法,用於判斷何時開始檢測屏幕,
* 該例中當觸摸到的View爲mMainView的時候返回true,表示開始檢測屏幕
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
return mMainView == child;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return super.clampViewPositionVertical(child, top, dy);
}
/**
* 觸摸屏幕的時候不斷調用該方法
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
/**
* 拖動結束後鬆開會調用該方法
*/
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
if (mMainView.getLeft() < mWidth/2){
/**
* 與下面兩個方法類似:
* mScroller.startScroll(x, y, dx, dy);
* invalidate();
*
* 查看下面兩個方法的源碼,內部也是調用的 mScroller.startScroll()、invalidate();
* 那麼就得重寫computeScroll()
*/
mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}else {
mViewDragHelper.smoothSlideViewTo(mMainView, mWidth/3, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
/**
* 在用戶觸摸到view後回調
*/
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
Log.i(TAG, "onViewCaptured: ");
}
/**
* 拖曳狀態發生改變的時候回調,整個過程只調用三次,如果單擊的話調用兩次
* 1:手指剛開始按壓屏幕拖拽的時候回調(1)
* 2:手指剛離開屏幕的時候回調(2),離開後view停止滑動的時候回調(3)。
* 3:手指一直按壓在屏幕上的時候,此時就算停止拖動,或者停止後再拖動都不會調用該方法
*
* @param state
*/
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
Log.i(TAG, "onViewDragStateChanged: ");
}
/**
* 拖動的時候View位置發生改變的時候回調該方法
*/
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
Log.i(TAG, "onViewPositionChanged: ");
}
};
@Override
public void computeScroll() {
super.computeScroll();
if (mViewDragHelper.continueSettling(true)){
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
}