吐槽
这几天晚上老睡不着,然后只能晚上听极客时间上老师的课程,听着听着就越来越兴奋了233,以后晚上11点半就要熄灯了,自己也要早点睡觉,然后早上起床早点哈,控制好生物钟。
本文思维导图
主要是是看《安卓艺术开发》第三章的学习笔记
好好把安卓的view的基础知识过一遍
1 View基础知识
主要就是把View里面零碎的知识总结下
1.1 什么是View
学了这么久安卓,突然看到这个问题,感觉无从下手,对我来说,view就是安卓界面上面各种能让别人看到的东西,不论是自己写的,还是系统自带的,都要调用它安卓View
- View是安卓所有控件的基类
- Android里所有与用户交互的控件的父类
- View是界面层的一种抽象,代表一个控件
- ViewGroup代表控件组,里面包含很多控件,继承View
- 每一个View都有一个用于绘图的画布,这个画布可以进行任意扩展
1.2 View的位置参数
首先看下坐标系
安卓因为和手机很相关,所以它这块的坐标系也和之前学的数学的坐标系不一样
android的坐标系定义
- 屏幕的左上角为坐标原点
- 向右为x轴增大方向
- 向下为y轴增大方向
View的位置描述
下面两点很重要
- View的位置是相对于父控件而言的
- View的位置由4个顶点决定的
其中四个顶点分别是
Top = getTop() //子View上边界到父view上边界的距离
Left = getLeft() //子View左边界到父view左边界的距离
Bottom = getBottom() //子View下边距到父View上边界的距离
Right = getRight() //子View右边界到父view左边界的距离
根据上面的图,和四个顶点的,我们也很容易得出View的宽高和坐标的关系
width = right - left
height = bottom - top
看下图也很明显的能得到这块的
然后还有两组方法:
getTanslationX() getTranslationY()
Android3.0之后提供的两个方法,getTranslationX()和getTranslationY(),它们不同于上面的四个参数,这两个参数会由于 View的平移而变化,表示View左上角坐标相对于left、top(原始左上角坐标)的偏移量。
看图就好了emmmmmm
然后还有最后一组
getX() getY()
Android3.0之后提供了getX()和getY()两个方法。
进去看下源码
/**
* The visual x position of this view, in pixels. This is equivalent to the
* {@link #setTranslationX(float) translationX} property plus the current
* {@link #getLeft() left} property.
*
* @return The visual x position of this view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "drawing")
public float getX() {
return mLeft + getTranslationX();
}
一目了然,很清楚,这个方法就是调用getTanslationX()方法,另一个方法肯定也是这样的哈哈哈
代码是将mLeft加上translationX得到x的,可以看出来,x和y代表的就是当前View左上角相对于父布局的偏移量。
可以看图很明显得到一个等式
x = left + translationX;
y = top + translationY;
1.3 MotionEvent
在手指接触屏幕之后产生的一系列事件中,典型的事件有下面3种:
- ACTION_DOWN——手指刚接触屏幕
- ACTION_MOVE——在屏幕上移动
- ACTION_DOWN——从屏幕上松开
我们进MotionEvent这个类的源码去看下
public final class MotionEvent extends InputEvent implements Parcelable {
private static final long NS_PER_MS = 1000000;
private static final String LABEL_PREFIX = "AXIS_";
public static final int INVALID_POINTER_ID = -1;
public static final int ACTION_MASK = 0xff;
public static final int ACTION_DOWN = 0;
public static final int ACTION_UP = 1;
public static final int ACTION_MOVE = 2;
public static final int ACTION_CANCEL = 3;
...........................................
进去发现一大堆静态变量emmmmm,别人的代码写的真的舒服整齐,里面也发现了我们上面写的那些
正常操作的情况下,一次手指触摸屏幕的行为会触发一大堆点击事件
- 点击屏幕后离开松开 DOMN->UP
- 点击屏幕滑动一段时间再松开,事件序列为 DOWN->MOVE->…MOVE->UP
我们发现这个就是手指在屏幕移动时候发生的,也可以得到手指在滑动的时候的坐标值
可以通过MotionEvent对象调用getX()、getY()、getRawX()、getRawY()获取触碰点的位置参数。
- getX()、getY() 相对于当前View左上角的x、y值
- getRawX()、getRawY() 相对于手机屏幕左上角的x、y值。
我们进去看下这块的源码
public final float getX() {
return nativeGetAxisValue(mNativePtr, AXIS_X, 0, HISTORY_CURRENT);
}
/**
* {@link #getY(int)} for the first pointer index (may be an
* arbitrary pointer identifier).
*
* @see #AXIS_Y
*/
public final float getY() {
return nativeGetAxisValue(mNativePtr, AXIS_Y, 0, HISTORY_CURRENT);
}
好像这块都是调用nativeGetAxisValue方法
然后我们再看下MotionEvent的基本用法
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mode){
control1.x = event.getX();
control1.y = event.getY();
}else {
control2.x = event.getX();
control2.y = event.getY();
}
invalidate();
return true;
}
我们发现这个方法会返回一个boolean的值,但是之前自己好像也没在意过这个值是干嘛的,一直都是自己返回的true,这块好像和View的事件分发有关,自己去网上查了下
true:
1.告诉Android,MotionEvent对象已被使用,不能再提供给其他方法。
2.还告诉Android,继续将此触摸序列的触摸事件(move,up)发送到此方法。
false:
1.告诉Android,onTouch()方法未使用该事件,所以Android寻找要调用的下一个方法。
2.告诉Android。不再将此触摸序列的触摸事件(move,up)发送到此方法。
这块先放在这里,等看到view的事件分发那块我再重新看
1.4 TouchSlop
这个就是安卓里面系统能识别出来的滑动的最小距离,如果手指滑动的距离比这个值少的话,就是系统默认不滑动,感觉这个机制蛮合理的,万一用户轻轻一触碰就触发滑动就很尴尬了
- 这个是个常量
- 这个和设备有关
- 用来过滤滑动距离很少的情况
获取这个值的方式:
int TouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
然后我们进去看下getScaledTouchSlop()方法
/**
* @return Distance in pixels a touch can wander before we think the user is scrolling
*/
public int getScaledTouchSlop() {
return mTouchSlop;
}
然后我们再找啊找
mTouchSlop = TOUCH_SLOP;
private static final int TOUCH_SLOP = 8;
所以,在里面默认的是8dp
在处理滑动的时候可以使用这个值来做一些过滤,过滤掉滑动距离小于这个值,会有更好的用户体验。
1.5 VelocityTracker
这个是获取用户滑动过程中的速度,包括水平速度和竖直速度的,
用法如下
1.先获得一个VelocityTracker对象,然后把时间传入
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
2.计算自定义时间内的速度,再调用get获得定义时间内划过的像素点。
velocityTracker.computeCurrentVelocity(1000);
int xV = (int)velocityTracker.getXVelocity();
int yV = (int)velocityTracker.getYVelocity();
3.回收内存
velocityTracker.clear();
velocityTracker.recycle();
注意:
- 这里的速度就是指一段时间内的手指划过的像素的点的数
- 手指从右->左 速度为负,反之为正//和坐标系有关
- 获取速度之前必须要调用computeCurrentVelocity()计算速度。
- getXVelocity()\getYVelocity()获取到的是计算单位时间内滑过的像素值,并不是速度。
1.5 GestureDetector
手势检测,辅助检测用户的单击,滑动,长按,双击的情况
这里分享一个大佬的博客,里面讲的很清楚
大佬博客地址
我们看下这块整体的结构
然后我们进入源码看下这个GestureDetector类
public class GestureDetector {
public interface OnGestureListener {
boolean onDown(MotionEvent e);//手指轻轻触摸屏幕的瞬间,一个ACTION_DOWN触发
void onShowPress(MotionEvent e);//手指轻触屏幕,没有松开或挪动
boolean onSingleTapUp(MotionEvent e);轻触后松开,单击行为,伴随一个ACTION_UP触发
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);拖动行为,由一个ACTION_DOWN和一系列ACTION_MOVE触发
.................
}
public interface OnDoubleTapListener {
boolean onSingleTapConfirmed(MotionEvent e);//严格的单击行为,不能是双击中的其中一次单击,onSingleTapUp可以是双击中的其中一次
boolean onDoubleTap(MotionEvent e);//双击,两次单击,不可能和onSingleTapConfirmed共存
boolean onDoubleTapEvent(MotionEvent e);
}//双击行为,双击期间ACTION_DOWN ACTION_MOVE ACTION_UP都会触发此回调。
public interface OnContextClickListener {
boolean onContextClick(MotionEvent e);
}
..........................
}
仔细看了下这个类,里面就主要有三个接口,每个接口里面有不同的方法,这些方法就是触摸回调,实现了这些方法,就能实现传入触摸事件之后做出相应的回调
然后我们来看下使用过程:
第一步:目标实现OnGestureListener接口
public class XXXActivity extends Activity implements GestureDetector.GestureListener{
//下面会有6个方法,开发的时候,不建议直接在Activity中使用,先封装后使用是更好的办法
}
第二步:创建一个GestureDetector对象,并初始化它
private GestureDetector gestureDetector ;
.....
protect void onCreat(Bundle saveInstanceStated){
......
//这样的写法有一个前提,就是这个Activity实现了OnGestureListener接口
gestureDetecture = new GestureDetector(this,this);
}
第三步:重写目标onTouchEvent(XXX)的方法 Activity中的OnTouchEvent事件交给手势监听器处理:
public boolean onTouchEvent(MotionEvent event){
return gestureDetector.onTouchEvent(event);
}
2 View的滑动
在安卓里面有很多的滑动的情况,比如下拉,左右滑动切换界面什么的,也是安卓中比较重要的一方面,有三种方法
2.1 scrollTo/scrollBy
- 在android中每一个view里都有这两个方法,所以理论上所有的view都是可以滑动的
- scrollTo是相对于绝对滑动的//就是相对于View的初始位置
- scrollBy是相对于当前位置的,位置一直是移动的
我们来看下这块的源码:
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);
}
我们看完这块的源码就可以知道:
- scrollBy实际上也是调用了scrollTo
- scorllTo()首先比较内容偏移量和传入的x y是否相等,都不相等再操作。
- 接着调用了invalidateParentCaches(),方法注释意思是当启动了硬件加速时去通知此View的父容器清除缓存。
- 调用了onScrollChanged(mScrollX, mScrollY, oldX, oldY),这个方法内部会判断我们是否有设置OnScrollChangeListener,如果有就调用它的回调方法。
- scrollTo使基于所传参数的绝对滑动(比如:当前坐标是(1,1)所传参数x:2,y:2,最终会滑动到(2,2))
- scrollBy使基于当前位置的相对滑动(比如:当前坐标是(1,1)所传参数x:2,y:2,最终会滑动到(1+2,1+2))
我们分析下这个过程:
滑动过程中View内部的两个属性mScrollX和mScrollY的改变规则
分别可以通过getScrollX、getScrollY获得
- scrollTo/scrollBy只是改变了View中内容的位置,并没有改变View的实际位置
- 在滑动过程中,mScrollX的值总是等于View的左边缘到View内容的左边缘的水平距离
- mScrollY的值总是等于View的上边缘到View内容的上边缘的竖直距离
- mScrollX/mScrollY单位是像素
- 从左向右滑动时mScrollX为负数 从上向下滑动时mScrollY为负数
反正,简单来说逻辑就是改变mScrollX和mScrollY的值,之后刷新UI,显示在新位置。这个滑动不改变View的位置,只是内容的位置
2.2 动画的方式
通过安卓里面的动画让View去平移
第一步:在xml里面定义一个动画集合
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal">
<translate
android:duration="100"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="100"
android:toYDelta="100"
android:interpolator="@android:anim/linear_interpolator"/>
</set>
这个就是让view向右下角移动
再对View对象开始动画,传入加载进来的上面写的动画。
tv.startAnimation(AnimationUtils.loadAnimation(MainActivity.this, R.anim.anim_view_event));
第二步:使用属性动画
ObjectAnimator.ofFloat(tv, "translationX", 0, 10).setDuration(100).start();
注意的地方:
- View动画是对View影像进行操作,不是真的233
- 点击事件什么的还是在原来的位置
2.3 改变布局参数
这块很简单,比如我们想把一个Button向右平移100px,我们只需要把这个Button的LayoutParams里面的marginLeft增加100px,类似这种形式
MarginLayoutParams params = (MarginLayoutParams) tv.getLayoutParams();
params.leftMargin += 100;
tv.requestLayout();
//tv.setLayoutParams(params); 也可以使用这个重新设置参数
LayoutParams继承于Android.View.ViewGroup.LayoutParams.
LayoutParams相当于一个Layout的信息包,它封装了Layout的位置、高、宽等信息。假设在屏幕上一块区域是由一个Layout占领的,如果将一个View添加到一个Layout中,最好告诉Layout用户期望的布局方式,也就是将一个认可的layoutParams传递进去。
3 弹性滑动
因为之前我们看到的view的滑动都是特别特别死板那种,不流畅,这样只能算是移动,如果我们要实现滑动的话,我觉得我们就是把一次大的滑动分为很多份小的滑动,每个小的滑动可以看成一次移动
3.1 Scoller
Scroller本身无法实现弹性滑动,需要和View的computeScroll()配合使用。在最后通过分析可以发现也是通过scrollTo()实现滑动的,所以它也是View内容的滑动,而不是View本身的滑动。
private Scroller mScroller;
public void smoothScroll(int destX, int destY) {
//画的初始滑动偏移
int scrollX = getScrollX();
int scrollY = getScrollY();
//计算需要滑动的两个方向的大小
int deltaX = -destX - scrollX;
int deltaY = -destY - scrollY;
调用Scroller对象的startScroll()
mScroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000);
invalidate();//重绘
}
//固定的重写compuuteScroll
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
- 初始化一个Scroller对象
- 实现computeScroll()方法
- 自定义滑动内容
- 刷新
- 调用
看了下自己之前写的例子
public class CustomView extends View {
private int lastx;
private int lasty;
private Scroller mscroller;
public CustomView(Context context) {
super(context);
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mscroller = new Scroller(context);
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int)event.getX();
int y = (int)event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastx = x;
lasty = y;
break;
case MotionEvent.ACTION_MOVE:
int offx = x - lastx;
int offy = y - lasty;
//用layout方法重新放置他的位置
// layout(getLeft()+offx,getTop()+offy,getRight()+offx,getBottom()+offy);
//offsetLeftAndRight
//offsetLeftAndRight(offx);
//offsetTopAndBottom(offy);
//LayoutParams 布局参数
// LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)getLayoutParams();
// layoutParams.leftMargin = getLeft()+offx;
// layoutParams.topMargin = getTop()+offy;
// setLayoutParams(layoutParams);
//scrollTO和scrollBy
// ((View)getParent()).scrollBy(-offx,offy);
smoothScrollTo(-lastx,-lasty);
break;
}
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
if (mscroller.computeScrollOffset()){
((View)getParent()).scrollBy(mscroller.getCurrX(),mscroller.getCurrY());
invalidate();
}
}
public void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
mscroller.startScroll(scrollX,0,delta,0,2000);
invalidate();
}
}
里面有两个方法要看下
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
startScroll()只是进行了一些计算和参数的记录,并没有进行真正的滑动工作。四个参数分别是其实位置的x、y坐标,x、y方向的滑动距离,滑动的时间间隔。
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
这个类也很神奇
- 它首先判断是否完成,如果已经完成就直接返回false。
- 如果还没完成,计算过去的时间,如果还有剩余,就根据时间百分比计算下一个滑动位置,返回true。
- 如果已经超过时间,就赋值下一个滑动位置为目标位置,并将mFinished变成true,返回true。
- 在调用computeScrollOffset()的地方,如果computeScrollOffset()返回了true就进行scrollTo()并重新绘制。
类似于一个差值器
3.2 动画属性
动画本身就是一种渐进的方式
final int startX = 0;
final int deltaX = -100;
final ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
tv.scrollTo(startX + (int)(deltaX * fraction), 0);
}
});
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
animator.start();
}
});
利用动画的回调,实现像Scroller类似的,在动画改变的时候通过onAnimationUpdate()监听,获得百分比,调用scrollTo()滑动一小步,也是View内容的滑动。
3.3 延时策略
通过发送延时消息从而达到渐近式的效果。可以使用Handler、View的postDelayed()、Thread的sleep()