Android View绘制的三大流程

简介:
本文讲的是 Android View绘制的三大流程, View的工作流程主要是指measure、layout、draw这三大流程,即测量、布局和绘制,其中measure确定View的测量宽高,layout根据测量的宽高确定View在其父View中的四个顶点的位置,而draw则将View绘制到屏幕上,这样通过ViewGroup的递归遍历,一个View树就展现在屏幕上了。说的简单,下面带大家一步一步从源码中分析:

Android的View是树形结构的:

基本概念

在介绍View的三大流程之前,我们必须先介绍一些基本的概念,才能更好地理解这整个过程。

Window的概念

Window表示的是一个窗口的概念,它是站在WindowManagerService角度上的一个抽象的概念,Android中所有的视图都是通过Window来呈现的,不管是Activity、Dialog还是Toast,只要有View的地方就一定有Window。

这里需要注意的是,这个抽象的Window概念和PhoneWindow这个类并不是同一个东西,PhoneWindow表示的是手机屏幕的抽象,它充当Activity和DecorView之间的媒介,就算没有PhoneWindow也是可以展示View的。

抛开一切,仅站在WindowManagerService的角度上,Android的界面就是由一个个Window层叠展现的,而Window又是一个抽象的概念,它并不是实际存在的,它是以View的形式存在,这个View就是DecorView。

关于Window这方面的内容,我们这里先了解一个大概

DecorView的概念

DecorView是整个Window界面的最顶层View,View的测量、布局、绘制、事件分发都是由DecorView往下遍历这个View树。DecorView作为顶级View,一般情况下它内部会包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下两个部分(具体情况和Android的版本及主题有关),上面是【标题栏】,下面是【内容栏】。在Activity中我们通过setContentView所设置的布局文件其实就是被加载到【内容栏】中的,而内容栏的id是content,因此指定布局的方法叫setContent().

ViewRoot的概念

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完之后,会讲DecorView添加到Window中,同时会创建对应的ViewRootImpl,并将ViewRootImpl和DecorView建立关联,并保存到WindowManagerGlobal对象中。


 
 
  1. WindowManagerGlobal.java 
  2.  
  3. root = new ViewRootImpl(view.getContext(), display);   
  4. root.setView(view, wparams, panelParentView);  

Java

View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来,大致流程如下图:

Measure测量

为了更好地理解View的测量过程,我们还需要理解MeasureSpec,它是View的一个内部类,它表示对View的测量规格。MeasureSpec代表一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(测量大小),我们可以看看它的具体实现:


 
 
  1. MeasureSpec.java 
  2.  
  3. public static class MeasureSpec {   
  4.         private static final int MODE_SHIFT = 30; 
  5.         private static final int MODE_MASK  = 0x3 << MODE_SHIFT; 
  6.  
  7.         /** 
  8.           * UNSPECIFIED 模式: 
  9.           * 父View不对子View有任何限制,子View需要多大就多大 
  10.           */  
  11.         public static final int UNSPECIFIED = 0 << MODE_SHIFT; 
  12.  
  13.         /** 
  14.           * EXACTYLY 模式: 
  15.           * 父View已经测量出子Viwe所需要的精确大小,这时候View的最终大小 
  16.           * 就是SpecSize所指定的值。对应于match_parent和精确数值这两种模式 
  17.           */  
  18.         public static final int EXACTLY     = 1 << MODE_SHIFT; 
  19.  
  20.         /** 
  21.           * AT_MOST 模式: 
  22.           * 子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值, 
  23.           * 即对应wrap_content这种模式 
  24.           */  
  25.         public static final int AT_MOST     = 2 << MODE_SHIFT; 
  26.  
  27.         //将size和mode打包成一个32位的int型数值 
  28.         //高2位表示SpecMode,测量模式,低30位表示SpecSize,某种测量模式下的规格大小 
  29.         public static int makeMeasureSpec(int sizeint mode) { 
  30.             if (sUseBrokenMakeMeasureSpec) { 
  31.                 return size + mode; 
  32.             } else { 
  33.                 return (size & ~MODE_MASK) | (mode & MODE_MASK); 
  34.             } 
  35.         } 
  36.  
  37.         //将32位的MeasureSpec解包,返回SpecMode,测量模式 
  38.         public static int getMode(int measureSpec) { 
  39.             return (measureSpec & MODE_MASK); 
  40.         } 
  41.  
  42.         //将32位的MeasureSpec解包,返回SpecSize,某种测量模式下的规格大小 
  43.         public static int getSize(int measureSpec) { 
  44.             return (measureSpec & ~MODE_MASK); 
  45.         } 
  46.         //... 
  47.     }  

