7.4.3 自绘控件

Android控件框架为开发者提供了高度可定制性和可扩展性,帮助开发者定制适合产品需求的控件框架,满足了大部分产品的需求。

但在很多产品中,追求的是极其特别或者是高效的交互方式,比如Android原生的3D图库管理应用(如图7-16所示)。它可以根据设备的重力感应信息,将图片列表展现成3D效果,并且用户可以通过滑动、拖动、双手缩放等操作,快速地对图片进行操作,响应非常灵敏,界面渲染非常高效。

7.4.3 自绘控件 - 图1

图 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.4.3 自绘控件 - 图2

图 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的资源体系整合,开发成本较高,并不能取代原生控件的作用。