本文所有内容来自网络,通过本人梳理编撰成文。文章仅对知识要点做罗列整理,属于知识大纲,预期读者为具备相关开发经验的研发,不建议初学者阅读。如果需要进一步探究,可以查看参考资料查看原文。
UI基础概念
坐标系
屏幕坐标系
移动设备的坐标系一般定义屏幕的左上角为坐标原点,向右为X轴增大方向,向下为Y轴增大方向。全局偏移量是指控件相对屏幕坐标系原点的偏移量。控件的屏幕全局偏移量可以通过以下方式获取:
view.getLocationOnScreen(int[] location)
获取相对屏幕的全局坐标view.getLocationInWindow(int[] location)
获取相对窗口的全局坐标,当窗口与屏幕重合时就等于全局偏移量view.getGlobalVisibleRect(Rect rect)
获取相对屏幕的全局坐标
view.getWindowVisibleDisplayFrame(rect)
可获取可见view所在window的全局坐标,比如可以通过rect.top
来获取状态栏高度。由于是从WMS获取,涉及IPC,注意性能。
View坐标系
控件的坐标系是相对于父控件而言的,对应局部偏移量。可以通过下述方式获取view的局部偏移量:
- 通过
view.getLeft()/view.getRight()/view.getTop()/view.getBottom()
获取四条边偏移量(float类型),此数值是在布局完成后赋值的 - 通过
view.getX()
/view.getY()
获取局部x/y轴偏移量(float类型),这是实际局部偏移量 - 通过
view.getLocalVisibleRect(Rect rect)
获取局部偏移量,与left/top相对应
其中:
x = left + translationX
y = top + translationY
当我们执行view的位置偏移动画时便是通过修改translationX/translationY
实现的。
事件坐标系
触摸事件的坐标提供了上述两个坐标系,即一个相对于整个手机屏幕的坐标,另一个相对于当前控件的坐标。
角度deg与弧度rad
-
大小关系:
360(deg) = 2π(rad) -
增大方向:
颜色
- 类型
类型 | 枚举 | 存储格式 |
---|---|---|
四通道颜色(32位) | ARGB8888 | 0x00000000->0xffffffff |
三通道颜色(16位) | RGB888 | 0x000000->0xffffff |
- 相关API
- int color =Color.GRAY;
- int color =Color.argb(127,255,0,0);
- int color = 0xaaff0000; //0x大小写敏感,其他大小写不敏感
- int color= Color.parseColor("#ff00ff00");
- int color = context.getResources().getColor(R.color.mycolor);
- xml中颜色硬编码:
#00ff0000
绘制操作
视图构成
- Activity:内部有个Window成员,它的实例为PhoneWindow;
- PhoneWindow:有个内部类是DecorView;
- DecorView:继承FrameLayout用于存放布局文件,里面有TitleActionBar和setContentView传进去的layout布局文件,其中id=content的就是我们传入的布局视图;
绘制流程
DecorView被加载到Window流程:
- 从Activity的startActivity开始,最终调用到ActivityThread的handleLaunchActivity方法来创建Activity,首先,会调用performLaunchActivity方法,内部会执行Activity的onCreate方法,从而完成DecorView和Activity的创建。然后,会调用handleResumeActivity,里面首先会调用performResumeActivity去执行Activity的onResume()方法,执行完后会得到一个ActivityClientRecord对象,然后通过r.window.getDecorView()的方式得到DecorView,然后会通过a.getWindowManager()得到WindowManager,最终调用其addView()方法将DecorView加进去。
- WindowManager的实现类是WindowManagerImpl,它内部会将addView的逻辑委托给WindowManagerGlobal,可见这里使用了接口隔离和委托模式将实现和抽象充分解耦。在WindowManagerGlobal的addView()方法中不仅会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView通过root.setView()把DecorView加载到Window中。这里的ViewRootImpl是ViewRoot的实现类,是连接WindowManager和DecorView的纽带。View的三大流程均是通过ViewRoot来完成的。
扩展阅读:
measure
MeasureSpec
MeasureSpec
表示的是一个32位的整形值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize,它有三种测量模式:
EXACTLY
:精确测量模式,视图宽高指定为match_parent或具体数值时生效,表示父视图已经决定了子视图的精确大小,这种模式下View的测量值就是SpecSize的值。AT_MOST
:最大值测量模式,当视图的宽高指定为wrap_content时生效,此时子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。UNSPECIFIED
:不指定测量模式, 父视图没有限制子视图的大小,子视图可以是想要的任何尺寸,通常用于系统内部,应用开发中很少用到。
打包方法为makeMeasureSpec,解包方法为getMode和getSize。
layout
未完待续…
LayoutParams
未完待续…
draw
未完待续…
Canvas
未完待续…
Paint
未完待续…
Path
未完待续…
触摸事件
事件分发机制
流程伪代码:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
coonsume = child.dispatchTouchEvent(ev);
}
return consume;
}
View#dispatchTouchEvent
:分发事件的入口方法true
表示事件被当前视图消费掉;false
表示交给父类的onTouchEvent处理;super.dispatchTouchEvent
表示继续分发该事件;
ViewGroup#onInterceptTouchEvent
:容器决绝是否自己消费还是向下传递true
表示拦截这个事件并交由自身的onTouchEvent方法进行消费,比如ScrollView、ListView等支持交互的容器;false
表示不拦截需要继续传递给子视图,如LinearLayout、 RelativeLayout等纯布局容器。super.onInterceptTouchEvent(ev)
根据子View情况决定:- 存在子View且点击到了该子View, 则不拦截继续分发给子View处理, 相当于返回false;
- 没有子View或有子View但是没有点击中子View(此时ViewGroup 相当于普通View), 则交由该View的onTouchEvent响应,相当于返回true;
View#onTouchEvent
:消费事件的方法true
表示当前视图可以处理对应的事件;false
表示当前视图不处理这个事件,交给父View的onTouchEvent方法进行处理;super.onTouchEvent(ev)
控件默认策略:- 如果该View是
clickable
或者longclickable
的,则返回true表示消费了该事件,如Button等系统默认的可触摸交互的控件; - 如果该View非
clickable
或者longclickable
的,则返回false表示不消费该事件,如TextView等默认不支持触摸交互的控件;
- 如果该View是
特别注意:
- Activity也拥有分发和消费两个方法;
- 事件的传递是从外向内(activity最先),而消费是从内向外(activity最后);
- 控件消费端三个回调的优先级:
onTouchListener.onTouch
>onTouchEvent
>onClickListener.onClick
;- 事件的消费是按序列来的,一旦消费了ACTION_DOWN,那么后续的ACTION_MOVE、ACTION_DOWN等事件直接交给同一个View处理;
- View的enable属性不影响onTouchEvent的默认返回值;
- requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外;
扩展阅读:
滑动冲突解决方案
- 外部拦截法:
指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。具体方法:需要重写父容器的onInterceptTouchEvent方法,在内部做出相应的拦截。@Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; switch(event.getAction()) { case MotionEvent.ACTION_DOWN:{ // 不可以拦截 ACTION_DOWN 否则后续事件都会交由父容器来处理 intercepted = false break; } case MotionEvent.ACTION_MOVE:{ if(父容器需要拦截当前事件) { intercept = true; } else { intercept = false; } break; } case MotionEvent.ACTION_UP:{ intercept = false; break; } default: break; } return intercept; }
- 内部拦截法:
指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。具体方法:需要配合requestDisallowInterceptTouchEvent方法。@Override public boolean dispatchTouchEvent(MotionEvent ev) { // 一定要在ACTION_DOWN时就要求父容器不能拦截 if(ev.getAction() == MotionEvent.ACTION_DOWN){ getParent().requestDisallowInterceptTouchEvent(true); } return super.dispatchTouchEvent(ev); }
推荐:当子元素占满父元素空间时使用外部拦截法,当没有占满时使用内部拦截
核心API
MotionEvent
封装了触摸事件的详细数据,包括位置、基础事件类型等信息。
GestureDetector
手势识别封装工具类,对于所有触控点击事件,我们无非就是要分析它的触控操作类型然后执行相应的逻辑,这些逻辑都是可以复用的,因此封装了GestureDetector这个工具类。
总结一下触控操作类型包括:
- 单击SingleTap
时间很短:onDown ----> onSingleTapUp ----> onSingleTapConfirmed
时间稍长:onDown ----> onShowPress ----> onSingleTapUp ----> onSingleTapConfirmed
当GestureDetector
支持双击时,我们应当以onSingleTapConfirmed
作为单击的回调,因为onSingleTapUp
只要手指离开屏幕就会触发,意味着双击时第一次点击抬起时也会触发。 - 双击DoubleTap
onDown ----> onSingleTapUp ----> onDoubleTap ----> onDoubleTapEvent ----> onDown ----> onDoubleTapEvent
这里需要注意onDoubleTap是在第二次点击的down回调前触发,而不是第二次点击up的时候。 - 长按LongPress
手指始终在屏幕上不移动:onDown ----> onShowPress ----> onLongPress - 滚动Scroll
手指始终在屏幕上发生移动:onDown ----> [onShowPress手指停留115毫秒不移动触发] ----> onScroll… - 抛Filing
手指在触摸移动过程中离开屏幕:onDown ----> onScroll… ----> onFling,理论上只要手指离开时还具有滚动速度,那么onFling就会触发,现实中屏幕操作基本是符合这个现象的,所以Scroll最后往往伴随Filing。
GestureDetector
的用法分两步:
- 构造GestureDetector实例,需传入
GestureDetector.OnGestureListener
,这是处理手势事件的地方; - 代理
boolean onTouchEvent(MotionEvent event)
或boolean onTouch(View v, MotionEvent event)
方法进行事件分析。
一个简单的示例如下:
public class GestureActivity extends BaseActivity {
private static final String TAG = "GestureActivity";
private TextView mTextView;
private GestureDetector mDetector;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mDetector = new GestureDetector(this, getCallback());
View content = findViewById(android.R.id.content);
content.setFocusable(true);
content.setClickable(true);
content.setLongClickable(true);
content.setOnTouchListener((v, event) -> mDetector.onTouchEvent(event));
}
private GestureDetector.OnGestureListener getCallback() {
return new GestureDetector.SimpleOnGestureListener(){
@Override
public boolean onSingleTapUp(MotionEvent e) {
log( "onSingleTapUp() called with: e = [" + e + "]");
return false;
}
@Override
public void onLongPress(MotionEvent e) {
super.onLongPress(e);
log( "onLongPress() called with: e = [" + e + "]");
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
log( "onScroll() called with: e1 = [" + e1 + "], e2 = [" + e2 + "], distanceX = [" + distanceX + "], distanceY = [" + distanceY + "]");
return false;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
log( "onFling() called with: e1 = [" + e1 + "], e2 = [" + e2 + "], velocityX = [" + velocityX + "], velocityY = [" + velocityY + "]");
return false;
}
@Override
public void onShowPress(MotionEvent e) {
super.onShowPress(e);
log( "onShowPress() called with: e = [" + e + "]");
}
@Override
public boolean onDown(MotionEvent e) {
log( "onDown() called with: e = [" + e + "]");
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
log( "onDoubleTap() called with: e = [" + e + "]");
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
log( "onDoubleTapEvent() called with: e = [" + e + "]");
return false;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
log( "onSingleTapConfirmed() called with: e = [" + e + "]");
return false;
}
@Override
public boolean onContextClick(MotionEvent e) {
log( "onContextClick() called with: e = [" + e + "]");
return false;
}
};
}
}
ViewDragHelper
封装了控件拖拽逻辑的工具类,用在自定义ViewGroup内部快速实现子View的拖拽效果。使用方面也非常简单:
- 初始化ViewDragHelper,需要传入一个ViewDragHelper.Callback,这是整个拖拽逻辑的核心部分;
- 代理ViewGroup的onInterceptTouchEvent方法;
- 代理ViewGroup的onTouchEvent方法;
一个简单的示例如下:
public class DragContainer extends ConstraintLayout {
private final ViewDragHelper helper;
public DragContainer(Context context) {
this(context, null);
}
public DragContainer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragContainer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
helper = ViewDragHelper.create(this, new ViewDragHelper.Callback() {
Integer rightBound;
@Override
public boolean tryCaptureView(View child, int pointerId) {
// child是被触摸的子view
// pointerId来自MotionEvent#getPointerId(int index),区间[0, MotionEvent#getPointerCount()-1]
return true;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
// 1. 不对横向拖拽做限制
// return left;
// 2. 禁用横向拖拽
// return 0;
// 3. 限制横向拖拽范围不超出父控件边界
if (rightBound == null) {
int[] location = new int[2];
getLocationOnScreen(location);
rightBound = location[0] + getWidth() - child.getWidth();
}
return Math.min(Math.max(left, 0), rightBound);
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
});
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return helper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
helper.processTouchEvent(event);
return true;
}
}
然后在布局中使用就可以了,比如在下面的布局中view_crop这个控件就可以直接拖拽了:
<?xml version="1.0" encoding="utf-8"?>
<com.hao.android_api_test.view.DragContainer xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/robot">
<View
android:id="@+id/view_crop"
android:layout_width="200dp"
android:layout_height="50dp"
android:background="@drawable/shape_border"/>
</com.hao.android_api_test.view.DragContainer>
动画体系
未完待续…