Android-高级-UI-进阶之路-(三)-理解-View-工作原理并带你入自定义-View-门

return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
复制代码

这一段代码虽然比较多,但是还是比较容易理解,它主要的工作就是先拿到在父容器中可用的尺寸,然后根据父元素的测量规格和子元素中 LayoutParams 参数来决定当前子 View 的 MeasureSpec。

上面的代码,如果用一张表格来表示的话,应该更好理解,请看下表:

parentSpecMode / childLaoutParamsEXACTLY(精准模式)AT_MOST(最大模式)UNSPECIFIED(精准模式)
dp/pxEXACTLY childSizeEXACTLY childSizeEXACTLY childSize
match_parentEXACTLY parentSizeAT_MOST parentSizeUNSPECIFIED 0
Warap_contentAT_MOST parentSizeAT_MOST parentSizeUNSPECIFIED 0

通过此表可以更加清晰的看出,只要提供父容器的 MeasureSpec 和子元素的 LayoutParams, 就可以快速的确定出子元素的 MeasureSpec 了,有了 MeasureSpec 就可以进一步确定出子元素测量后的大小了。

View 工作流程

View 的工作流程主要是指 measure、layout 、draw 这三个流程,即 测量 -> 布局 -> 绘制,其中 measure 确定 View 测量宽高,layout 确定 View 的最终宽高和四个顶点的位置,而 draw 则将 View 绘制到屏幕上。

在讲解 View 的绘制流程之前,我们有必要知道 View 的 measure 何时触发,其实如果对 Activity 生命周期源码有所了解的应该知道,在 onCreate 生命周期中,我们做了 setContentView 把 XML 中的节点转为 View 树的过程,然后在 onResume 可以交互的状态,开始触发绘制工作,可以说 Activity 的 onResume 是开始绘制 View 的入口也不为过,下面看入口代码:

//ActivityThread.java
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {

/**

  • 1. 最终会调用 Activity onResume 生命周期函数
    /
    r = performResumeActivity(token, clearHide, reason);

    if (a.mVisibleFromClient) {
    if (!a.mWindowAdded) {
    a.mWindowAdded = true;
    /
    *
  • 2. 调用 ViewManager 的 addView 方法
    */
    wm.addView(decor, l);
    } else {

} else {

}
}
复制代码

通过上面代码我们知道,首先会调用注释 1 performResumeActivity 方法,其内部会执行 Activity onResume 生命周期方法, 然后会执行将 Activity 所有 View 的父类 DecorView 添加到 Window 的过程,我们看注释 2 代码它调用的是 ViewManager#addView 方法,在讲解 WindowManager 源码的时候,我们知道了 WindowManager 继承了 ViewManager 然后它们的实现类就是 WindowManagerImpl 所以我们直接看它内部 addView 实现:

//WindowManagerImpl.java
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
/**

  • 委托给 WindowManagerGlobal 来处理 addView
    */
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }
    复制代码

内部处理又交给了 WindowManagerGlobal 对象,我们继续跟踪,代码如下:

//WindowManagerGlobal.java
/**
*

  • @param view DecorView
  • @param params
  • @param display
  • @param parentWindow
    */
    public void addView(View view, ViewGroup.LayoutParams params,
    Display display, Window parentWindow) {
    if (view == null) {
    throw new IllegalArgumentException(“view must not be null”);
    }
    if (display == null) {
    throw new IllegalArgumentException(“display must not be null”);
    }
    if (!(params instanceof WindowManager.LayoutParams)) {
    throw new IllegalArgumentException(“Params must be WindowManager.LayoutParams”);
    }

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
/**

  • 1. 根据 WindowManager.LayoutParams 的参数来对添加的子窗口进行相应的调整
    */
    parentWindow.adjustLayoutParamsForSubWindow(wparams);
    } else {

    }

ViewRootImpl root;
View panelParentView = null;

