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);
}
}
}