Java

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,并提供了打包和解包的方法。

SpecMode有三种类型,每一类都表示特殊的含义:

UNSPECIFIED

父容器不对View有任何限制,要多大就给多大,这种情况一般用于系统内部,表示一种测量的状态;

EXACTLY

父容器已经检测出View所需的精确大小,这个时候View的最终打消就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体数值这两种模式。

AT_MOST

父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中wrap_content。

View的MeasureSpec是由父容器的MeasureSpec和自己的LayoutParams决定的,但是对于DecorView来说有点不同,因为它没有父类。在ViewRootImpl中的measureHierarchy方法中有如下一段代码展示了DecorView的MeasureSpec的创建过程,其中desiredWindowWidth和desireWindowHeight是屏幕的尺寸大小:

ViewGroup的measure


 
 
  1. childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); 
  2.  
  3. childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); 
  4.  
  5. performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);  

Java

再看看getRootMeasureSpec方法:


 
 
  1. private static int getRootMeasureSpec(int windowSize, int rootDimension) { 
  2.         int measureSpec; 
  3.         switch (rootDimension) { 
  4.  
  5.         case ViewGroup.LayoutParams.MATCH_PARENT: 
  6.             // Window can't resize. Force root view to be windowSize. 
  7.             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); 
  8.             break; 
  9.         case ViewGroup.LayoutParams.WRAP_CONTENT: 
  10.             // Window can resize. Set max size for root view
  11.             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); 
  12.             break; 
  13.         default
  14.             // Window wants to be an exact sizeForce root view to be that size
  15.             measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); 
  16.             break; 
  17.         } 
  18.         return measureSpec; 
  19.     }  

Java

通过以上代码,DecorView的MeasureSpec的产生过程就很明确了,因为DecorView是FrameLyaout的子类,属于ViewGroup,对于ViewGroup来说,除了完成自己的measure过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,他没有重写View的onMeasure方法,这里很好理解,因为每个具体的ViewGroup实现类的功能是不同的,如何测量应该让它自己决定,比如LinearLayout和RelativeLayout。

因此在具体的ViewGroup中需要遍历去测量子View,这里我们看看ViewGroup中提供的测量子View的measureChildWithMargins方法:


 
 
  1. protected void measureChildWithMargins(View child, 
  2.             int parentWidthMeasureSpec, int widthUsed, 
  3.             int parentHeightMeasureSpec, int heightUsed) { 
  4.         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 
  5.  
  6.         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 
  7.                 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin 
  8.                         + widthUsed, lp.width); 
  9.         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, 
  10.                 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin 
  11.                         + heightUsed, lp.height); 
  12.  
  13.         child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 
  14.     }  

Java