synchronized (mLock) {

/**

  • 2. 实例化 ViewRootImpl 对象,并赋值给 root 变量
    */
    root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

/**

  • 3. 添加 view 到 mViews 列表中
    /
    mViews.add(view);
    /
    *
  • 4. 将 root 存储在 ViewRootImp 列表中
    /
    mRoots.add(root);
    /
    *
  • 5. 将窗口的参数保存到布局参数列表中。
    */
    mParams.add(wparams);

// do this last because it fires off messages to start doing things
try {
/**

  • 6. 将窗口和窗口的参数通过 setView 方法设置到 ViewRootImpl 中
    */
    root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
    // BadTokenException or InvalidDisplayException, clean up.
    if (index >= 0) {
    removeViewLocked(index, true);
    }
    throw e;
    }
    }
    }
    复制代码

我们直接注释 6 ViewRootImpl#setView 方法,代码如下:

//ViewRootImpl.java
/**

  • We have one child
    */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {

//1. 请求刷新布局
requestLayout();

}
复制代码

该类 setView 代码比较多,我们直接直接找我们需要的核心代码注释 1 ,我们看它内部实现,代码如下:

//ViewRootImpl.java
@Override
public void requestLayout() {
//如果 onMeasure 和 onLayout 工作还没完成,那么就不允许调用 执行
if (!mHandlingLayoutInLayoutRequest) {
//检查线程,是否是主线程
checkThread();
mLayoutRequested = true;
//开始遍历
scheduleTraversals();
}
}
复制代码

上面代码首先是检查是否可以开始进入绘制流程,我们看 checkThread 方法实现,代码如下:

//ViewRootImpl.java
/**

  • 检查当前线程,如果是子线程就抛出异常。
    */
    void checkThread() {
    if (mThread != Thread.currentThread()) {
    throw new CalledFromWrongThreadException(
    “Only the original thread that created a view hierarchy can touch its views.”);
    }
    }
    复制代码

是不是在程序中这个异常经常遇见?现在知道它是从哪里抛出来的了吧,下面我们接着看 scheduleTraversals 方法的实现,代码如下:

//ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
/**

  • mChoreographer:用于接收显示系统的 VSync 信号,在下一帧渲染时控制执行一些操作,
  • 用于发起添加回调 在 mTraversalRunnable 的 run 中具体实现
    */
    mChoreographer.postCallback(
    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    if (!mUnbufferedInputDispatch) {
    scheduleConsumeBatchedInput();
    }
    notifyRendererOfFramePending();
    pokeDrawLockIfNeeded();
    }
    }
    复制代码

这里代码的意思就是如果收到系统 VSYNC 信号,那么就会在 mTraversalRunnable run 方法执行,代码如下:

//ViewRootImpl.java
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//1. 删除当前接收 SYNCBarrier 信号的回调
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {
Debug.startMethodTracing(“ViewAncestor”);
}
//2. 绘制入口
performTraversals();

if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
复制代码

在文章的开始部分我们知道 performTraversals 就是测量 layout draw 入口,那么我们继续看它的实现,代码如下:

//ViewRootImpl.java
private void performTraversals() {

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

//1. 执行测量流程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

//2. 执行布局流程
performLayout(lp, desiredWindowWidth, desiredWindowHeight);

//3. 执行绘制流程
performDraw();
}

//说明 1.
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, “measure”);
try {
//调用 View 的 measure 方法
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}

//说明 2.
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {

final View host = mView;//代表 DecorView

//内部在调用 View onLayout
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

}

//说明 3.
private void performDraw() {

//内部通过调用 GPU/CPU 来绘制
draw(fullRedrawNeeded);

}
复制代码

到这里上面对应的函数会对应调用 View 的 onMeasure -> onLayout -> ondraw 方法,下面我们就具体来说明下绘制过程。

measure 过程

