Android(安卓)应用界面绘制流程
上一篇文章《Android 应用界面显示流程》讲到,从Activity.setContentView(int)方法,把布局文件某xml交给了PhoneWindow,PhoneWindow把布局inflate出来附在了DecorView里,然后交给了WMS管理,WMS和AMS调度后最终把画面显示出来。
这篇文章讲ViewRootImpl.performTraversals(),是如何把DecorView绘制出来的。
我们知道自定义View中,需要搞懂三个重要方法onMeasure、onLayout和onDraw。performTraversals()也是分别从measure、layout和draw这3个流程去把View绘制出来的。
方法流程大概是这样的,源码基于Android5.1.1
private void performTraversals() { ... performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... performLayout(lp, desiredWindowWidth, desiredWindowHeight); ... performDraw(); ...}private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { ... mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); ...}private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { ... final View host = mView; ... host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); ...}private void performDraw() { ... draw(fullRedrawNeeded); ...}private void draw(boolean fullRedrawNeeded) { ... if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { return; } ...}private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) ... mView.draw(canvas); ...}
1.performMeasure
我们知道View里面有个MeasureSpec的概念,类型是int。这个int型的32位的二进制数里由两部分组成分别是mode和size,头两位表示类型mode,00表示未指定,01表示精确值,10表示最大值。后面30位表示具体的数值size。举两个例子就是可以有『精确值|100』『最大值|1080』这种长度规格。
private void performTraversals() { ... int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ...}private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { ... mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); ...}private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec;}
执行performMeasure需要MeasureSpec,所以首先需要构建一个MeasureSpec,通过getRootMeasureSpec传入了窗口的实际大小和LayoutParams.MATCH_PARENT(来自WindowManager.LayoutParam的默认值)。MeasureSpec.makeMeasureSpec用于把这两个mode和size合并为一个int型整数。然后performMeasure里,执行了mView.measure,mView就是DecorView了。
View.javapublic final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... onMeasure(widthMeasureSpec, heightMeasureSpec); ... }==================================================================================FrameLayout.java@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); ... //遍历测量子View for (int i = 0; i < count; i++) { final View child = getChildAt(i); //需要判断子View不是GONE的状态 if (mMeasureAllChildren || child.getVisibility() != GONE) { //调用ViewGroup的measureChildWithMargins测量子View measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); childState = combineMeasuredStates(childState, child.getMeasuredState()); if (measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } } ...}==================================================================================ViewGroup.javaprotected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { //获取子类的LayoutParams final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //getChildMeasureSpec的第二个参数,是已经用掉了多少空间。 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的spec,padding+margin已消耗的空间,子View的LayoutParam最终确定子View的specpublic static int getChildMeasureSpec(int spec, int padding, int childDimension) { //父View mode int specMode = MeasureSpec.getMode(spec); //父View size int specSize = MeasureSpec.getSize(spec); //如果父View的大小-已消耗空间 = 负数了,子View的大小就设为0 int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; //判断父View的mode switch (specMode) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}
View.measure(int, int)方法是final类型的,意味着子类均不可重写。所以对于不同的View测量自身,需要重写onMeasure方法。不同的ViewGroup有不同布局策略(比如对与一个wrap_content的ViewGroup布局,LinearLayout是对子View顺序摆放,不会重叠,所以需要的空间是累加的,而FrameLayout只需要求子View空间的最大值就行),所以ViewGroup有不同的onMeasure方法。
这里还有一个重要的前提,就是对于ViewGroup,是有义务测量自己的子View的。Android正式通过这种递归方式,才使得代理里只需要对DecorView操作,自然会递归到最上层的子View。
子View的MeasureSpec是根据父View的MeasureSpec和自己的LayoutParams共同决定的。LayoutParams也就是我们平时在xml中写的layout_widht, layout_height, layout_gravity等这些带layout_前缀的属性。根据这两种排列组合,可以有这几种最终结果。
竖排父View\横排子View | childDimension >= 0 | childDimension == LayoutParams.MATCH_PARENT | childDimension == LayoutParams.WRAP_CONTENT |
MeasureSpec.EXACTLY | 准确值|子View大小 | 准确值|父View大小-padding-margin | 最大值|父View大小-padding-margin |
MeasureSpec.AT_MOST | 准确值|子View大小 | 最大值|父View大小-padding-margin | 最大值|父View大小-padding-margin |
MeasureSpec.UNSPECIFIED | 准确值|子View大小 | 未指定|0 | 未指定|0 |
这几种结果最终也是以MeasureSpec呈现,然后会通过调用到子View的measure方法,把childWidthMeasureSpec和childHeightMeasureSpec传到了子View里。到此完成了一次循环。之后就是不断重复此循环,直到某个View再也没有子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);}private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;}public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); 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的onMeasure方法实现,根据文档说明,在此方面必须调用了setMeasureDimension,主要是为了给属性mMeasuredWidth和mMeasuredHeight赋值。后面的布局会使用到这两个属性。
不同的View有不同的onMeasure方法,它应该根据传进来的MeasureSpec来补充自己的测量逻辑。一个正常的View如果拿到MeasureSpec的mode为准确值(MeasureSpec.EXACTLY),它应该直接设置大小为size大小。如果拿到的MeasureSpec的mode为最大值(MeasureSpec.AT_MOST),它应该自己根据自身需要测量自身内容,比如TextView就是测量文字需要占据多少空间,最终结果取自身测量结果和size的最小值,也就是所谓的不要超过MeasureSpec要求的最大值。
对于ViewGroup,需要把子View都测量完才会最终确定确定自己的尺寸。如上面的例子说的,如果是个wrap_content的LinearLayout需要测量完所有子View占据多少空间才能知道自己应该多大。
2.performLayout
onMeasure方法中会给赋值,在layout这步骤就会使用到
ViewRootImpl.javaprivate void performTraversals() { ... performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... performLayout(lp, desiredWindowWidth, desiredWindowHeight); ... performDraw(); ...}private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { ... final View host = mView; ... host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); ...}==================================================================================ViewGroup.java@Overridepublic final void layout(int l, int t, int r, int b) { if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) { if (mTransition != null) { mTransition.layoutChange(this); } super.layout(l, t, r, b); } else { // record the fact that we noop'd it; request layout when transition finishes mLayoutCalledWhileSuppressed = true; }}@Overrideprotected abstract void onLayout(boolean changed, int l, int t, int r, int b);==================================================================================View.javapublic void layout(int l, int t, int r, int b) { ... boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); ... onLayout(changed, l, t, r, b); ...}protected boolean setFrame(int left, int top, int right, int bottom) { ... mLeft = left; mTop = top; mRight = right; mBottom = bottom; ...}
performLayout里面会有其他功能的代码,比如一定情况下,使用desiredWindowWidth和desiredWindowHeight重新执行测量流程。但是重点是在执行host这个DecorView的Layout方法。Layout方法里会调用到setFrame方法,里面会对四个属性赋值,mLeft、mTop、mRight和mBottom。这4个属性是最终确定该View边界的字段。需要注意的是四个值是相对父View的位置,而不是整个屏幕的。
然后是流程是ViewGroup.layout -> View.layout -> onLayout方法。如果是继承ViewGroup,就需要实现onLayout方法。因为ViewGroup.onLayout是抽象类型的。如果是继承View,则无需实现onLayout。
对于DecorView,会执行到FrameLayout.onLayout方法。FrameLayout是ViewGroup,所以需要确定所有子View的位置。在layouChildren里,会根据布局的Gravity属性进行子View的摆放。同样也是递归,直到View再也没有子View停止。
@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) { layoutChildren(left, top, right, bottom, false /* no force left gravity */);}void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) { final int count = getChildCount(); final int parentLeft = getPaddingLeftWithForeground(); final int parentRight = right - left - getPaddingRightWithForeground(); final int parentTop = getPaddingTopWithForeground(); final int parentBottom = bottom - top - getPaddingBottomWithForeground(); mForegroundBoundsChanged = true; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int width = child.getMeasuredWidth(); final int height = child.getMeasuredHeight(); int childLeft; int childTop; int gravity = lp.gravity; if (gravity == -1) { gravity = DEFAULT_CHILD_GRAVITY; } final int layoutDirection = getLayoutDirection(); final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.CENTER_HORIZONTAL: childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: if (!forceLeftGravity) { childLeft = parentRight - width - lp.rightMargin; break; } case Gravity.LEFT: default: childLeft = parentLeft + lp.leftMargin; } switch (verticalGravity) { case Gravity.TOP: childTop = parentTop + lp.topMargin; break; case Gravity.CENTER_VERTICAL: childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin; break; case Gravity.BOTTOM: childTop = parentBottom - height - lp.bottomMargin; break; default: childTop = parentTop + lp.topMargin; } child.layout(childLeft, childTop, childLeft + width, childTop + height); } }}
3.performDraw
ViewRootImpl.javaprivate void performTraversals() { ... performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ... performLayout(lp, desiredWindowWidth, desiredWindowHeight); ... performDraw(); ...}private void performDraw() { ... draw(fullRedrawNeeded); ...}private void draw(boolean fullRedrawNeeded) { ... if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { return; } ...}private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) ... mView.draw(canvas); ...}==================================================================================View.javapublic 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; /* * 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) */ // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { drawBackground(canvas); } // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 6, draw decorations (scrollbars) onDrawScrollBars(canvas); if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } // we're done... return; } /* * Here we do the full fledged routine... * (this is an uncommon case where speed matters less, * this is why we repeat some of the tests that have been * done above) */ boolean drawTop = false; boolean drawBottom = false; boolean drawLeft = false; boolean drawRight = false; float topFadeStrength = 0.0f; float bottomFadeStrength = 0.0f; float leftFadeStrength = 0.0f; float rightFadeStrength = 0.0f; // Step 2, save the canvas' layers int paddingLeft = mPaddingLeft; final boolean offsetRequired = isPaddingOffsetRequired(); if (offsetRequired) { paddingLeft += getLeftPaddingOffset(); } int left = mScrollX + paddingLeft; int right = left + mRight - mLeft - mPaddingRight - paddingLeft; int top = mScrollY + getFadeTop(offsetRequired); int bottom = top + getFadeHeight(offsetRequired); if (offsetRequired) { right += getRightPaddingOffset(); bottom += getBottomPaddingOffset(); } final ScrollabilityCache scrollabilityCache = mScrollCache; final float fadeHeight = scrollabilityCache.fadingEdgeLength; int length = (int) fadeHeight; // clip the fade length if top and bottom fades overlap // overlapping fades produce odd-looking artifacts if (verticalEdges && (top + length > bottom - length)) { length = (bottom - top) / 2; } // also clip horizontal fades if necessary if (horizontalEdges && (left + length > right - length)) { length = (right - left) / 2; } if (verticalEdges) { topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength())); drawTop = topFadeStrength * fadeHeight > 1.0f; bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength())); drawBottom = bottomFadeStrength * fadeHeight > 1.0f; } if (horizontalEdges) { leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength())); drawLeft = leftFadeStrength * fadeHeight > 1.0f; rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength())); drawRight = rightFadeStrength * fadeHeight > 1.0f; } saveCount = canvas.getSaveCount(); int solidColor = getSolidColor(); if (solidColor == 0) { final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG; if (drawTop) { canvas.saveLayer(left, top, right, top + length, null, flags); } if (drawBottom) { canvas.saveLayer(left, bottom - length, right, bottom, null, flags); } if (drawLeft) { canvas.saveLayer(left, top, left + length, bottom, null, flags); } if (drawRight) { canvas.saveLayer(right - length, top, right, bottom, null, flags); } } else { scrollabilityCache.setFadeColor(solidColor); } // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children 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; if (drawTop) { matrix.setScale(1, fadeHeight * topFadeStrength); matrix.postTranslate(left, top); fade.setLocalMatrix(matrix); p.setShader(fade); canvas.drawRect(left, top, right, top + length, p); } if (drawBottom) { matrix.setScale(1, fadeHeight * bottomFadeStrength); matrix.postRotate(180); matrix.postTranslate(left, bottom); fade.setLocalMatrix(matrix); p.setShader(fade); canvas.drawRect(left, bottom - length, right, bottom, p); } if (drawLeft) { matrix.setScale(1, fadeHeight * leftFadeStrength); matrix.postRotate(-90); matrix.postTranslate(left, top); fade.setLocalMatrix(matrix); p.setShader(fade); canvas.drawRect(left, top, left + length, bottom, p); } if (drawRight) { matrix.setScale(1, fadeHeight * rightFadeStrength); matrix.postRotate(90); matrix.postTranslate(right, top); fade.setLocalMatrix(matrix); p.setShader(fade); canvas.drawRect(right - length, top, right, bottom, p); } canvas.restoreToCount(saveCount); // Step 6, draw decorations (scrollbars) onDrawScrollBars(canvas); if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); }}
类似的,也调用到的DecorView.draw方法,DecorView和FrameLayout都有draw方法的实现。但最重要的部分还是写在了View.draw里面,里面的代码不少,但是通过注释配合代码还是能知道其过程。
1.是绘制背景
2.第二步保存图层信息
3.绘制自身内容
4.绘制子View内容
5.绘制View的边缘渐变内容,还原图层
6.绘制装饰比如滚动条
第三步绘制自身内容是调用了onDraw方法,所以对于ViewGroup一般不用重写这方法。第四步调用到dispatchDraw。
@Overrideprotected void dispatchDraw(Canvas canvas) { ... // Draw any disappearing views that have animations if (mDisappearingChildren != null) { final ArrayList disappearingChildren = mDisappearingChildren; final int disappearingCount = disappearingChildren.size() - 1; // Go backwards -- we may delete as animations finish for (int i = disappearingCount; i >= 0; i--) { final View child = disappearingChildren.get(i); more |= drawChild(canvas, child, drawingTime); } } ...}protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime);}
里面会遍历所有的子View,调用到子View.draw方法,开始下一层的递归绘制。
对于常用的几个 LinearLayout、FrameLayout、RelativeLayout、ConstraintLayout,看了下源码发现,只有LinearLayout有重写 onDraw 方法用来画一个叫 Divider 的东西(没怎么听过啊),其他的都没有自己的实现。也很合理,对于 ViewGroup 这种容器,基本是没什么东西需要绘制的,所以把「绘制背景」这项功能放到 draw() 方法就行了。
更多相关文章
- android studio编写运行java main的三种方法(亲测)
- Android(安卓)Canvas清屏失效
- Android(安卓)ADB server didn't ACK 错误解决
- 【Android】TextView 显示超链接的几种方法
- Android(安卓)中部分文字高亮显示方法
- Android面试宝典(更新中)
- Android(安卓)WebView 详细介绍
- android之AsyncQueryHandler详解
- Surface与SurfaceHolder.Callback