上述方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。从代码上看,子元素的MeasureSpec的创建与父容器的MeasureSpec和本身的LayoutParams有关,此外和View的margin和父类的padding有关,现在看看getChildMeasureSpec的具体实现:


 
 
  1. ViewGroup.java 
  2.  
  3. public static int getChildMeasureSpec(int spec, int padding, int childDimension) {   
  4.     int specMode = MeasureSpec.getMode(spec); 
  5.     int specSize = MeasureSpec.getSize(spec); 
  6.  
  7.     int size = Math.max(0, specSize - padding); 
  8.  
  9.     int resultSize = 0; 
  10.     int resultMode = 0; 
  11.  
  12.     switch (specMode) { 
  13.     // Parent has imposed an exact size on us 
  14.     case MeasureSpec.EXACTLY: 
  15.         if (childDimension >= 0) { 
  16.             resultSize = childDimension; 
  17.             resultMode = MeasureSpec.EXACTLY; 
  18.         } else if (childDimension == LayoutParams.MATCH_PARENT) { 
  19.             // Child wants to be our size. So be it. 
  20.             resultSize = size
  21.             resultMode = MeasureSpec.EXACTLY; 
  22.         } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
  23.             // Child wants to determine its own size. It can't be 
  24.             // bigger than us. 
  25.             resultSize = size
  26.             resultMode = MeasureSpec.AT_MOST; 
  27.         } 
  28.         break; 
  29.  
  30.     // Parent has imposed a maximum size on us 
  31.     case MeasureSpec.AT_MOST: 
  32.         if (childDimension >= 0) { 
  33.             // Child wants a specific size... so be it 
  34.             resultSize = childDimension; 
  35.             resultMode = MeasureSpec.EXACTLY; 
  36.         } else if (childDimension == LayoutParams.MATCH_PARENT) { 
  37.             // Child wants to be our size, but our size is not fixed. 
  38.             // Constrain child to not be bigger than us. 
  39.             resultSize = size
  40.             resultMode = MeasureSpec.AT_MOST; 
  41.         } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
  42.             // Child wants to determine its own size. It can't be 
  43.             // bigger than us. 
  44.             resultSize = size
  45.             resultMode = MeasureSpec.AT_MOST; 
  46.         } 
  47.         break; 
  48.  
  49.     // Parent asked to see how big we want to be 
  50.     case MeasureSpec.UNSPECIFIED: 
  51.         if (childDimension >= 0) { 
  52.             // Child wants a specific size... let him have it 
  53.             resultSize = childDimension; 
  54.             resultMode = MeasureSpec.EXACTLY; 
  55.         } else if (childDimension == LayoutParams.MATCH_PARENT) { 
  56.             // Child wants to be our size... find out how big it should 
  57.             // be 
  58.             resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size
  59.             resultMode = MeasureSpec.UNSPECIFIED; 
  60.         } else if (childDimension == LayoutParams.WRAP_CONTENT) { 
  61.             // Child wants to determine its own size.... find out how 
  62.             // big it should be 
  63.             resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size
  64.             resultMode = MeasureSpec.UNSPECIFIED; 
  65.         } 
  66.         break; 
  67.     } 
  68.     //noinspection ResourceType 
  69.     return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 
  70.  

Java

上述代码根据父类的MeasureSpec和自身的LayoutParams创建子元素的MeasureSpec,具体过程同学们自行分析,最终的创建规则如下表:

ViewGroup在遍历完子View后,需要根据子元素的测量结果来决定自己最终的测量大小,并调用setMeasuredDimension方法保存测量宽高值。


 
 
  1. setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),heightSizeAndState); 

Java

这里调用了resolveSizeAndState来确定最终的大小,主要是保证测量的大小不能超过父容器的最大剩余空间maxWidth,这里我们看看它里面的实现:


 
 
  1. public static int resolveSizeAndState(int sizeint measureSpec, int childMeasuredState) { 
  2.         final int specMode = MeasureSpec.getMode(measureSpec); 
  3.         final int specSize = MeasureSpec.getSize(measureSpec); 
  4.         final int result; 
  5.         switch (specMode) { 
  6.             case MeasureSpec.AT_MOST: 
  7.                 if (specSize < size) { 
  8.                     result = specSize | MEASURED_STATE_TOO_SMALL; 
  9.                 } else { 
  10.                     result = size
  11.                 } 
  12.                 break; 
  13.             case MeasureSpec.EXACTLY: 
  14.                 result = specSize; 
  15.                 break; 
  16.             case MeasureSpec.UNSPECIFIED: 
  17.             default
  18.                 result = size
  19.         } 
  20.         return result | (childMeasuredState & MEASURED_STATE_MASK); 
  21.     }  

Java

关于具体ViewGroup的onMeasure过程这里不做分析,由于每种布局的测量方式不一样,不可能逐个分析,但在它们的onMeasure里面的步骤是有一定规律的:

1.根据各自的测量规则遍历Children元素,调用getChildMeasureSpec方法得到Child的measureSpec;

2.调用Child的measure方法;

3.调用setMeasuredDimension确定最终的大小。

View的measure

View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在View的measure方法里面会去调用onMeasure方法,我们这里只要看onMeasure的实现即可,如下:


 
 
  1. View.java 
  2.  
  3.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
  4.         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
  5.                 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); 
  6.     }  

Java

代码很简单,我们继续看看getDefaultSize方法的实现:


 
 
  1. View.java 
  2.  
  3.     public static int getDefaultSize(int sizeint measureSpec) { 
  4.         int result = size
  5.         int specMode = MeasureSpec.getMode(measureSpec); 
  6.         int specSize = MeasureSpec.getSize(measureSpec); 
  7.  
  8.         switch (specMode) { 
  9.         case MeasureSpec.UNSPECIFIED: 
  10.             result = size
  11.             break; 
  12.         case MeasureSpec.AT_MOST: 
  13.         case MeasureSpec.EXACTLY: 
  14.             result = specSize; 
  15.             break; 
  16.         } 
  17.         return result; 
  18.     } 