measure 过程要分情况来看,如果一个原始的 View ,那么通过 measure 方法就完成了其测量过程,如果是一个 ViewGroup ,除了完成自己的测量过程外,还会遍历它所有的子 View 的 measure 方法,各个子元素在递归去执行这个流程(有子 View 的情况),下面针对这两种情况分别讨论。

View 的 measure 过程

//View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);

onMeasure(widthMeasureSpec, heightMeasureSpec);


}
复制代码

View 的 measure 过程由其 measure 方法来完成,通过 View#measure 源码可以知道 它是被 final 修饰的,那么就代表了子类不能重写,通过上面源码我们知道在 View#measure 内部又会去调用 onMeasure 方法,我们接着看它的源码实现,代码如下:

//View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
复制代码

上面的代码就做了一件事儿,就是设置测量之后的宽高值,我们先来看看 getDefaultSize 方法,代码如下:

//View.java
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//根据 measureSpec 拿到当前 View 的 specMode
int specMode = MeasureSpec.getMode(measureSpec);
//根据 measureSpec 拿到当前 View 的 specSize
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED: //一般用于系统内部的测量过程
result = size;//直接返回传递进来的 size
break;
case MeasureSpec.AT_MOST:// wrap_content 模式,大小由子类决定但是不能超过父类
case MeasureSpec.EXACTLY://精准模式
result = specSize;//返回测量之后的大小
break;
}
return result;
}
复制代码

通过上面代码可以看出,getDefaultSize 内部逻辑不多,也比较简单,对于我们来说只需要关心 AT_MOST, EXACTLY 这两种情况就行,其最终就是返回测量之后的大小。这里要注意的是这里测量之后的大小并不是最终 View 的大小,最终大小是在 layout 阶段确定的,所以这里一定要注意。

我们来看一下 getSuggestedMinimumXXXX() 源码实现:

//View.java
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
复制代码

可以看到 getSuggestedMinimumXXX 内部的代码意思就是,如果 View 没有设置背景,那么返回 android:minWidth 这个属性所指定的值,这个值可以为 0,如果 View 设置了背景,则返回 android:minWidth 和背景的 最小宽度/最小高度 这两者的者中的最大值,它们返回的就是 UNSPECIFIED 情况下的宽高。

从 getDefaultSize 方法实现来看, View 的宽高由 specSize 决定,所以我们可以得到如下结论:既然 measure 被 final 修饰不能重写,可是我们在它内部也发现了新大陆 onMeasure 方法,我们可以直接继承 View 然后重写 onMeasure 方法并设置自身大小。

这里在重写 onMeasure 方法的时候设置自身宽高需要注意一下,如果在 View 在布局中使用 wrap_content ,那么它的 specMode 是 AT_MOST 模式,在这种模式下,它的宽高等于 specSize,也就是父类控件空剩余可以使用的空间大小,这种效果和在布局中使用 match_parent 完全一致,那么如何解决这个问题勒,可以参考下面代码:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)

val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)

/**

  • 说明在布局中使用了 wrap_content 模式
    */
    if (widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
    setMeasuredDimension(mWidth,mHeight)
    }else if (widthMeasureSpec == MeasureSpec.AT_MOST){
    setMeasuredDimension(mWidth,heightSize)
    }else if (heightMeasureSpec == MeasureSpec.AT_MOST){
    setMeasuredDimension(widthSize,mHeight)
    }else {
    setMeasuredDimension(widthSize,heightSize)
    }
    }
    复制代码

在上面代码中,我们只需要给 View 指定一个默认的内部宽高(mWidth、mHeight),并在 wrap_content 的时候设置此宽高即可。对于非 wrap_content 情形,我们就沿用系统的测量值即可。

ViewGroup 的 measure 过程

对于 ViewGroup 来说,初了完成自己的 measure 过程以外,还会去遍历调用所有的子 View 的 measure 方法,各个元素递归去执行这个过程,和 View 不同的是 ViewGroup 是一个抽象类,因此它没有重写 View 的 onMeasure 方法,但是它定义了一个 measureChild 方法,代码如下:

