1. View基础知识
- Android
中所有控件的基类,是一种界面层的控件的一种抽象
- ViewGroup
也继承于View
知识点:
-
View
的位置参数 -
MotionEvent
和TouchSlop
对象 -
VelocityTracker
、GestureDetector
和Scroller
对象
1.1 View的位置参数
y = top + translationY |
---|
x= left + translationX |
x
,y
分别代表滑动后的位置
translationX
,translationY
分别代表滑动后相对原始位置的偏移量
1.2 MotionEvent和TouchSlop
1. MotionEvent
手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:
- ACTION_DOWN
- ACTION_MOVE
- ACTION_UP
2. TouchSlop
- 系统所称识别出的被认为是活动的最小距离
- 获取:ViewConfiguration.getContext().getScaledTouchSlop();
Android FingerPaint Undo/Redo implementation
Android Paint的基本用法
1.3 VelocityTracker,GestureDetector和Scroller
1. VelocityTracker
- 速度追踪,用于追踪手指在活动过程中的速度,包括水平和竖直方向的速度
使用方法:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event); //开始追踪事件
velocityTracker.computeCurrentVelocity(1000); //先计算才能获取速度
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
2. GestureDetector
- 手势检测,用于辅助检测用户的单机、滑动、长按、双击等行为
使用方法:
GestureDetector mGestureDetector = new GestureDetector(this);
mGestureDetector.setIsLongpressEnabled(true);
//在待监听View的onTouchEvent方法中添加如下实现
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
Android手势检测——GestureDetector全面分析
GestureDetector 手势的检测
2.1 GestureDetector
的基本用法
2.1.1 创建GestureDetector
gestureDetector = new GestureDetector(this);
2.2.2 实现OnGestureListener
@Override
public boolean onDown(MotionEvent motionEvent) {
return false;
}
@Override
public void onShowPress(MotionEvent motionEvent) {
Toast.makeText(this, "onShoPress", Toast.LENGTH_SHORT).show();
}
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
Log.d(TAG, "onSingleTapUp: ");
Toast.makeText(this, "onSingleTapUp", Toast.LENGTH_SHORT).show();
return true;
}
@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
return false;
}
@Override
public void onLongPress(MotionEvent motionEvent) {
}
@Override
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
return false;
}
2.2.3 GestureDetector接管OnTouch
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
boolean consume = gestureDetector.onTouchEvent(motionEvent);
return consume;
}
3. Scroller
- Scroller本身无法让View弹性滑动,其过程不是瞬间完成的,而是在一定的时间间隔内完成的
2. View的滑动
常见的滑动方式
-
scrollTo
/scrollBy
- 动画
- 改变布局参数
2.1使用scrollTo/scrollBy
- View
的方法
scrollTo是基于所传递参数的绝对滑动
scrollBy相对滑动
左滑/上滑 为正
右滑/下滑 为负
2.2使用动画
- 主要是操作View的translationX和translationY来进行移动
- View动画是对View的影像做操作,它并不能真正改变View的位置参数,包括宽/高
- 设置fillAfter为true,则可以保留动画结果
属性动画不会产生上述问题
ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();
兼容性问题:
- Android 3.0以下无法使用属性动画
解决方法:使用nineoldandroids
- Android 2.2以下无法使用属性动画
解决方法:创建2个Button,隐藏切换来帮助实现点击等效果
2.3改变布局参数
LayoutParams
2.4各种滑动方式的对比
使用scrollTo/scrollBy
优点:原生方法,专门用于View的滑动,方便
缺点:只能滑动View的内容,不能滑动View本身 ????动画
优点:复杂的效果
缺点:适配性问题改变布局
使用场景:一些具有交互性的控件
3. 弹性滑动
- scroller
- 动画
- Thread#sleep or Handler#posterDelayed
3.1 Scroller
- 本身不能实现滑动,需要配合View
里面的computeScroll
3.2 动画
3.3 使用延时策略
例如使用Handler发送消息
4. View的事件分发机制
4.1 点击事件的传递规则
- MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,这个传递的过程就叫做分发过程
- 分发过程由三个方法完成:dispatchTouchEvent
,onInterceptTouchEvent
,onTouchEvent
- dispatchTouchEvent
:进行事件的分发,如果事件能够传递给当前View,那么此方法一定调用
- onInterceptTouchEvent
:用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件
- onTouchEvent
:处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个时间序列中,当前View无法再次接受到事件
- 父级元素如果没有进行拦截则传递给子级元素
- onTouchListener >> onClickListener >> onTouchEvent
- 传递顺序:Activity -> Window -> View
如果一个事件所有元素都不处理,则会传递给Activity
结论
- 同一事件序列是指从手指接触屏幕开始的那一刻,到手指离开屏幕的那一刻产生的一系列事件
- ViewGroup默认不拦截任何事件,默认返回false
- View没有
onInterceptTouchEvent
方法 -
View
的onTouchEvent
默认都会消耗事件,除非它是不可点击的(clickable
和longClickable
都为false
) -
View
的enable
属性不影响onTouchEvent
的默认返回值 - 通过设置子元素的
requestDisallowInterceptTouchEvent
可以在子元素干预父元素的事件分发过程
5. View的滑动冲突
5.1 常见的滑动冲突场景
-
场景1:外部华东方向和内部滑动方向不一致
例如ViewPager
和内部的ListView
处理规则:当用户左右滑动时,需要让外部的VIew拦截点击事件;上下滑动拦截内部点击时间 -
场景2:外部滑动方向和内部滑动方向一致
处理规则:在业务上找到突破点,比如处于某种状态时需要外部View响应用户的滑动,而处于另外一种状态则需要内部View来响应View的滑动 - 场景3:上面两种情况的嵌套
6. View的工作原理
6.1 View的三大过程
-
measure
决定了View的宽/高 -
Layout
决定了View的四个顶点的坐标和实际的宽/高 -
Draw
决定了View的显示
6.2 理解MeasureSpec
6.2.1 MeasureSpec
SpecMode
SpecSize
SpecMode
-
UNSPECIFIED
: 父容器不对View有任何的限制,要多大给多大 -
EXACTLY
: 父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值 -
AT_MOST
: 父容器指定了一个可用大小,View的大小不能超过这个值
6.2.2 MeasureSpec和LayoutParams的对应关系
子
view
的大小由父view
的MeasureSpec
值 和 子view
的LayoutParams
属性 共同决定
6.3 View的工作流程
6.3.1 Measure
1. View的Measure过程
- 只能确定测量大小,最终大小由
Layout
过程确定 - 直接继承
View
的自定义控件需要重写onMeasure
方法并设置wrap_content
时的自身大小,否则在布局中使用wrap_content
相当于使用match_parent
2. ViewGroup的Measure过程
- 不同ViewGroup对
onMeasure
都有不同的实现
-
performTraversals
:整个View
绘制的核心,从measure
到layout
,再从layout
到draw
,全部在这个方法里面完成了。它包括performMeasure
、performLayout
、performDraw
三个方法 -
performMeasure
:调用measure方法,进而调用onMeasure方法,在onMeasure中会对子元素进行measure过程,如果子元素是一个ViewGroup,那么会对子元素进行向下传递,直到所有的元素都遍历到
6.3.2 Layout
-
View
中有Layout
方法,ViewGroup
继承了View
的Layout
,并且用final
修饰
6.3.3 Draw
Draw
的流程:
- 绘制背景
drawBackground(canvas);
- 绘制控件自己本身的内容
onDraw(canvas);
- 绘制子控件
dispatchDraw(canvas);
传递给子View
,如果子View
是一个ViewGroup
,则再进行一次递归 - 绘制装饰(比如滚动条)和前景
onDrawForeground(canvas);
drawDefaultFocusHighlight(canvas);
7. 自定义View
7.1 自定义View简介
- 继承
View
重写onDraw
方法:主要用于实现一些不规则的效果 - 继承
ViewGroup
派生特殊的Layout:主要用于实现自定义的布局 - 继承特定的
View
:主要用于扩展某种已有View的功能 - 继承特定的
ViewGroup
:和2功能类似
自定义View须知:
- 让
View
支持wrap_content
- 如果有必要,让
View
支持Padding
- 尽量不要在
View
中使用Handler
-
View
中的线程和动画要及时停止 -
View
带有滑动嵌套情形时,需要处理好滑动冲突
7.2 示例
7.2.1 继承View重写onDraw方法
public class CircleView extends View {
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init(){
mPaint.setColor(mColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min(width, height) / 2;
canvas.drawCircle(width/2,height/2,radius,mPaint);
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.example.myview.CircleView
android:id="@+id/mCircle"
android:layout_height="100dp"
android:layout_width="wrap_content"
android:background="#000000" />
</LinearLayout>
-
margin
属性由父容器控制,因此不用加以控制 -
padding
控制方法
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingTop = getPaddingTop();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width, height) / 2;
canvas.drawCircle(paddingLeft + width/2,paddingTop + height/2,radius,mPaint);
}
-
wrap_content
控制方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取宽-测量规则的模式和大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 获取高-测量规则的模式和大小
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 设置wrap_content的默认宽 / 高值
// 默认宽/高的设定并无固定依据,根据需要灵活设置
// 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽 / 高值有特殊处理,具体读者可以自行查看
int mWidth = 400;
int mHeight = 400;
// 当布局参数设置为wrap_content时,设置默认值
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, mHeight);
// 宽 / 高任意一个布局参数为= wrap_content时,都设置默认值
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, heightSize);
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(widthSize, mHeight);
}
为什么你的自定义View wrap_content不起作用?
- 自定义属性
第一步:在valus下创建自定义属性XML,比如attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
<!-- 格式为color的属性circle_color-->
</declare-styleable>
</resources>
- 自定义属性还有其他格式,例如reference是指资源的id,dimension指尺寸
第二步:在View的构造方法中解析出自定义属性的值并作出相应处理
public CircleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView); //加载自定义属性集合CircleView
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED); //解析CircleView属性集合circle_color, RED为默认值
Log.d("guanzihan","123");
a.recycle();//实现资源
init();
}
第三步:在xml布局文件中设置
<com.example.myview.CircleView
android:id="@+id/mCircle"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:background="#000000"
app:circle_color="@color/colorPrimary"
android:padding="100dp"/>
7.2.2 继承ViewGroup派生特殊的Layout
- 主要用于实现自定义的布局
- 需要合适地处理测量和布局,同时正确处理子元素的测量和布局过程
示例:HorizontalScrollViewEx
public class HorizontalScrollViewEx extends ViewGroup {
private String TAG = "HorizontalScrollViewEx";
private int mChildrenSize;
private int mChildrenWidth;
private int mChildrenIntex;
private int mLastX = 0;
private int mLastY = 0;
private int mLastInterceptX = 0;
private int mLastInterceptY = 0;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public HorizontalScrollViewEx(Context context) {
super(context);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth;
int measuredHeight;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
if(childCount == 0){
setMeasuredDimension(0, 0);
} else if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
} else if (heightSpecMode == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize, measuredHeight);
} else if(widthMeasureSpec == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth();
setMeasuredDimension(measuredWidth, heightSpaceSize);
}
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
for(int m = 0; m< mChildrenSize; m++){
final View childView = getChildAt(m);
if(childView.getVisibility() != View.GONE){
final int childWidth = childView.getMeasuredWidth();
mChildrenWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int)ev.getX();
int y = (int)ev.getY();
switch(ev.getAction()){
case MotionEvent.ACTION_DOWN: {
intercepted = false;
if(!mScroller.isFinished()){
mScroller.abortAnimation();
intercepted = true;
}
Log.d(TAG,"ACTION_DOWN");
break;
}
case MotionEvent.ACTION_MOVE:{
int deltaX = x - mLastInterceptX;
int deltaY = y - mLastInterceptY;
if(Math.abs(deltaX) > Math.abs(deltaY)){
intercepted = true;
} else {
intercepted = false;
}
Log.d(TAG,"ACTION_MOVE");
break;
}
case MotionEvent.ACTION_UP:{
intercepted = false;
Log.d(TAG,"ACTION_UP");
break;
}
default:
break;
}
Log.d(TAG,"intercepted = " + intercepted);
mLastX = x;
mLastY = y;
mLastInterceptX = x;
mLastInterceptY = y;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int)event.getX();
int y = (int)event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:{
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
Log.d(TAG,"ACTION_DOWN from onTOuch Event");
break;
}
case MotionEvent.ACTION_MOVE:{
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
Log.d(TAG,"move from on touch event");
break;
}
case MotionEvent.ACTION_UP:{
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if(Math.abs(xVelocity) >= 50){
mChildrenIntex = xVelocity >0? mChildrenIntex -1 : mChildrenIntex + 1;
} else{
mChildrenIntex = (scrollX + mChildrenWidth / 2) / mChildrenWidth;
}
mChildrenIntex = Math.max(0,Math.min(mChildrenIntex,mChildrenSize - 1));
int dx = mChildrenIntex * mChildrenWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
default:
break;
}
mLastY = y;
mLastX = x;
return true;
}
private void init(){
if(mScroller == null){
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
}
private void smoothScrollBy(int dx, int dy){
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}
}
- 根据需求复写onMeasure()从而实现你的子View测量逻辑