Java

从上述代码可以得出,View的宽/高由specSize决定,直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。

上述就是View的measure大致过程,在measure完成之后,通过getMeasuredWidth/Height方法就可以获得测量后的宽高,这个宽高一般情况下就等于View的最终宽高了,因为View的layout布局的时候就是根据measureWidth/Height来设置宽高的,除非在layout中修改了measure值。

Layout布局

Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法。简单的来说就是,layout方法确定View本身的位置,而onLayout方法则会确定所有子元素的位置。

先看看View的layout方法:


 
 
  1. public void layout(int l, int t, int r, int b) { 
  2.         if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { 
  3.             onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); 
  4.             mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; 
  5.         } 
  6.  
  7.         int oldL = mLeft; 
  8.         int oldT = mTop; 
  9.         int oldB = mBottom; 
  10.         int oldR = mRight; 
  11.  
  12.         boolean changed = isLayoutModeOptical(mParent) ? 
  13.                 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); 
  14.  
  15.         if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { 
  16.             onLayout(changed, l, t, r, b); 
  17.  
  18.             if (shouldDrawRoundScrollbar()) { 
  19.                 if(mRoundScrollbarRenderer == null) { 
  20.                     mRoundScrollbarRenderer = new RoundScrollbarRenderer(this); 
  21.                 } 
  22.             } else { 
  23.                 mRoundScrollbarRenderer = null
  24.             } 
  25.  
  26.             mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; 
  27.  
  28.             ListenerInfo li = mListenerInfo; 
  29.             if (li != null && li.mOnLayoutChangeListeners != null) { 
  30.                 ArrayList<OnLayoutChangeListener> listenersCopy = 
  31.                         (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); 
  32.                 int numListeners = listenersCopy.size(); 
  33.                 for (int i = 0; i < numListeners; ++i) { 
  34.                     listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); 
  35.                 } 
  36.             } 
  37.         } 
  38.  
  39.         mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; 
  40.         mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; 
  41.     }  

因微信字数限制,请点击原文链接查看完整内容

总结

到这里,View的measure、layout、draw三大流程就说完了,这里做一下总结:

  • 如果是自定义ViewGroup的话,需要重写onMeasure方法,在onMeasure方法里面遍历测量子元素,同理onLayout方法也是一样,最后实现onDraw方法绘制自己;
  • 如果自定义View的话,则需要从写onMeasure方法,处理wrap_content的情况,不需要处理onLayout,最后实现onDraw方法绘制自己; 

本文作者:佚名

来源:51CTO

