预备知识
-
Android屏幕区域划分
我们先看一副图来了解一下Android屏幕的区域划分,如下:
通过上图我们可以很直观的看到Android对于屏幕的划分定义。下面我们就给出这些区域里常用区域的一些坐标或者度量方式。如下:
//获取屏幕区域的宽高等尺寸获取
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
int widthPixels = metrics.widthPixels;
int heightPixels = metrics.heightPixels;
//应用程序App区域宽高等尺寸获取
Rect rect = new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
//获取状态栏高度
Rect rect= new Rect();
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
int statusBarHeight = rectangle.top;
//View布局区域宽高等尺寸获取
Rect rect = new Rect();
getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(rect);
特别注意:上面这些方法最好在Activity的onWindowFocusChanged ()方法或者之后调运,因为只有这时候才是真正的显示OK。
- Android坐标系、View坐标系、位置的获取、距离的获取和View宽度的获取
在Android中,将屏幕最左上角的顶点作为Android坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向。
在Android中,将View的左上角顶点作为View坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向。
下面我们就来看看在上面两种坐标系下位置的获取、距离的获取和View宽度的获取 的方法。
1中我们分析了Android屏幕的划分,可以发现我们平时开发的重点其实都在关注View布局区域,那么下面我们就来细说一下View区域常用的位置和距离。先看下面这幅图:
通过上图我们可以很直观的给出View一些坐标相关的方法解释,不过必须要明确的是上面这些方法必须要在layout之后才有效,如下:
View的静态坐标方法 | 解释 |
---|---|
getLeft() | 返回View自身左边到父布局左边的距离(返回值是mLeft) |
getTop() | 返回View自身顶边到父布局顶边的距离(返回值是mTop) |
getRight() | 返回View自身右边到父布局左边的距离(返回值是mRight) |
getBottom() | 返回View自身底边到父布局顶边的距离(返回值是mBottom) |
getX() | 返回值为getLeft()+getTranslationX(),当setTranslationX()时getLeft()不变,getX()变。 |
getY() | 返回值为getTop()+getTranslationY(),当setTranslationY()时getTop()不变,getY()变。 |
同时也可以看见上图中给出了手指触摸屏幕时MotionEvent提供的一些方法解释,如下:
MotionEvent坐标方法 | 解释 |
---|---|
getX() | 当前触摸事件距离当前View左边的距离 |
getY() | 当前触摸事件距离当前View顶边的距离 |
getRawX() | 当前触摸事件距离整个屏幕左边的距离 |
getRawY() | 当前触摸事件距离整个屏幕顶边的距离 |
下面我们来看看几个和上面方法紧密相关的获取View宽高的View方法。如下:
View宽高方法 | 解释 |
---|---|
getWidth() | layout后有效,返回值是mRight-mLeft,一般会参考measure的宽度(measure可能没用),但不是必须的。 |
getHeight() | layout后有效,返回值是mBottom-mTop,一般会参考measure的高度(measure可能没用),但不是必须的。 |
getMeasuredWidth() | 返回measure过程得到的mMeasuredWidth值,供layout参考,或许没用。 |
getMeasuredHeight() | 返回measure过程得到的mMeasuredHeight值,供layout参考,或许没用。 |
上面解释了自定义View时各种获取宽高的一些方法,下面我们再来看看获取View可见区域和顶点坐标的一些方法,不过这些方法需要在Activity的onWindowFocusChanged ()方法之后才能使用。如下图:
下面我们就给出上面这幅图涉及的View的一些坐标方法的结果,如下所示:
View的方法 | 上图View1结果 | 上图View2结果 | 结论描述 |
---|---|---|---|
getLocalVisibleRect() | (0, 0, 410, 100) | (0, 0, 410, 470) | 获取View自身可见的坐标区域,坐标以自己的左上角为原点(0,0),另一点为可见区域右下角相对自己(0,0)点的坐标,其实View2当前height为550,可见height为470。 |
getGlobalVisibleRect() | (30, 100, 440, 200) | (30, 250, 440, 720) | 获取View在屏幕绝对坐标系中的可视区域,坐标以屏幕左上角为原点(0,0),另一个点为可见区域右下角相对屏幕原点(0,0)点的坐标。 |
getLocationOnScreen() | (30, 100) | (30, 250) | 坐标是相对整个屏幕而言,Y坐标为View左上角到屏幕顶部的距离。 |
getLocationInWindow() | (30, 100) | (30, 250) | 如果为普通Activity则Y坐标为View左上角到屏幕顶部(此时Window与屏幕一样大);如果为对话框式的Activity则Y坐标为当前Dialog模式Activity的标题栏顶部到View左上角的距离。 |
通过layout方法实现滑动
我们知道,在View进行绘制时,会调用onLayout方法来设置显示的位置。同样,可以通过修改View的mLeft, mTop, mRight, mBottom四个属性来控制View的位置。实现代码如下所示:
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录触摸点坐标
android.util.Log.i("chenyang", "onTouchEvent ACTION_DOWN x = " + x + ", y = " + y);
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 计算偏移量
android.util.Log.i("chenyang", "onTouchEvent ACTION_HOVER_MOVE x = " + x + ", y = " + y);
int offsetX = x - mLastX;
int offsetY = y - mLastY;
// 在当前mLeft, mTop, mRight, mBottom的基础上加上偏移量
layout(getLeft() + offsetX, getTop() + offsetY, getRight()
+ offsetX, getBottom() + offsetY);
break;
default:
break;
}
return true;
}
通过offsetLeftAndRight()与offsetTopAndBottom实现滑动
这两个方法相当于系统提供了一个对左右、上下移动的API的封装。与上面一样,也是通过修改View的mLeft, mTop, mRight, mBottom四个属性来控制View的位置,实现代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录触摸点坐标
android.util.Log.i("chenyang", "onTouchEvent ACTION_DOWN x = " + x + ", y = " + y);
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 计算偏移量
android.util.Log.i("chenyang", "onTouchEvent ACTION_HOVER_MOVE x = " + x + ", y = " + y);
int offsetX = x - mLastX;
int offsetY = y - mLastY;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
default:
break;
}
return true;
}
通过LayoutParams实现滑动
LayoutParams保存了一个View的布局参数,因此可以在程序中,通过改变LayoutParams来动态地修改一个View的布局参数,从而达到改变View位置的效果。我们可以很方便的在程序中使用getLayoutParams()来获取一个View的LayoutParams(注意必须在layout之后才可以获取到)。实现代码如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录触摸点坐标
android.util.Log.i("chenyang", "onTouchEvent ACTION_DOWN x = " + x + ", y = " + y);
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 计算偏移量
android.util.Log.i("chenyang", "onTouchEvent ACTION_HOVER_MOVE x = " + x + ", y = " + y);
int offsetX = x - mLastX;
int offsetY = y - mLastY;
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
default:
break;
}
return true;
}
通过ViewDragHelper实现滑动
Google在其support库中为我们提供了DrawerLayout和SlidingPaneLayout两个布局来帮助开发者实现侧边栏滑动的效果。这两个新的布局大大方便了我们创建自己的滑动布局界面。然而,这两个功能强大的布局背后隐藏着一个鲜为人知却功能强大的类---ViewDragHelper。通过ViewDragHelper基本可以实现各种不同的滑动、拖放需求,因此此方法也是各种滑动解决方案中的终极绝招。
ViewDragHelper虽然功能强大,但其使用方法也是最复杂的。下面通过一个实例,来演示一下如何使用ViewDragHelper创建一个滑动布局,在这个例子中,准备实现类似QQ滑动侧边栏的效果,初始时显示内容界面,当用户手指滑动超过一定距离时,内容界面侧滑显示菜单界面,整个过程下图所示:
实现代码如下所示:
package com.cytmxk.test.scroll;
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
/**
* Created by chenyang on 16/6/26.
*/
public class DragViewGroup extends FrameLayout {
private static final String TAG = DragViewGroup.class.getCanonicalName();
private ViewDragHelper mViewDragHelper = null;
private View mMenuView = null;
private View mMainView = null;
private int mMenuWidth;
public DragViewGroup(Context context) {
super(context);
initView();
}
public DragViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public DragViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
//初始化ViewDragHelper,第一个参数是要监听的View,通常需要是一个ViewGroup,
//即parentView;第二个参数是一个Callback回调,后面会做解释。
mViewDragHelper = ViewDragHelper.create(this, callback);
}
//获取菜单布局的宽度,之后可以根据菜单布局(mMenuView)的宽度处理滑动后的效果。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mMenuWidth = mMenuView.getMeasuredWidth();
Log.d(TAG, "onSizeChanged mMenuWidth = " + mMenuWidth);
}
//初始化菜单布局(mMenuView)和主布局(mMainView)
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//将触摸事件传递给ViewDragHelper,此操作必不可少
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
//将触摸事件传递给ViewDragHelper,此操作必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
// 何时开始检测触摸事件,通过这个方法,我们可以指定在创建ViewDragHelper时,
//参数parentView中的哪一个View可以被移动。
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果当前触摸的child是mMainView时开始检测,并且只有mMainView可以被移动
return mMainView == child;
}
// 触摸到View后回调
@Override
public void onViewCaptured(View capturedChild,
int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
// 当拖拽状态改变,比如idle,dragging
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
// 当位置改变的时候调用,常用与滑动时更改scale等
@Override
public void onViewPositionChanged(View changedView,
int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
// 处理水平滑动
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
// 处理垂直滑动
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
// 拖动结束后调用
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
Log.d(TAG, "onViewReleased mMainView.getLeft() = " + mMainView.getLeft() + ", mMenuWidth = " + mMenuWidth);
//手指抬起后缓慢移动到指定位置
if (mMainView.getLeft() < mMenuWidth) {
//关闭菜单
//相当于Scroller的startScroll方法
mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
} else {
//打开菜单
mViewDragHelper.smoothSlideViewTo(mMainView, mMenuWidth, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
};
//由于ViewDragHelper内部是利用Scroller实现滑动的,所以利用computeScroll方法实现平滑滑动
@Override
public void computeScroll() {
super.computeScroll();
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}
通过scrollTo和scrollBy实现滑动
- 在一个View中,系统提供了scrollTo、scrollBy两种方式来改变一个View中初始可见内容的位置。这两个方法的区别非常好理解,与英文中To和By的区别类似,scrollTo(x, y)表示让View中初始可见内容的在水平方向偏移到点(- x, - y)(x大于零表示向左偏移,否者向右偏移; y大于零表示向上偏移,否者向右偏移),scrollBy(dx, dy)表示让View中初始可见内容的在水平方向偏移dx(dx大于零表示向左偏移,否者向右偏移),在垂直方向偏移dy(dy大于零表示向上偏移,否者向右偏移)如下是这两个方法的代码实现:
/**
* The offset, in pixels, by which the content of this view is scrolled
* horizontally.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollX;
/**
* The offset, in pixels, by which the content of this view is scrolled
* vertically.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollY;
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
有上面的代码可以得知mScrollX,mScrollY是用来保存View初始可见内容的偏移量。理解了mScrollX和mScrollY的用法,就不难理解getScrollX() 和getScrollY()。这两个函数的源码如下所示:
/**
* Return the scrolled left position of this view. This is the left edge of
* the displayed part of your view. You do not need to draw any pixels
* farther left, since those are outside of the frame of your view on
* screen.
*
* @return The left edge of the displayed part of your view, in pixels.
*/
public final int getScrollX() {
return mScrollX;
}
/**
* Return the scrolled top position of this view. This is the top edge of
* the displayed part of your view. You do not need to draw any pixels above
* it, since those are outside of the frame of your view on screen.
*
* @return The top edge of the displayed part of your view, in pixels.
*/
public final int getScrollY() {
return mScrollY;
}
-
举例说明,如下图所示(注意,图中黄色矩形区域表示的是View,绿色虚线矩形为View中初始可见的内容。一般情况下两者的大小一致,本文为了显示方便,将虚线框画小了一点。图中的黄色区域的位置始终不变,发生偏移的是初始可见的内容。):
scrollTo(0, 100)的效果如下图所示:
scrollTo(100, 100)的效果图如下:
若函数中参数为负值,则子View的移动方向将相反:
通过Scroller实现滑动
上面举例中通过scrollTo偏移View的初始可见内容是在瞬间完成的,这样的效果会让人感觉非常突兀。Google也想到了这一点,所以提供了Scroller类来模拟平滑滑动的效果。
Scroller类提供了startScroll方法来初始化一个模拟平滑滑动的过程,然后调用invalidate()方法,这个方法会导致View重绘,系统在绘制View的时候会在draw方法中调用computeScroll方法来实现模拟滑动,在computeScroll方法中通过调用Scroller的computeScrollOffset方法判断是否完成了整个滑动,同时Scroller也提供了getCurrX、getCurrY来获取当前滑动过程中 View初始可见内容 即将的偏移量,然后利用srcollTo方法实现偏移即可,然后执行invalidate方法实现循环调用computeScroll方法直到滑动结束。
- Scroller中相关API简介如下:
mScroller.getCurrX() //获取mScroller当前水平方向滑动过程中的位置
mScroller.getCurrY() //获取mScroller当前竖直方向滑动过程中的位置
mScroller.getFinalX() //获取mScroller最终停止滑动的水平位置
mScroller.getFinalY() //获取mScroller最终停止滑动的竖直位置
mScroller.setFinalX(int newX) //设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置
mScroller.setFinalY(int newY) //设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置
mScroller.startScroll(int startX, int startY, int dx, int dy) //使用默认完成时间250ms
mScroller.startScroll(int startX, int startY, int dx, int dy, int duration)
//开始滑动,startX, startY为 View初始可见内容 开始滑动的位置(即mScrollX,mScrollY的值),dx,dy分别为水平方向和垂直方向的偏移量(dx大于零表示向左偏移,否者向右偏移;dy大于零表示向上偏移,否者向下偏移), duration为完成滚动的时间 。
mScroller.computeScrollOffset() //返回值为boolean,true说明滑动尚未完成,false说明滑动已经完成。这是一个很重要的方法,通常放在View.computeScroll()中,用来判断是否滑动是否结束。
2 举例如下:
public void moveToDest(int index) {
/*
* 对 index 进行判断 ,确保 是在合理的范围
* 即 index >=0 && index <=getChildCount()-1
*/
//确保 index>=0
index = index >= 0 ? index : 0;
//确保 currIndex<=getChildCount()-1
currIndex = index <= getChildCount() - 1 ? index : getChildCount() - 1;
if (null != mOnPagerChangeListener) {
mOnPagerChangeListener.OnPagerChange(currIndex);
}
myScroller.startScroll(getScrollX(), 0, currIndex * getWidth() - getScrollX(), 0, 500);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (myScroller.computeScrollOffset()) {
scrollTo(myScroller.getCurrX(), 0);
invalidate();
}
}
参考文档
- Android应用坐标系统全面详解
- Android群英传