坐标系
- Android中有两种坐标系,Android坐标系和视图坐标系
Android坐标系
- 定义:屏幕左上角顶点为Android坐标系原点,向右为X轴正方向,向下为Y轴正方向;
- MotionEvent提供的getRawX()和getRawY()获取的坐标都是Android坐标系的坐标;
视图坐标系
- View获取自身宽高:getWidth(),getHeight();
- View自身坐标(View到其父控件原点的距离):getTop(),getLeft(),getRight(),getBottom()
- MotionEvent获取焦点坐标:
getX():触摸点到控件左边的距离,即视图坐标
getY():触摸点到控件顶边的距离,即视图坐标
getRawX():触摸点到屏幕左边的距离,即绝对坐标
getRawY():触摸点到屏幕顶边的的距离,即绝对坐标
View的滑动
- 基本原理:触摸事件传到View时,系统记下触摸点坐标,手指移动时系统记下移动后的坐标并算出偏移量,以此修改View的坐标。
实现View滑动的方法
1. layout()
- view绘制时会调用onLayout()来设置显示的位置,因此可以通过修改View的left、top、right、bottom属性来控制View的坐标。
private int lastX;
private int lastY;
@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 offsetX = x - lastX;
int offsetY = y - lastY;
//调用layout方法来重新放置它的位置
layout(getLeft() + offsetX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);
break;
}
return true;
}
2. offsetLeftAndRight()与offsetTopAndBottom()
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
3. 改变布局参数LayoutParams
- LayoutParams保存了View的布局参数
- LinearLayout和RelativeLayout的LayoutParams都继承自ViewGroup.MarginLayoutParams
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
ViewGroup.MarginLayoutParams layoutParams= (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
4. scollTo与scollBy
- 移动的是View的内容,如果在ViewGroup中使用则是移动他所有的子View;
- scollBy(dx,dy)表示移动的增量为dx、dy;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View)getParent()).scrollBy(-offsetX,-offsetY);
break;
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
- scollTo(x,y)表示移动到一个具体的坐标点;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollTo(-offsetX + ((View) getParent()).getScrollX(),
-offsetY + ((View) getParent()).getScrollY());
break;
5. Scroller
- scollTo/scollBy过程是瞬发的,用户体验不好,可以使用Scroller来实现有过度效果的滑动
- Scroller本身不能实现View的滑动,需要配合View的computeScroll()
public class MyView extends View {
private Scroller mScroller;
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
private int lastX;
private int lastY;
@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 offsetX = x - lastX;
int offsetY = y - lastY;
mScroller.startScroll(((View) getParent()).getScrollX(),
((View) getParent()).getScrollY(), -offsetX, -offsetY, 2000);
invalidate();
break;
}
return true;
}
}
6. 动画
private int firstX;
private int firstY;
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取到手指处的横坐标和纵坐标
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (firstX == 0 && firstY == 0) {
firstX = x;
firstY = y;
}
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - firstX;
int offsetY = y - firstY;
ObjectAnimator.ofFloat(this, "translationX", offsetX).setDuration(0).start();
ObjectAnimator.ofFloat(this, "translationY", offsetY).setDuration(0).start();
//translationX和translationY:作为增量控制View对象从他的布局容器的左上角开始位置。
//rotation、rotationX、rotationY:这三个属性控制View对象围绕它的支点进行2D和3D旋转
//PrivotX和PrivotY:控制View对象的支点位置,围绕这个支点进行旋转和缩放变换处理。默认该支点位置就是View对象的中心点。
//alpha:透明度,默认是1(不透明),0代表完全透明
//x和y:描述View对象在它容器中的最终位置,它是最初的做上角坐标和translationX,translationY值的累计的和
break;
default:
break;
}
return true;
}
- 如果一个属性没有get,set方法,也可以通过自定义一个属性类或则包装类来间接地给这个属性增加get和set方法。
private static class MyView{
private View mTarget;
private MyView(View mTarget){
this.mTarget=mTarget;
}
public int getWidth(){
return mTarget.getLayoutParams().width;
}
public void setWidth(int width){
mTarget.getLayoutParams().width=width;
mTarget.requestLayout();
}
}
MyView mMyView=new MyView(mButton);
ObjectAnimator.ofInt(mMyView,"width",500).setDuration(500).start();
- ValueAnimator:不提供任何动画效果,它更像一个数值发生器,用来产生一定规律数字,通常在ValueAnimator的AnimatorUpdateListener中监听数值的变化,从而完成动画的变换
ValueAnimator mValueAnimator=ValueAnimator.ofFloat(0,100);
mValueAnimator.setTarget(view);
mValueAnimator.setDuration(1000).start();
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Float mFloat=(Float)animation.getAnimatedValue();
}
});
}
View的事件分发机制
- 点击屏幕,就产生了触摸事件,这个事件被封装成了一个类:MotionEvent。而当这个MotionEvent产生后,那么系统就会将这个MotionEvent传递给View的层级,MotionEvent在View的层级传递的过程就是点击事件分发
ViewGroup.dispatchTouchEvent
- 总结:dispatchTouchEvent负责处理事件的分发,会先检查是否遮挡,然后重置之前触摸事件的遗留数据,然后判断是否需要拦截,需要就调用onInterceptTouchEvent,然后判断是否取消,如果不取消不拦截,检查子view有没有获得焦点的,然后遍历子view并把事件优先给获得焦点的view处理,会根据child是否为空判断是调用自己的view.dispatchTouchEvent还是child的dispatchTouchEvent
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
//检查是否分发本次事件:检查是否设置了被遮挡时不处理触摸事件的flag && 该事件的窗口是否被其它窗口遮挡
if (onFilterTouchEventForSecurity(ev)) {
//获取事件类型
final int action = ev.getAction();
//actionMasked能够区分出多点触控事件
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
//清理和重置之前触摸事件的各种标志和TouchTarget触摸目标链表
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 检查是否拦截这个TouchEvent
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//检查是否不允许拦截事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
//如果允许拦截就调用onInterceptTouchEvent
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // 防止Event中途被篡改
} else {
//有FLAG_DISALLOW_INTERCEPT标志就不进行拦截
//如果子View在ACTION_DWON时处理了事件,可以通过requestDisallowInterceptTouchEvent(true)来禁止父View拦截后续事件
//可以用来解决滑动冲突问题
intercepted = false;
}
} else {
//如果不是ACTION_DOWN事件,或者没有TouchTarget,ViewGroup就直接拦截了
intercepted = true;
}
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 标识是否需要取消
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
//检查父View是否支持多点触控,即将多个TouchEvent分发给子View
//可通过setMotionEventSplittingEnabled()可以修改这个值
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
&& !isMouseEvent;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//判断是否要给子View分发事件:没有拦截和取消
if (!canceled && !intercepted) {
//检查TouchEvent是否可以触发View获取焦点,
// 如果可以,查找本View中有没有获得焦点的子View,有就获取它,没有就为null
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//判断是否ACTION_DOWN,或者支持多点触控且ACTION_POINTER_DOWN,或者悬停啥的(鼠标)
//说明一个事件流只有一开始的DOWN事件才会去遍历分发事件,后面的事件将不再通过遍历分发,而是直接发到触摸目标队列的View中去
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//获取当前触摸手指在多点触控中的排序
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// 清理之前触摸事件中的目标
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;//子View数量
//第一个点的Down事件newTouchTarget肯定为null
if (newTouchTarget == null && childrenCount != 0) {
final float x = isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y = isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
// 将所有子View放到集合中,按照添加顺序排序,但是受到Z轴影响
//如果ViewGroup的子View数量不多于一个,为null
//如果ViewGroup的所有子View的z轴都为0,为null
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
//检查ViewGroup中的子视图是否是按照顺序绘制,其实就是不受z轴影响
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;//按照View添加顺序从前往后排的
//从后往前遍历子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
// 如果preorderedList不为空,从preorderedList中取View
// 如果preorderedList为空,从mChildren中取View
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 如果当前已经有View获得焦点了,后面的触摸事件会优先传给它
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//检查View是否显示或者播放动画以及TouchEvent点是否在View内
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);//再次重置View
//将事件传给子View,看子View有没有消费,消费了执行if中逻辑,并结束循环
//其中会根据child是否为空判断是调用自己的dispatchTouchEvent还是child的dispatchTouchEvent
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
//子View处理了本事件,那么接着会创建一个TouchTarget,并且关联该子View,
//后续的触摸事件就会通过这个TouchTarget取出子View,直接把事件分发给它
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;//标记已经有子View消费了事件
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
// 处理多点触控
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
//如果Down事件没有子View处理,mFirstTouchTarget会为null,
//那么把事件分发给ViewGroup自己的dispatchTransformedTouchEvent()处理
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// 还原状态
if (canceled || actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
- dispatchTransformedTouchEvent方法代码如下
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
// 如果没有子view,调用自己的dispatchTouchEvent
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
//根据滚动值计算触摸事件的偏移位置
event.offsetLocation(offsetX, offsetY);
//让子View处理事件
handled = child.dispatchTouchEvent(event);
//恢复TouchEvent坐标到原来位置,避免影响后面的流程
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
View.dispatchTouchEvent
- dispatchTransformedTouchEvent中的super.dispatchTouchEvent,因为ViewGroup继承自View,所以调用的是View的dispatchTouchEvent,OnTouchListener的优先级要比onTouchEvent()要高,如果我们设置了OnTouchListener并且onTouch()方法返回true,则onTouchEvent()方法不会被调用,否则则会调用onTouchEvent()方法,
public boolean dispatchTouchEvent(MotionEvent event) {
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//OnTouchListener不为null并且onTouch()方法返回true,
//表示事件被消费,也不会再执行onTouchEvent(event)
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//OnTouchListener为空则调用onTouchEvent消费事件
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
ViewGroup.onInterceptTouchEvent
- 用来进行事件的拦截,在ViewGroup.dispatchTouchEvent()中调用,仅ViewGroup中有此方法,默认返回false,不进行拦截
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
View.onTouchEvent(MotionEvent ev):
- 用来处理点击事件,在View.dispatchTouchEvent()方法中进行调用,上面view的移动就是在这个方法中实现的,onTouchEvent默认返回true,除非它是不可点击的也就是CLICKABLE和LONG_CLICKABLE都为false,如果设置了OnClickListener会执行它的onClick()方法
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
...
//如果可点击clickable
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
//performClickInternal中调用performClick,
//performClick中会调用mOnClickListener.onClick
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
...
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
..
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
从View体系看Activity的构成
- Activity的onCreate中会调用setContentView加载布局文件
继承Activity
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
- 调用了getWindow().setContentView,其中getWindow返回的mWindow是在activity的attach方法中初始化的,实际类型是PhoneWindow,其setContentView方法如下
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
- 其中调用了installDecor,其中调用generateDecor初始化了mDecor,这个DecorView就是Activity中的根View,继承了FrameLayout
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(-1);//return new DecorView...
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
- 其中generateDecor调用new DecorView创建了DecorView,generateLayout最终调用mDecor.onResourcesLoaded(mLayoutInflater, layoutResource),DecorView的onResourcesLoaded中通过调用LayoutInflater.inflate()解析布局赋值给mContentRoot,并在DecorView的onDraw中绘制
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
mBackgroundFallback.draw(this, mContentRoot, c, mWindow.mContentParent,
mStatusColorViewState.view, mNavigationColorViewState.view);
}
继承AppCompatActivity
@Override
public void setContentView(@LayoutRes int layoutResID) {
initViewTreeOwners();
getDelegate().setContentView(layoutResID);
}
- 其中getDelegate()利用设计模式中的代理模式,实现类是AppCompatDelegateImpl,其setContentView方法如下
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//调用LayoutInflater.inflate() 布局加载解析方法将Activity中的布局添加到父布局中
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
- 第一行调用了ensureSubDecor,代码如下
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
//创建SubDecor之后获取title并设置给对应的view
CharSequence title = getTitle();
if (!TextUtils.isEmpty(title)) {
if (mDecorContentParent != null) {
mDecorContentParent.setWindowTitle(title);
} else if (peekSupportActionBar() != null) {
peekSupportActionBar().setWindowTitle(title);
} else if (mTitleView != null) {
mTitleView.setText(title);
}
}
...
}
}
private ViewGroup createSubDecor() {
...
ensureWindow();
mWindow.getDecorView();
final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;
...
mWindow.setContentView(subDecor);
..
return subDecor;
}
//ensureWindow调用Activity.getWindow给mWindow赋值
private void ensureWindow() {
if (mWindow == null && mHost instanceof Activity) {
attachToWindow(((Activity) mHost).getWindow());
}
if (mWindow == null) {
throw new IllegalStateException("We have not been given a Window");
}
}
- 其中ensureWindow中调用了activity.getWindow给mWindow赋值,而mWindow.getDecorView的实现在PhoneWindow中,最终是调用了installDecor
@Override
public final @NonNull View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
- 综上:一个Activity包含一个window对象,这个对象是由PhoneWindow来实现的,PhoneWindow将DecorView做为整个应用窗口的根View,而这个DecorView又将屏幕划分为两个区域一个是TitleView一个是ContentView,而我们平常做应用所写的布局正是展示在ContentView中的。
View的绘制
- View 绘制主要分为measure(测量),layout(布局), draw(绘制)三个阶段;
measure
ViewGroup.measureChildren
- ViewGroup的measureChildren会遍历子View调用measureChild方法;
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
//遍历子View调用measureChild方法;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
- measureChild方法会获取子View的LayoutParams,并调用getChildMeasureSpec获取子元素的MeasureSpec,最后调用子View的measure()方法进行测量
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//获取子View的LayoutParams
final LayoutParams lp = child.getLayoutParams();
//获取子元素的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//调用子View的measure()方法进行测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
- getChildMeasureSpec会根据父View的MeasureSpec,结合子View的LayoutParams属性,最后得到子View的MeasureSpec属性
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
//根据父View的specMode判断
switch (specMode) {
case MeasureSpec.EXACTLY://精确模式,尺寸的值是多少组件的长或宽就是多少
//根据子View的size判断
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST://最大模式,由父组件能够给出的最大的空间决定
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED://未指定模式,当前组件可以随便使用空间,不受限制
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//将size和mode拼接成一个int值
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
LinearLayout.onMeasure
- ViewGroup并没有提供onMeasure()方法,而是让其子类来各自实现测量的方法,究其原因就是ViewGroup有不同的布局的需要很难统一,我们可与来看一下其子类LinearLayout的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
- LinearLayout的onMeasure区分了水平和垂直两个分支,我们挑其中的measureVertical看一下
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;//用来存储LinearLayout在垂直方向的高度
...
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
...
//遍历子View,根据子View的MeasureSpec模式分别计算每个子View的高度
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//+分隔线高度
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
//useExcessSpace表示如果高度是0并且设置了权重
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
lp.height = LayoutParams.WRAP_CONTENT;
}
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
//布局之前的测量,measureChildBeforeLayout 里面调用了 measureChildWithMargins方法,和上面的measureChild类似,
//只是getChildMeasureSpec时增加了Margin
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
//获取子view的高度
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
//叠加到mTotalLength
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
...
}
...
//测量最大宽度maxWidth
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
...
}
...
//mTotalLength+父view的上下padding
mTotalLength += mPaddingTop + mPaddingBottom;
...
maxWidth += mPaddingLeft + mPaddingRight;
...
}
- 其中调用了measureChildBeforeLayout测量子View,measureChildBeforeLayout中又调用了measureChildWithMargins方法,和上面的measureChild类似,只是getChildMeasureSpec时增加了Margin的技术
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
View.measure
- viewGroup测量子view最终都会调用到child.measure,那么来看一下View.measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if (forceLayout || needsLayout) {
...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
//如果没有缓存就调用onMeasure进行测量
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
} else {
//有缓存就从缓存中取
long value = mMeasureCache.valueAt(cacheIndex);
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
...
}
...
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
//缓存到数组中
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
View.onMeasure
- 上面的View.measure最终是调用了onMeasure方法进行测量的,其中又调用了setMeasuredDimension方法来设置View的宽高;
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
- 上面onMeasure中调用了getDefaultSize方法,根据measureSpec的不同返回size
public static int getDefaultSize(int size, int measureSpec) {
//从onMeasure中可看到这个size是getSuggestedMinimumWidth,getSuggestedMinimumHeight
int result = size;
//specMode是View的测量模式
int specMode = MeasureSpec.getMode(measureSpec);
//specSize是View的测量大小
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
//如果View没有设置背景则取值为mMinWidth,
//对应android:minWidth设置的值或setMinimumWidth的值
//如果View设置了背景在取值为mMinWidth, mBackground.getMinimumWidth()的最大值,
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
//mBackground是Drawable类型的, intrinsicWidth得到的是这个Drawable的固有的宽度,
//如果固有宽度大于0则返回固有宽度,否则返回0。
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
layout
View.layout
//入参是View四个点的坐标(相对于父View的)
public void layout(int l, int t, int r, int b) {
...
//setFrame()设置View的四个顶点的值,也就是mLeft 、mTop、mRight和 mBottom的值
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//调用了onLayout
onLayout(changed, l, t, r, b);
...
}
...
}
View.onLayout
- layout中调用了onLayout方法,其中并没有具体的实现,因为确定位置时根据不同的控件有不同的实现,所以在View和ViewGroup中均没有实现onLayout()方法。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
LinearLayout.onLayout
- View.onLayout中没有具体实现,那么来看一下其子类LinearLayout的实现吧,也是区分了水平和垂直两个分支方法
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
void layoutVertical(int left, int top, int right, int bottom) {{
...
//遍历子View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
...
//setChildFrame()方法中调用子元素的layout()方法来确定自己的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
//childTop是叠加的,因为垂直方向上子元素是一个接一个排列的而不是重叠的
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
//调用子元素的layout()方法来确定自己的位置
child.layout(left, top, left + width, top + height);
}
draw流程
View.draw
- view的draw方法代码如下,其中官方注释写的也很清除,总共分了7步
public void draw(Canvas canvas) {
...
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
* 7. If necessary, draw the default focus highlight
*/
// Step 1, draw the background, if needed
//如果设置了背景,则绘制背景
drawBackground(canvas);
...
// Step 2, save the canvas' layers
//保存canvas层
...
saveCount = canvas.getSaveCount();
int topSaveCount = -1;
int bottomSaveCount = -1;
int leftSaveCount = -1;
int rightSaveCount = -1;
int solidColor = getSolidColor();
if (solidColor == 0) {
if (drawTop) {
topSaveCount = canvas.saveUnclippedLayer(left, top, right, top + length);
}
if (drawBottom) {
bottomSaveCount = canvas.saveUnclippedLayer(left, bottom - length, right, bottom);
}
if (drawLeft) {
leftSaveCount = canvas.saveUnclippedLayer(left, top, left + length, bottom);
}
if (drawRight) {
rightSaveCount = canvas.saveUnclippedLayer(right - length, top, right, bottom);
}
} else {
scrollabilityCache.setFadeColor(solidColor);
}
// Step 3, draw the content
//调用onDraw绘制自身内容
onDraw(canvas);
// Step 4, draw the children
//调用dispatchDraw绘制子View
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
//绘制附加效果
final Paint p = scrollabilityCache.paint;
final Matrix matrix = scrollabilityCache.matrix;
final Shader fade = scrollabilityCache.shader;
...
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
//调用onDrawForeground绘制装饰品
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
//绘制默认的焦点高亮显示
drawDefaultFocusHighlight(canvas);
if (isShowingLayoutBounds()) {
debugDrawFocus(canvas);
}
}
View.onDraw
- draw中调用了onDraw,也是没有具体实现,需要子view自己去实现
protected void onDraw(Canvas canvas) {
}
LinearLayout.onDraw
- 同样的我们看一下LinearLayout.onDraw方法
@Override
protected void onDraw(Canvas canvas) {
if (mDivider == null) {
return;
}
if (mOrientation == VERTICAL) {
drawDividersVertical(canvas);
} else {
drawDividersHorizontal(canvas);
}
}
- 我们还是选一种的垂直方法drawDividersVertical看一下
void drawDividersVertical(Canvas canvas) {
final int count = getVirtualChildCount();
//遍历子View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child != null && child.getVisibility() != GONE) {
//判断如果子view前面有分割线
if (hasDividerBeforeChildAt(i)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int top = child.getTop() - lp.topMargin - mDividerHeight;
//画水平的分割线
drawHorizontalDivider(canvas, top);
}
}
}
//检查最后位置的分割线
if (hasDividerBeforeChildAt(count)) {
final View child = getLastNonGoneChild();
int bottom = 0;
if (child == null) {
bottom = getHeight() - getPaddingBottom() - mDividerHeight;
} else {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
bottom = child.getBottom() + lp.bottomMargin;
}
drawHorizontalDivider(canvas, bottom);
}
}
- 上面调用了drawHorizontalDivider画分割线,其实现还是比较简单的,只有两行代码
void drawHorizontalDivider(Canvas canvas, int top) {
mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
mDivider.draw(canvas);
}
- 其中的mDivider是Drawable 类型的,可在通过如下方法设置分割线
/*
设置显示分割线的模式
public static final int SHOW_DIVIDER_NONE = 0; 不显示分割线
public static final int SHOW_DIVIDER_BEGINNING = 1; 在开始处显示分割线
public static final int SHOW_DIVIDER_MIDDLE = 2; 在子视图之间显示分割线
public static final int SHOW_DIVIDER_END = 4; 在结束尾部显示分割线
*/
linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
//设置分割线Drawable
linearLayout.setDividerDrawable(ResourcesCompat.getDrawable(getResources(),R.drawable.line,null));
自定义View
- 自定义View一般可以继承View,ViewGroup,已有的系统控件或其他自定义控件,根据需要重写onMeasure,onLayout,onDraw,onTouchEvent等方法;
优点
- 少了解析xml的过程
- 自定义View 减少了ViewGroup与View之间的测量,包括父量子,子量自身,子在父中位置摆放,当子view变化时,父的某些属性都会跟着变化;需要注意的是自定义View的onDraw()方法会被频繁调用,此方法中尽量避免创建对象;
几种自定义View的实现方式
自定义组合控件
- 用已有的控件在xml中组合起来重新定义成一个新的控件,例如一个titleBar会在很多页面用到,那么就可以抽出一个组合控件进行封装,复用起来会方便很多
继承系统控件
- 在系统控件的基础上进行拓展,添加新的功能或修改显示效果,一般在onDraw方法中处理;
继承View
- 不只是要实现onDraw()方法,而且在实现过程中还要考虑到wrap_content属性以及padding属性的设置;为了方便配置自己的自定义View还会对外提供自定义的属性,另外如果要改变触控的逻辑,还要重写onTouchEvent()等触控事件的方法。
对padding属性进行处理
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft=getPaddingLeft();
int paddingRight=getPaddingRight();
int paddingTop=getPaddingTop();
int paddingBottom=getPaddingBottom();
int width = getWidth()-paddingLeft-paddingRight;
int height = getHeight()-paddingTop-paddingBottom;
canvas.drawRect(0+paddingLeft, 0+paddingTop, width+paddingRight, height+paddingBottom, mPaint);
}
对wrap_content属性进行处理
//在onMeasure()方法中指定一个默认的宽和高,在设置wrap_content属性时设置此默认的宽和高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize=MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize=MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode==MeasureSpec.AT_MOST && heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(400,400);//参数的单位是px
}else if(widthSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(400,heightSpecSize);
}else if(heightSpecMode==MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize,400);
}
}
自定义属性
// 1. 在values目录下创建 attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyView">
<attr name="text_color" format="color" />
</declare-styleable>
</resources>
// 2. 在自定义View构造函数中解析自定义属性的值:
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray mTypedArray=context.obtainStyledAttributes(attrs,R.styleable.MyView);
//提取属性集合的text_color属性,如果没设置默认值为Color.RED
mColor=mTypedArray.getColor(R.styleable.MyView_text_color,Color.RED);
//获取资源后要及时回收
mTypedArray.recycle();
initDraw();
}
// 3. 在布局文件中配置自定义属性
<com.jinyang.jetpackdemo.activity.ui.MyView
android:layout_width="50dp"
android:background="#f00"
app:text_color="@color/purple_200"
android:layout_height="50dp"/>
继承ViewGroup
- 相关的知识点在上面基本已经说过了,下面直接给一个完整的例子,具体的都有注释解释
class HorizontalView : ViewGroup {
private var lastX = 0
private var lastY = 0
/**
* 当前子元素
*/
private var currentIndex = 0
private var childWidth = 0
private var scroller: Scroller? = null
/**
* 增加速度检测,如果速度比较快的话,就算没有滑动超过一半的屏幕也可以
*/
private var tracker: VelocityTracker? = null
private var lastInterceptX = 0
private var lastInterceptY = 0
constructor(context: Context?) : super(context) {
init()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init()
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init()
}
fun init() {
scroller = Scroller(context)
tracker = VelocityTracker.obtain()
}
/**
* 重写onMeasure处理wrap_content属性的尺寸测量
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
//测量所有子元素(没有考虑它的padding和子元素的margin)
measureChildren(widthMeasureSpec, heightMeasureSpec)
if (childCount == 0) {
//如果没有子元素,就设置宽高都为0(简化处理,正常的话应该根据LayoutParams中的宽和高来做相应的处理)
setMeasuredDimension(0, 0)
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//宽和高都是AT_MOST,则设置宽度所有子元素的宽度的和;高度设置为第一个元素的高度;
val childOne = getChildAt(0)
val childWidth = childOne.measuredWidth
val childHeight = childOne.measuredHeight
setMeasuredDimension(childWidth * childCount, childHeight)
} else if (widthMode == MeasureSpec.AT_MOST) {
//如果宽度是wrap_content,则宽度为所有子元素的宽度的和
val childOne = getChildAt(0)
val childWidth = childOne.measuredWidth
setMeasuredDimension(childWidth * childCount, heightSize)
} else if (heightMode == MeasureSpec.AT_MOST) {
//如果高度是wrap_content,则高度为第一个子元素的高度
val childHeight = getChildAt(0).measuredHeight
setMeasuredDimension(widthSize, childHeight)
}
}
/**
* 重写onLayout来布局子元素
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val childCount = childCount
//左边的距离
var left = 0
var child: View
//遍历布局子元素
for (i in 0 until childCount) {
child = getChildAt(i)
if (child.visibility != GONE) {
//子元素不是GONE,则调用子元素的layout方法将其放置到合适的位置上
val width = child.measuredWidth
//赋值给子元素宽度变量
childWidth = width
//没有处理自身的padding以及子元素的margin,right是left+元素的宽度
child.layout(left, 0, left + width, child.measuredHeight)
//left是一直累加的
left += width
}
}
}
/**
* 重写onInterceptTouchEvent处理滑动冲突
*/
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
var intercept = false
val x = event.x.toInt()
val y = event.y.toInt()
when (event.action) {
MotionEvent.ACTION_DOWN -> {
intercept = false
//如果动画还没有执行完成,则打断
if (!scroller!!.isFinished) {
scroller!!.abortAnimation()
}
}
MotionEvent.ACTION_MOVE -> {
val deltaX = x - lastInterceptX
val deltaY = y - lastInterceptY
//水平方向距离长 MOVE中返回true一次,后续的MOVE和UP都不会收到此请求
if (Math.abs(deltaX) - Math.abs(deltaY) > 0) {
intercept = true //用户想水平滑动的,所以拦截
Log.i("wangshu", "intercept = true")
} else {
intercept = false
Log.i("wangshu", "intercept = false")
}
}
MotionEvent.ACTION_UP -> intercept = false
else -> {}
}
//因为DOWN返回false,所以onTouchEvent中无法获取DOWN事件,这里要负责设置lastX,lastY
lastX = x
lastY = y
lastInterceptX = x
lastInterceptY = y
return intercept
}
/**
* 重写onTouchEvent方法使用Scroller来弹性滑动到其他页面
*/
override fun onTouchEvent(event: MotionEvent): Boolean {
tracker!!.addMovement(event)
val x = event.x.toInt()
val y = event.y.toInt()
when (event.action) {
MotionEvent.ACTION_DOWN ->//ACTION_DOWN处理再次触摸屏幕阻止页面继续滑动
if (!scroller!!.isFinished) {
scroller!!.abortAnimation()
}
MotionEvent.ACTION_MOVE -> {
//跟随手指滑动
val deltaX = x - lastX
scrollBy(-deltaX, 0)
}
MotionEvent.ACTION_UP -> {//ACTION_UP处理快速滑动到其他页面
//相对于当前View滑动的距离,正为向左,负为向右
val distance = scrollX - currentIndex * childWidth
//必须滑动的距离要大于1/2个宽度,否则不会切换到其他页面
if (Math.abs(distance) > childWidth / 2) {
if (distance > 0) {
currentIndex++
} else {
currentIndex--
}
} else {
//调用该方法计算1000ms内滑动的平均速度
tracker!!.computeCurrentVelocity(1000)
val xV = tracker!!.xVelocity//获取到水平方向上的速度
//如果速度的绝对值大于50的话,就认为是快速滑动,就执行切换页面
if (Math.abs(xV) > 50) {
if (xV > 0) {
//大于0切换上一个页面
currentIndex--
} else {
//小于0切换到下一个页面
currentIndex++
}
}
}
currentIndex = if (currentIndex < 0) 0 else Math.min(currentIndex, childCount - 1)
smoothScrollTo(currentIndex * childWidth, 0)
//重置速度计算器
tracker!!.clear()
}
else -> {}
}
lastX = x
lastY = y
return true
}
override fun computeScroll() {
super.computeScroll()
if (scroller!!.computeScrollOffset()) {
scrollTo(scroller!!.currX, scroller!!.currY)
postInvalidate()
}
}
/**
* 弹性滑动到指定位置
*/
private fun smoothScrollTo(destX: Int, destY: Int) {
scroller!!.startScroll(
scrollX, scrollY, destX - scrollX,
destY - scrollY, 1000
)
invalidate()
}
}
参考
我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章