//ViewGroup.java
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
//拿到所有子 View
final View[] children = mChildren;
//遍历子 View
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
//依次对子 View 进行测量
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
复制代码

上面代码也很简单,ViewGroup 在 measure 时,会对每一个子元素进行 measure ,如下代码所示:

//ViewGroup.java
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
//拿到该子 View 在布局XML中或者代码中定义的属性
final LayoutParams lp = child.getLayoutParams();
//通过 getChildMeasureSpec 方法,根据父元素的宽高测量规则拿到子元素的测量规则
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//拿到子元素的测量规则之后传递到 View 中,开始 measure 流程
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码

measureChild 代码逻辑也很容易理解,首先取出设置的 LayoutParams 参数,然后通过 getChildMeasureSpec 方法,根据父元素的宽高测量规格拿到子元素的测量规格,最后将拿到的测量规格直接传递给 View#measure 来进行测量。

measure 小总结:

  1. 获取 View 最终宽高,需要在 onLayout 中获取,因为 measure 在某些极端的情况下需要测量多次。
  2. 在 Activity 中获取 View 的宽高需要使用 Activity/View#onWindowFocusChangedview.post(runnable)ViewTreeObserver 的 onGlobalLayoutListener 回调手动调用 view.measure(int width,int height) ,最终使用哪个以实际情况来定。

layout 过程

measure 完之后就是 layout 确定子 View 的位置,当 ViewGroup 位置确定以后,它在 onLayout 中会遍历所有的子元素并调用其 layout 方法,在 layout 方法中 onLayout 方法又会被调用,我们直接看 View 的 layout 方法,代码如下:

//View.java
@SuppressWarnings({“unchecked”})
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

/**

  • 1. 通过 setFrame 来初始化四个点的位置
    */
    boolean changed = isLayoutModeOptical(mParent) ?
    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
/**

  • 2. 确定 子View 位置
    */
    onLayout(changed, l, t, r, b);


}
}
复制代码

layout 方法大致流程首先会通过 setFrame 方法来设定 View 四个顶点位置,View 的四个顶点一旦确认了那么就会接着调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置。

draw 过程

measure 和 layout 过程确定了之后就该执行绘制的最后一个流程了 draw,它的作用就是将 View 绘制到屏幕上面,View 的绘制过程遵循以下几点:

  1. 绘制背景 backgroud.draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制 children (dispatchDraw)
  4. 绘制装饰 (onDrawScrollBars)

下面我们从源码的角度来看一下 draw 实现,代码如下:

//ViewRootImpl.java
private void performDraw() {

draw(fullRefrawNeeded);

}

private void draw(boolean fullRedrawNeeded) {

//使用硬件加速绘制,mView -> DecorView
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);

//使用 CPU 绘制
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
return;
}
}
复制代码

这里我们直接看 CPU 绘制

//ViewRootImpl.java
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {

mView.draw(canvas);

}

复制代码

上面代码内部会调用 View#draw 方法,我们直接看内部实现,代码如下:

//View.java
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

int saveCount;

if (!dirtyOpaque) {
/**

  • 1. 绘制背景
    */
    drawBackground(canvas);
    }

final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
/**

  • 2. 调用 ondraw 绘制 View 内容
    */
    if (!dirtyOpaque) onDraw(canvas);

/**
*3. 绘制 View 的子 View
*/
dispatchDraw(canvas);

drawAutofilledHighlight(canvas);

if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}

/**

  • 4. 绘制 View 的装饰
    */
    onDrawForeground(canvas);

drawDefaultFocusHighlight(canvas);

if (debugDraw()) {
debugDrawFocus(canvas);
}

// we’re done…
return;
}


}
复制代码

到目前为止,View 的绘制流程就介绍完了。根节点是 DecorView,整个 View 体系就是一棵以 DecorView 为根的View 树,依次通过遍历来完成 measure、layout 和 draw 过程。而如果要自定义 view ,一般都是通过重写onMeasure(),onLayout(),onDraw() 来完成要自定义的部分,整个绘制流程也基本上是围绕着这几个核心的地方来展开的。

