Android UI界面由以下树形结构组成, 从图中可以看出, UI界面是有View与ViewGroup两大类控件组成,在下面树形图中不管是View还是ViewGroup都是从android.view.View中派生, 而ViewGroup作为容器, 它可以装载和管理其下的一些列由android.view.View派生出来的元素(View和ViewGroup):
由android.view.View派生出来的单一控件元素常见的有TextView, Button, ImageView等, 派生出的容器有LinearLayout, FrameLayout 等, 也有一些由ViewGroup派生出来的控件做为单一控件元素使用的, 比如说ListView, 当然我们也可以把ListView当做容器使用。Android通过布局可以完成很多有创意富有美感的界面, ViewGroup的作用很大,这里单独拿出来研究。
ViewGroup实现了android.view.ViewParent和android.view.ViewManager两个接口, 赋予其装载子控件和管理子控件的能力。这篇主要讲Android控件如何绘制到界面上的。
控件显示到界面上主要分三个流程, 如下图。这是一个非常自然的想法, 得到大小后才可以布局, 布局好了才可以绘制; 这三个流程都是按照上图树形结构递归的。对于这三个流程,只要对Android控件稍有研究的人都
会发现, 每一个控件都有measure(), layout(), draw()方法, 下面分别分析其作用:
measure 递归:
1、判断是否需要重新计算大小
2、调用onMeasure, 如果是ViewGroup类型, 则遍历所有子控件的measure方法,计算出子控件大小,
3、使用setMeasuredDimension(int, int)确定自身计算的大小
由于第二步会调用子控件的measure方法, 在子控件的大小计算当中也会经历这三步动作, 直到整个树遍历完, 此时此控件及其子控件的大小都确定了, 在这里强调控件的大小是由父控件和自身决定的,当然取决在于父控件, 控件自身只提供参考值, 这是因为控件的measure方法是由父控件调用的, 而父控件的控件有限,可能不完全按照你的申请要求给出, 这里留待以后讨论关于布局参数问题。
在android.view.View对于measure流程已经实现了一部分:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); ... }
对于android.view.View来说它不需要遍历子控件了, 下面贴出一个我实现的一个onMeasure :
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //获取mode和size, 方便给children分配空间 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); //TODO 这里可以检查你的大小, 或者mode final int count = getChildCount(); for(int i = 0; i < count; i++) { final View view = getChildAt(i); //这里只是举一个例子, 这里给child多少大小根据实际来定 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); view.measure(childWidthMeasureSpec, childHeightMeasureSpec); } // 得出自己计算出的大小, 这里也是一个例子, 可以根据所有子控件占多大空间 // 给出, 这里也根据要实现的效果看, 这部分建议看LinearLayout等容器的源码 setMeasuredDimension(widthSize, heightSize); }
layout 递归:
1、设置自身相对父控件的位置并判断是否需要重新布局,使用setFrame(left, top, right, bottom);
2、调用onLayout()布局子控件
在android.view.View也实现了此流程的一部分:
public void layout(int l, int t, int r, int b) { ... onLayout(changed, l, t, r, b); ... }
下面我也简单的实现了第二步:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); int widthSpan = 0; int heightSpan = 0; for(int i = 0; i < count; i++) { final View child = getChildAt(i); child.layout(widthSpan, heightSpan, child.getMeasuredWidth(), child.getMeasuredHeight()); widthSpan += child.getMeasuredWidth(); heightSpan += child.getMeasuredHeight(); } }
这是一个简陋的Grid布局。
draw递归:
1、绘制背景
2、调用onDraw()绘制控件内容
3、调用dispatchDraw()绘制所有的子控件
4、绘制渐变边界等
5、绘制装饰品, 比如滑动条等
draw递归在android.view.View已经有完整的实现, 自定义ViewGroup时一般只需要重写onDraw实现如何绘制内容就够了, 当然所有的流程都可以重写, 如果需要的话。下面看一下android.view.View里面draw递归的原型:
public void draw(Canvas canvas) { // Step 1, draw the background, if needed ...// Step 2, draw the content onDraw(canvas); // Step 3, draw the children dispatchDraw(canvas); // Step 4, draw the fade effect and restore layers ... //Step 5, draw decorations onDrawScrollBars(canvas); }
上面三个递归, 解决了一颗控件树的显示问题, 现在大家会很奇怪, 到底是谁发起这个递归, 即最上层的父控件到底是谁, 查看源码可以看到, 在android.view下面有一个ViewRoot(更新后变成ViewRootImpl)隐藏类, 在其performTraversals()方法中发起这三个递归,这个类没有研究太深入, 以后补上。在performTraversals()中大概的流程是:
private void performTraversals() { final View host = mView; ... host.measure(); ... host.layout(); ... host.draw(); ... }
这样就实现了一个大的递归, 把完整的界面给绘制出来了。下面我自己写一个实现ViewGroup的Demo:
package com.ui.viewgroup; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; public class ViewGroupImpl extends ViewGroup { public class LayoutParams extends ViewGroup.LayoutParams { public int left = 0; public int top = 0; public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(int left, int top, int width, int height) { super(width, height); this.left = left; this.top = top; } } public ViewGroupImpl(Context context) { this(context, null); } public ViewGroupImpl(Context context, AttributeSet attrs) { super(context, attrs); } public void addInScreen(View child, int left, int top, int width, int height) { addView(child, new LayoutParams(left, top, width, height)); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); // 检测控件大小是否符合要求 if(widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { throw new IllegalArgumentException("不合法的MeasureSpec mode"); } // 计算子控件大小 final int count = getChildCount(); for(int i = 0; i < count; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams)child.getLayoutParams(); //确定大小的 final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } // 设置计算的控件大小 setMeasuredDimension(widthSize, heightSize); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); LayoutParams lp; for(int i = 0; i < count; i++) { final View child = getChildAt(i); lp = (LayoutParams)child.getLayoutParams(); //相对父控件坐标 child.layout(lp.left, lp.top, lp.left + lp.width, lp.top + lp.width); } } // draw递归 不需要我们接管, @Override public void draw(Canvas canvas) { super.draw(canvas); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); } }
Activity:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ViewGroupImpl viewGroupImpl = new ViewGroupImpl(this); setContentView(viewGroupImpl, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); // 因为此时无法获取viewGroupImpl的实际大小, 所以只好假设一个大小 final int parentWidth = 400; final int parentHeight = 700; final int maxWidthSize = parentWidth / 4; final int maxHeightSize = parentHeight / 4; Random random = new Random(); for(int i = 0; i < 50; i++) { int left = random.nextInt(parentWidth) - 10; int top = random.nextInt(parentHeight) - 10; int width = random.nextInt(maxWidthSize) + 10; int height = random.nextInt(maxHeightSize) + 10; ImageView child = new ImageView(this); child.setImageResource(R.drawable.ic_launcher); viewGroupImpl.addInScreen(child, left, top, width, height); }
下面是效果图:
介绍一本Git的书,<<Pro Git>>,此书的网络版是可以免费获取的,是一本非常好的介绍Git的书籍。
然后,介绍一下我的运行环境,是ubuntu10.10,Git版本是1.7.1。
现在,进入正题介绍一下基本的Git命令,通过这些命令,你可以搭建一个单机版的Git库,开始你的工作,并且,在你是用Git管理你的软件时,你绝大部分时间使用的命令就是这几个。
这些命令有:
git init 初始化Git库
git add 向Git库提交文件修改(包括创建文件)
git commit 基于此分支提交一个更改
git reset 去除目标提交之后的一切提交记录(世界清净级大招)
git log 查看当前分支下的提交记录
git status 查看当前状态
git checkout 切换分支或回到某次提交
git branch 创建分支,查看分支等
git merge 合并目标分支到当前分支
从现在开始,我们一一实践这些命令
首先我们创建文件夹GitTest
#mkdir GitTest
#cd GitTest
然后创建文件readme,test并在test输入字符串“1”
#touch readme
#echo "1">test
接下来,执行一下三个操作
#git init 创建git仓库
#git config user.name yym 配置作者名
#git config user.email yym@**.com 配置email
#git add . 向git仓库中添加文件(.表示当前目录及其子目录下所有文件)
#git commit -m "initial" 进行第一次提交
现在,分别运行三个命令
#ls -a
. .. .git readme test
如果你看到.git恭喜你,你已经有了一个Git仓库,如果你没看到,抱歉,请重新安装Git。
#git status
# On branch master
nothing to commit (working directory clean)
翻译一下,你在分支master上,没什么可以提交的(工作文件夹很干净)
这里有两个概念需要解释,都很重要:分支,干净
1.branch分支是Git的一个重要概念,可以说Git是以这个概念为核心设计的。你可以把分支理解为你当前的工作发展方向,你的程序所在的位置,你的未来。一般来说,会有如下分支稳定版本bug修复分支,程序发展分支1~n,特性分支,发展分支,主分支等等。分支的管理和权限分配构成里项目的组织结构,这就像数据结构之于算法。
关于分支的妙用,这里推荐一个篇文章
《Git分支管理是一门艺术》
2.干净,这是一个美妙的词。干净代表你的程序没有什么需要提交的修改,意味着你完成了这个阶段性成果,当然这是在你的程序是正确的情况下。
#git log
commit 5b1ac4cff1bc7d91622ba6d5f733db1d2ba2af0f
Author: yym <yym@**.com>
Date: Wed Sep 28 17:56:06 2011 +0800
initial
commit代表提交,后面的hash值是这次提交的唯一标志,接下来是作者与时间,然后就是这次提交的名称。简洁直观
接下来,我们修改readme文件及test文件
#echo I" create this dir for learning Git">>readme
#echo “test”>test
注意>>和>的区别
创建文件notrack
#touch notrack
然后我们输入
#git status
# On branch master
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: readme
# modified: test
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# notrack
no changes added to commit (use "git add" and/or "git commit -a")
很好,我们发现,我们还在master分支上(当然!),然后,我们发现readme与test处于修改未更新状态,notrack处于未追踪状态。这是什么意思呢?
要回答这个问题,就需要介绍一个很要的概念。在Git的世界中,文件被分为3类:
1.未追踪,也就是说在此文件夹下,但是未被Git库追踪。
2.未修改,这代表这个文件未被改变
3.修改未更新,这就是上面的状态了
4.已暂存,处于这种状态,那就是等着提交(commit)了
现在我们需要用到add命令,将修改提交打Git库中。
#git add readme
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: readme
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: test
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# notrack
此时,你就可以将修改提交到Git库中,当然,你也可以将test文件的修改也暂存一起提交。这里我只提交单个文件的修改
#git commit -m "2nd commit"
#git status
# On branch master
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: test
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# notrack
no changes added to commit (use "git add" and/or "git commit -a")
可以看到,刚才的readme文件的修改已经被提交了
#git log
commit ae79a590fa13673a584bc5cea08eaa260a7dd473
Author: yym <yym@**.com>
Date: Wed Sep 28 18:10:48 2011 +0800
2nd commit
commit 52a2d3bb92cc54a69f43a6118ca3ee1d1b520156
Author: yym <yym@**.com>
Date: Wed Sep 28 18:01:29 2011 +0800
initial
这是提交记录,就不多说了。现在,我们把test文件也提交了。
#git add test
#git commit -m "3rd commit"
#git log
commit b8d7f73a84944bc38a36990512ab5d9ceda77fef
Author: yym <yym@**.com>
Date: Thu Sep 29 09:34:51 2011 +0800
3rd commit
commit ae79a590fa13673a584bc5cea08eaa260a7dd473
Author: yym <yym@**.com>
Date: Wed Sep 28 18:10:48 2011 +0800
2nd commit
commit 52a2d3bb92cc54a69f43a6118ca3ee1d1b520156
Author: yym <yym@**.com>
Date: Wed Sep 28 18:01:29 2011 +0800
initial
突然,你想回退到2nd commit,想看看当时test文件中的内容。
你可以使用命令
#git checkout ae79a590fa13673a584bc5cea08eaa260a7dd473
#cat 1
1
看完test之前的内容后,你觉得还是第三次提交的内容比较合适,又想回到第三次提交,同样的,你可以
#git checkout b8d7
#cat test
test
你可以不必打出所有位数,只要你保证他是独一无二的,但是你至少需要打出四位。
好吧,不要觉得我是个反复无常的人,现在我有觉得3rd commit完全是多余,我想回到2nd,彻底丢弃之后的内容。只需输入如下命令
#git reset --hard ae79
#git log
commit ae79a590fa13673a584bc5cea08eaa260a7dd473
Author: yym <yym@**.com>
Date: Wed Sep 28 18:10:48 2011 +0800
2nd commit
commit 52a2d3bb92cc54a69f43a6118ca3ee1d1b520156
Author: yym <yym@**.com>
Date: Wed Sep 28 18:01:29 2011 +0800
initial
最后,觉得每次都要add所有修改文件太麻烦了?
ok
git commit -a -m "CommitName"
你可以使用如上命令跳过暂存区域,或者你也可以把它看作是自动add已修改文件
关于Git的一部分基本操作就介绍到这里,下文会介绍Git的另一部分常用操作。
一直对 cocos2d 的 opengl 混合机制不太明晰,昨日纠查 bug 的时候连带着注意了一下,
CCNode 中包含了一个 m_glServerState 的成员,这个东西是与 混合开启与否相关联的,
混合默认是开启的。
CCLayerColor、CCSprite 等类型里面包含了一个 m_blendFunc 成员,这个东西是与采用怎么样的混合方式相关联的。
在 CCProtocols.h 的 CCBlendProtocol 的 @brief 注释里面可以看到,
默认是采用 {GL_ONE, GL_ONE_MINUS_SRC_ALPHA} 或
{GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA} 的混合方式,选择哪种与 premultiplied alpha 相关。
当一个场景被绘制的时候,会根据子节点的 z 值来决定绘制的先后顺序,
在具体绘制某个可视子节点的时候,会根据该节点的 m_blendFunc 成员来决定用什么样的方式混合。
cocos2d 只是对 opengles 简化使用的一种封装!这里存在一个问题:
在 CCRenderTexture 上面绘制东西的时候,绘制几何图元 或 拿 sprite 对象执行 visit 动作的时候,
如果不调用 glBlendFunc 来指定混合方式的话,就会沿用绘制上一个 sprite 的混合方式。
显然这就让此次操作的结果带有不确定性,因为谁也没办法预料之前绘制的最后一个 sprite 采用的是何种混合方式。
(这里说的有点儿夸张了,实际上很多数情况下都不会对 sprite 的 blendFunc 做设置)
最稳妥的方式就是:在 CCRenderTexture 上面绘制的东西的时候即时设置一下混合方式,消除不确定性
代码:ccGLBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
需求多变,但是记住默认的混合参数是没有害处的,默认的为 {GL_ONE, GL_ONE_MINUS_SRC_ALPHA}。
另外一个问题就是,该采用何种的 shader?
毫无疑问,我在刚接触 shader 的时候也是碰了一鼻子灰,
经过一连串开发的磨砺,我才逐渐得以一窥全貌
(不敢托大,这里的全貌指的是有了一个大体的正确认识,gles1都还未能摸透就被迫迁移到2,这令我亚历山大)
cocos2d 缓存了一些常用的 shader,分别是用于一些特别的绘制情形。
打个比方来说,现在有一个需求,要绘制 50 个相同颜色的点,
要达到最高的性能,可以用 kCCShader_Position_uColor 这个枚举值所代表的 shader 来画,
这个 uColor 的 u 表示 uniform,具有 “统一” 的意思,
也就是说,不管花多少个点,都只能采用同一种颜色。
但如果我要绘制五十个不同颜色的点呢?
这确实是个问题,很显然已经超出上面那个缓存的 shader 对象所掌管的能力范围了。
不过这依然不是一个难题,用 kCCShader_PositionColor 从缓存里面拿相应的 shader 就能满足需求了~
具体方式是传入一个长度为 50 的颜色数组,再传入一个长度为 50 的位置数组,然后绘制。
挺能的啊,再出个难题!那五光十色的材质是怎么贴出来的呢?
答案也是 shader,而且是具备贴材质能力的 shader,
具体是那种我就不指明了,自己去摸索吧~
(提示:请于之前提及过的两个枚举值的定义处寻找答案)
又扩展了一些知识,当然这些知识是与主体有所关联的,
因为在 CCRenderTexture 上面绘制东西的时候也有关于 shader 方面的东西要注意,
与混合方式差不多的意思,不过这里的是 shader 是否启用 vertexArrayAttribute
没做仔细测试,不清楚 cocos2d 缓存的 kCCShader_Position_uColor 是否默认就启用了 Position 的 vertexArrayAttribute
同上,为了消除不确定因素,这里最好也是使用一下下面的代码:
ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position);
上一些代码来看一看吧(一个专门用来往 RenderTexture 上面画东西的单例类):
ItemRender.h
// // ItemRender.h // DreamStack // // Created by Bruce Yang on 12-12-26. // Copyright (c) 2012年 __MyCompanyName__. All rights reserved. // #ifndef DreamStack_ItemRender_h #define DreamStack_ItemRender_h #include "cocos2d.h" #include "Box2D.h" USING_NS_CC; class ItemRender { public: void drawSolidPolygon(const b2Vec2* vertices, int32 vertexCount); void drawSolidCircle(const b2Vec2& center, float32 radius); static ItemRender* sharedInstance(); private: // 采用 cocos2d 缓存的 shader 对象~ void setupCachedShader(); // 采用由自己亲手创建的 shader 对象~ void setupMyShader(); ItemRender(); ~ItemRender(); static ItemRender* m_pItemRender; CCGLProgram* m_pGLProgram; GLint m_iColorLocation; }; #endif
ItemRender.cpp
// // ItemRender.cpp // DreamStack // // Created by Bruce Yang on 12-12-26. // Copyright (c) 2012年 __MyCompanyName__. All rights reserved. // #include "ItemRender.h" /** * p~ */ void ItemRender::drawSolidPolygon(const b2Vec2* vertices, int32 vertexCount) { m_pGLProgram->use(); m_pGLProgram->setUniformForModelViewProjectionMatrix(); ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position); ccGLBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glUniform4f( m_iColorLocation, 1.f, 1.f, 1.f, 1.f); glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, vertices); glDrawArrays(GL_TRIANGLE_FAN, 0, vertexCount); CHECK_GL_ERROR_DEBUG(); } void ItemRender::drawSolidCircle(const b2Vec2& center, float32 radius) { m_pGLProgram->use(); m_pGLProgram->setUniformForModelViewProjectionMatrix(); ccGLEnableVertexAttribs(kCCVertexAttribFlag_Position); ccGLBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); const float32 t_fSegsCount = 32.f; int t_iVertsCount = 32; const float32 t_fIncrement = 2.f * b2_pi / t_fSegsCount; float32 theta = 0.f; GLfloat glVertices[t_iVertsCount * 2]; for (int32 i = 0; i < t_fSegsCount; ++ i) { b2Vec2 v = center + radius * b2Vec2(cosf(theta), sinf(theta)); glVertices[i * 2] = v.x; glVertices[i * 2 + 1] = v.y; theta += t_fIncrement; } glUniform4f( m_iColorLocation, 1.f, 1.f, 1.f, 1.f); glVertexAttribPointer(kCCVertexAttrib_Position, 2, GL_FLOAT, GL_FALSE, 0, glVertices); glDrawArrays(GL_TRIANGLE_FAN, 0, t_iVertsCount); CHECK_GL_ERROR_DEBUG(); } #pragma mark ItemRender* ItemRender::sharedInstance() { if (!m_pItemRender) { m_pItemRender = new ItemRender(); } return m_pItemRender; } void ItemRender::setupCachedShader() { m_pGLProgram = CCShaderCache::sharedShaderCache()->programForKey(kCCShader_Position_uColor); m_iColorLocation = glGetUniformLocation(m_pGLProgram->getProgram(), "u_color"); } void ItemRender::setupMyShader() { m_pGLProgram = NULL; m_iColorLocation = (GLint)0; } ItemRender::ItemRender() { this->setupCachedShader(); } ItemRender::~ItemRender() { } ItemRender* ItemRender::m_pItemRender = 0;
还有就是,带 ccGL- 前缀的方法都是 cocos2d 封装的一层带缓存作用的方法。
其内部机制也非常简单,就是判断一下当前要改变到的值和老值是否相同,不同的话才去修改该值。
后续还会对 cocos2d-x 2.x,opengles 2.0 做更深入细致的探索,敬请关注~