7.4.3 自绘控件
Android控件框架为开发者提供了高度可定制性和可扩展性,帮助开发者定制适合产品需求的控件框架,满足了大部分产品的需求。
但在很多产品中,追求的是极其特别或者是高效的交互方式,比如Android原生的3D图库管理应用(如图7-16所示)。它可以根据设备的重力感应信息,将图片列表展现成3D效果,并且用户可以通过滑动、拖动、双手缩放等操作,快速地对图片进行操作,响应非常灵敏,界面渲染非常高效。
图 7-16 3D图库应用的显示效果
为了实现这样的交互效果,3D图库的界面基于Surface控件定制而成,自行构建了一套控件框架,对于需要实现特殊交互界面的应用而言,非常有指导意义。
3D图库的界面由自定义控件GLRootView来构建,GLRootView派生自Surface控件之一android.opengl.GLSurfaceView。GLSurfaceView具有独立的窗口,可以使用Android提供的OpenGL在控件上任意绘制需要呈现的内容:
public class GLRootView extends GLSurfaceView
implements GLSurfaceView.Renderer, GLRoot{
private GLView mContentView;//承载界面中自定义控件树的对象
private GLCanvasImpl mCanvas;//绘制内容的画布对象
public void setContentPane(GLView content){
//设置自定义的控件树,触发重新丈量和绘制界面内容
mContentView=content;
if(content!=null){
content.attachToRoot(this);
setNeedLayout(true);
}
…
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int
bottom){
//当控件尺寸发生变化时,也需要重新绘制界面内容
if(changed)setNeedLayout(true);
}
@Override
public void onSurfaceCreated(GL10 gl1,EGLConfig config){
//当控件被构造准备绘制之前,该函数会被调用
//在该函数中通常会进行一些初始化工作,比如准备好画布Canvas对象
GL11 gl=(GL11)gl1;
mCanvas=new GLCanvasImpl(gl);
…
}
@Override
public void onSurfaceChanged(GL10 gl1,int width, int height){
//当控件尺寸发生变更时,会调用该函数
//在该函数中,需要重新对一些参数进行调整,比如变更画布的尺寸
mCanvas.setSize(width, height);
…
}
@Override
public void onDrawFrame(GL10 gl){
//如果内容的位置发生了变更,先重新丈量和设置一下内容的位置
if(NeedLayout()){
setNeedLayout(false);
int width=getWidth();
int height=getHeight();
if(mContentView!=null&&
width!=0&&height!=0){
mContentView.layout(0,0,width, height);
}
}
//绘制控件树中的内容
if(mContentView!=null){
mContentView.render(mCanvas);
}
…
}
}
从定义GLRootView控件的代码片段中可以看到,自定义GLSurfaceView控件需要完成两件事情。首先是监控控件的变化,控制需要绘制内容的位置,因为GLSurfaceView中的内容并没有现成的控件架构,需要开发者自行计算。其次,是需要实例化GLSurfaceView.Renderer接口,在对应的函数中进行内容的绘制。
在3D图库应用中,GLRootView是一个通用的自定义控件,被用在多个界面中,这就需要GLRootView中的内容可以定制。图库自定义了一套自绘控件,这些自绘控件并没有派生自Android的控件基类android.view.View,而是从头开始完全自定义(结构如图7-17所示)。
图 7-17 用基于GLView的自绘控件构建交互界面
图库中自绘控件的基类是GLView。与Android的控件架构类似,GLView是一个容器控件,通过子控件的方式构造出一棵控件树,通过GLRootView.setContentPane函数,可以将自绘控件树与自定义的GLRootView控件绑定在一起,共同实现内容的绘制:
public class GLView{
//保存有根控件、父控件和子控件,共同构成了控件树
private GLRoot mRoot;
protected GLView mParent;
private ArrayList<GLView>mComponents;
//定义了自绘控件的丈量和排版函数
//子控件可以继承onMeasure和onLayout函数,计算控件的位置
public void measure(int widthSpec, int heightSpec){
mLastWidthSpec=widthSpec;
mLastHeightSpec=heightSpec;
mViewFlags&=~FLAG_SET_MEASURED_SIZE;
onMeasure(widthSpec, heightSpec);
…
}
public void layout(
int left, int top, int right, int bottom){
boolean sizeChanged=setBounds(left, top, right, bottom);
if(sizeChanged){
mViewFlags&=~FLAG_LAYOUT_REQUESTED;
onLayout(true, left, top, right, bottom);
}else if((mViewFlags&FLAG_LAYOUT_REQUESTED)!=0){
mViewFlags&=~FLAG_LAYOUT_REQUESTED;
onLayout(false, left, top, right, bottom);
}
}
//自定义了界面绘制函数,从上到下,逐个控件进行绘制
//子控件可以通过派生renderBackground函数,绘制其本身的内容
protected void render(GLCanvas canvas){
renderBackground(canvas);
//逐个绘制子控件内容
for(int i=0,n=getComponentCount();i<n;++i){
renderChild(canvas, getComponent(i));
}
}
protected void renderChild(GLCanvas canvas, GLView component){
//在滚动区域内,绘制子控件的内容
int xoffset=component.mBounds.left-mScrollX;
int yoffset=component.mBounds.top-mScrollY;
canvas.translate(xoffset, yoffset,0);
…
component.render(canvas);
canvas.translate(-xoffset,-yoffset,0);
}
}
从GLView的代码片段可以看到,GLView模仿Android的控件实现,为子控件提供了丈量和绘制的接口,通过这种方式自上而下构建了一棵控件树。它使用OpenGL进行内容绘制,可以高效地绘制出漂亮的交互效果,但它同时也失去了原生控件的一些优势,比如:利用资源定制界面内容,处理各种交互事件,等等。
因此,在实践中,自绘控件都会控制需要处理交互事件的种类,来降低开发成本。比如,在图库应用中,GLView就仅仅处理了触摸事件(Touch Event):
protected boolean dispatchTouchEvent(MotionEvent event){
int x=(int)event.getX();
int y=(int)event.getY();
int action=event.getAction();
…
//沿着控件树,逐级往下传播触摸事件
if(action==MotionEvent.ACTION_DOWN){
//从最后一个子控件,到第一个子控件,逐个查看
for(int i=getComponentCount()-1;i>=0;—i){
GLView component=getComponent(i);
if(dispatchTouchEvent(event, x,y, component, true)){
mMotionTarget=component;
return true;
}
}
//如果没有子控件截获该事件,自行处理该事件
//子控件可以派生onTouch函数来获取触摸事件
return onTouch(event);
}
}
自绘控件是一类特殊的自定义控件,它摆脱了Android原生控件的束缚,因此可以更高效地进行交互界面绘制。但相比而言,构建自绘控件无法与Android的资源体系整合,开发成本较高,并不能取代原生控件的作用。