自定义 View

下面我们将详细介绍自定义 View 。自定义 View 的作用不用多说,这个大家应该都比较清楚,如果你想做出比较绚丽华彩的 UI 那么仅仅依靠系统的控件是远远不够的,这个时候就必须通过自定义 View 来实现这个绚丽的效果。自定义 View 是一个综合性技术体系,它涉及 View 的层次结构、事件分发、和 View 工作原理等,这些技术每一项又都是初学者难以掌握的,所以前面 2 篇文章我们分别讲解了 View 基础,事件分发以及该篇文章的 View 工作原理等知识,有了这些知识之后再来学习自定义 View 那将面对复杂的 UI 效果也能一一应对了,下面我们就来认识自定义 View 在该小节末尾也会给出实际例子,以供大家参考。

自定义 View 分类

自定义 View 的分类标准不唯一,这里则把它分为四大类,请看下面:

  1. 继承 View 重写 onDraw 方法

这个方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式达到,往往需要静态或者动态地显示一些不规则的图形。很显然这需要通过绘制的方式实现,即重写 onDraw 方法,采用这种方式需要自己支持 wrap_content ,并且 padding 也需要自己处理。

  1. 继承 ViewGroup 派生特殊的 Layout

这种方式主要用于实现自定义的布局,即除了基本系统布局之外,我们重新定义一种新的布局,当某种效果看起来像几种 View 组合在一起的时候,可以采用这种方式来实现。采用这种方式稍微复杂一些,需要合适的处理 ViewGroup 的 measure 、布局这两个过程,并同时处理子元素的测量和布局过程。

  1. 继承特定的 View (比如 TextView)

这种方式比较常见,一般是用于扩展某种已有的 View 的功能,比如 TextView ,这种方法比较容易实现。也不需要自己支持 wrap_content 和 padding 等。

  1. 继承特定的 ViewGroup(比如 LinearLayout)

这种方式也比较常见,当某种效果看起来很像几种 View 组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理 ViewGroup 的测量和布局这 两个过程,需要注意的这种方法和方法 2 的区别,一般来说方法 2 能实现的效果方式 4 也能实现,两则的区别在于方法 2 更接近 View 的底层。

自定义 View 注意事项

该小节主要介绍 自定义 View 过程中的一些注意事项,这些问题如果处理不好有可能直接导致 View 的正常使用,具体事项如下:

  1. 让 View 支持 wrap_content

这是因为直接继承 View 或者 ViewGroup 的控件,如果不在 onMeasure 中对 wrap_content 做特殊处理,那么当外界在布局中使用 wrap_content 属性时就无法达到预期的效果,具体处理可以参考该篇文章的 View 的 measure 过程

  1. 如果有必要,让你的 View 支持 padding

这是因为直接继承 View 的控件,如果不在 draw 方法中处理 padding ,那么 padding 属性时无法起作用的。另外直接继承 ViewGroup 的控件需要在 onMeasure 和 onLayout 中考虑 padding 和子元素的 margin 对其造成的影响,不然将导致 padding 和 子元素的 margin 失效。

  1. 尽量不要在 View 中使用 Handler

这是因为 View 内部本身就提供了 post 系列的方法,完全可以替代 Handler 的作用,当然除非你很明确需要使用 handler 来发送消息。

  1. View 中如果有线程或者动画,需要及时停止,参考 View#onDetachedFromWindow

这一条也很好理解,如果有线程或者动画需要停止时,那么 onDetachedFromWindow 是一个很好的时机。当包含此 View 的 Activity 退出或者当前 View 被 remove 时,View 的 onDetachedFromWindow 方法会被调用,和此方法对应的是 onAttachedToWindow ,当包含此 View 的 Activity 启动时,View 的 onAttachedToWindow 方法会被调用,同时,当 View 变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,将有可能会造成内存泄漏。

  1. View 带有滑动嵌套情形时,需要处理好滑动冲突