原文标题:Android View绘制的三大流程
相关文章
|
23天前
|
Android开发 UED 计算机视觉
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
本文介绍了一款受游戏“金铲铲之战”启发的Android自定义View——线条等待动画的实现过程。通过将布局分为10份,利用`onSizeChanged`测量最小长度,并借助画笔绘制动态线条,实现渐变伸缩效果。动画逻辑通过四个变量控制线条的增长与回退,最终形成流畅的等待动画。代码中详细展示了画笔初始化、线条绘制及动画更新的核心步骤,并提供完整源码供参考。此动画适用于加载场景,提升用户体验。
251 5
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
|
23天前
|
Android开发 开发者
Android设置View是否可用
在Android开发中,有时需要将布局设置为不可点击状态(失去焦点)。常见的解决方法是使用`setOnClickListener(null)`,但本文介绍一种更通用的方式:通过封装`setViewEnabled`方法实现。该方法可递归设置View及其子View的启用状态,支持传入目标View和布尔值(`true`为可用,`false`为禁用)。例如,调用`setViewEnabled(edittext, false)`即可禁用EditText。文章附有源码及示例动图,帮助开发者快速理解与应用。
|
23天前
|
Android开发
Android自定义view之利用PathEffect实现动态效果
本文介绍如何在Android自定义View中利用`PathEffect`实现动态效果。通过改变偏移量,结合`PathEffect`的子类(如`CornerPathEffect`、`DashPathEffect`、`PathDashPathEffect`等)实现路径绘制的动态变化。文章详细解析了各子类的功能与参数,并通过案例代码展示了如何使用`ComposePathEffect`组合效果,以及通过修改偏移量实现动画。最终效果为一个菱形图案沿路径运动,源码附于文末供参考。
|
23天前
|
Android开发 开发者
Android自定义view之利用drawArc方法实现动态效果
本文介绍了如何通过Android自定义View实现动态效果,重点使用`drawArc`方法完成圆弧动画。首先通过`onSizeChanged`进行测量,初始化画笔属性,设置圆弧相关参数。核心思路是不断改变圆弧扫过角度`sweepAngle`,并调用`invalidate()`刷新View以实现动态旋转效果。最后附上完整代码与效果图,帮助开发者快速理解并实践这一动画实现方式。
|
23天前
|
Android开发 数据安全/隐私保护 开发者
Android自定义view之模仿登录界面文本输入框(华为云APP)
本文介绍了一款自定义输入框的实现,包含静态效果、hint值浮动动画及功能扩展。通过组合多个控件完成界面布局,使用TranslateAnimation与AlphaAnimation实现hint文字上下浮动效果,支持密码加密解密显示、去除键盘回车空格输入、光标定位等功能。代码基于Android平台,提供完整源码与attrs配置,方便复用与定制。希望对开发者有所帮助。
|
23天前
|
XML Java Android开发
Android自定义view之网易云推荐歌单界面
本文详细介绍了如何通过自定义View实现网易云音乐推荐歌单界面的效果。首先,作者自定义了一个圆角图片控件`MellowImageView`,用于绘制圆角矩形图片。接着,通过将布局放入`HorizontalScrollView`中,实现了左右滑动功能,并使用`ViewFlipper`添加图片切换动画效果。文章提供了完整的代码示例,包括XML布局、动画文件和Java代码,最终展示了实现效果。此教程适合想了解自定义View和动画效果的开发者。
135 65
Android自定义view之网易云推荐歌单界面
|
23天前
|
XML 前端开发 Android开发
一篇文章带你走近Android自定义view
这是一篇关于Android自定义View的全面教程,涵盖从基础到进阶的知识点。文章首先讲解了自定义View的必要性及简单实现(如通过三个构造函数解决焦点问题),接着深入探讨Canvas绘图、自定义属性设置、动画实现等内容。还提供了具体案例,如跑马灯、折线图、太极图等。此外,文章详细解析了View绘制流程(measure、layout、draw)和事件分发机制。最后延伸至SurfaceView、GLSurfaceView、SVG动画等高级主题,并附带GitHub案例供实践。适合希望深入理解Android自定义View的开发者学习参考。
380 84
|
23天前
|
Android开发 开发者
Android自定义View之不得不知道的文件attrs.xml(自定义属性)
本文详细介绍了如何通过自定义 `attrs.xml` 文件实现 Android 自定义 View 的属性配置。以一个包含 TextView 和 ImageView 的 DemoView 为例,讲解了如何使用自定义属性动态改变文字内容和控制图片显示隐藏。同时,通过设置布尔值和点击事件,实现了图片状态的切换功能。代码中展示了如何在构造函数中解析自定义属性,并通过方法 `setSetting0n` 和 `setbackeguang` 实现功能逻辑的优化与封装。此示例帮助开发者更好地理解自定义 View 的开发流程与 attrs.xml 的实际应用。
Android自定义View之不得不知道的文件attrs.xml(自定义属性)
|
23天前
|
前端开发 Android开发 UED
讲讲Android为自定义view提供的SurfaceView
本文详细介绍了Android中自定义View时使用SurfaceView的必要性和实现方式。首先分析了在复杂绘制逻辑和高频界面更新场景下,传统View可能引发卡顿的问题,进而引出SurfaceView作为解决方案。文章通过Android官方Demo展示了SurfaceView的基本用法,包括实现`SurfaceHolder.Callback2`接口、与Activity生命周期绑定、子线程中使用`lockCanvas()`和`unlockCanvasAndPost()`方法完成绘图操作。
|
23天前
|
Android开发 开发者
Android自定义view之围棋动画(化繁为简)
本文介绍了Android自定义View的动画实现,通过两个案例拓展动态效果。第一个案例基于`drawArc`方法实现单次动画,借助布尔值控制动画流程。第二个案例以围棋动画为例,从简单的小球直线运动到双向变速运动,最终实现循环动画效果。代码结构清晰,逻辑简明,展示了如何化繁为简实现复杂动画,帮助读者拓展动态效果设计思路。文末提供完整源码,适合初学者和进阶开发者学习参考。
Android自定义view之围棋动画(化繁为简)