如果有滑动冲突的话,那么就需要合适的处理滑动冲突,否则将严重影响 View 的效果,具体处理请看高级 UI 成长之路 (二) 深入理解 Android 8.0 View 触摸事件分发机制

自定义 View 示例

下面通过示例代码来一起学习自定义 View, 下面还是以自定义分类来具体体现。

  1. 继承 View 重写 onDraw 方法

为了更好的展示一些平时不容易注意到的问题,这里先实现一个很简单的自定义控件,我们先绘制一个圆,尽管如此,需要注意的细节还是很多的,为了实现一个规范控件,在实现过程必须考虑 wrap_content 模式以及 padding ,同时为了便捷性,还要对外提供自定义属性,我们先来看一下代码实现,如下:

class CircleView2: View {
val color = Color.RED
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
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()
}

override fun draw(canvas: Canvas) {
super.draw(canvas)
val height = height
val width = width
val radius = Math.min(width, height) / 2f
canvas.drawCircle(width/2f,height/2f,radius,paint)
}
private fun init() {
paint.setColor(color)
paint.isAntiAlias = true
}
}
复制代码

<?xml version="1.0" encoding="utf-8"?>

<com.devyk.customview.sample_1.CircleView2
android:layout_width=“match_parent”
android:layout_height=“100dp”/>

复制代码

上面简单绘制了一个以当前宽高的一半的最小值在自己的中心点绘制一个红色的实心圆,其实上面并不是一个规范的自定义控件为什么这么说呢?我们通过调整布局参数再来看一下

<?xml version="1.0" encoding="utf-8"?>

<com.devyk.customview.sample_1.CircleView2
android:layout_width=“match_parent”
android:background=“#000”
android:layout_height=“100dp”/>

<com.devyk.customview.sample_1.CircleView2
android:layout_width=“match_parent”
android:background=“#9C27B0”
android:layout_margin=“20dp”
android:layout_height=“100dp”/>

复制代码

结语

  • 现在随着短视频,抖音,快手的流行NDK模块开发也显得越发重要,需要这块人才的企业也越来越多,随之学习这块的人也变多了,音视频的开发,往往是比较难的,而这个比较难的技术就是NDK里面的技术。
  • 音视频/高清大图片/人工智能/直播/抖音等等这年与用户最紧密,与我们生活最相关的技术一直都在寻找最终的技术落地平台,以前是windows系统,而现在则是移动系统了,移动系统中又是以Android占比绝大部分为前提,所以AndroidNDK技术已经是我们必备技能了。
  • 要学习好NDK,其中的关于C/C++,jni,Linux基础都是需要学习的,除此之外,音视频的编解码技术,流媒体协议,ffmpeg这些都是音视频开发必备技能,而且
  • OpenCV/OpenGl/这些又是图像处理必备知识,下面这些我都是当年自己搜集的资料和做的一些图,因为当年我就感觉视频这块会是一个大的趋势。所以提前做了一些准备。现在拿出来分享给大家。

有需要的小伙伴可以私信我免费分享给你或者点击下面链接自行领取

Android学习PDF+架构视频+面试文档+源码笔记

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

在拿出来分享给大家。

有需要的小伙伴可以私信我免费分享给你或者点击下面链接自行领取

Android学习PDF+架构视频+面试文档+源码笔记

[外链图片转存中…(img-Hk1CUKNT-1710959865818)]

[外链图片转存中…(img-ymkMQIY4-1710959865818)]

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-LRHKda0w-1710959865819)]
[外链图片转存中…(img-RUFzqcfU-1710959865819)]
[外链图片转存中…(img-Gqr5b30G-1710959865820)]
[外链图片转存中…(img-8F1s1hNH-1710959865820)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-zSlCCrQE-1710959865821)]

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值