玩转Android Camera开发(三):国内首发—使用GLSurfaceView预览Camera 基础拍照demo

GLSurfaceView是OpenGL中的一个类,也是可以预览Camera的,而且在预览Camera上有其独到之处。独到之处在哪?当使用Surfaceview无能为力、痛不欲生时就只有使用GLSurfaceView了,它能够真正做到让Camera的数据和显示分离,所以搞明白了这个,像Camera只开预览不显示这都是小菜,妥妥的。Android4.0的自带Camera源码是用SurfaceView预览的,但到了4.2就换成了GLSurfaceView来预览。如今到了4.4又用了自家的TextureView,所以从中可以窥探出新增TextureView的用意。 虽说Android4.2的Camera源码是用GLSurfaceView预览的,但是进行了大量的封装又封装的,由于是OpenGL小白,真是看的不知所云。俺滴要求不高,只想弄个可拍照的摸清GLSurfaceView在预览Camera上的使用流程。经过一番百度一无所获,后来翻出去Google一大圈也没发现可用的。倒是很多人都在用GLSurfaceView和Surfaceview同时预览Camera,Surfaceview用来预览数据,在上面又铺了一层GLSurfaceView绘制一些信息。无奈自己摸索,整出来的是能拍照也能得到数据,但是界面上不是一块白板就是一块黑板啥都不显示。后来在stackoverflow终于找到了一个可用的链接,哈哈,苍天啊,终于柳暗花明了!参考此链接,自己又改改摸索了一天才彻底搞定。之所以费这么多时间是不明白OpenGL ES2.0的绘制基本流程,跟简单的OpenGL的绘制还是稍有区别。下面上源码: 一、CameraGLSurfaceView.java 此类继承GLSurfaceView,并实现了两个接口 **[java]** [view plain](http://blog.csdn.net/yanzi1225627/article/details/33339965#)[copy](http://blog.csdn.net/yanzi1225627/article/details/33339965#)[print](http://blog.csdn.net/yanzi1225627/article/details/33339965#)[?](http://blog.csdn.net/yanzi1225627/article/details/33339965#)[![在CODE上查看代码片](https://code.csdn.net/assets/CODE_ico.png)](https://code.csdn.net/snippets/402612)[![派生到我的代码片](https://code.csdn.net/assets/ico_fork.svg)](https://code.csdn.net/snippets/402612/fork) <div> </div> </div> - <span style="color: black;"><span style=<span class="string" style="color: red;">&#8220;font-family:Comic Sans MS;font-size:18px;&#8221;</span>><span class="keyword" style="font-weight: bold; color: blue;">package</span> org.yanzi.camera.preview; </span> - <span style="color: black;"> </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> javax.microedition.khronos.egl.EGLConfig; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> javax.microedition.khronos.opengles.GL10; </span> - <span style="color: black;"> </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> org.yanzi.camera.CameraInterface; </span> - <span style="color: black;"> </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.content.Context; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.graphics.SurfaceTexture; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.opengl.GLES11Ext; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.opengl.GLES20; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.opengl.GLSurfaceView; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.opengl.GLSurfaceView.Renderer; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.util.AttributeSet; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.util.Log; </span> - <span style="color: black;"> </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">public</span> <span class="keyword" style="font-weight: bold; color: blue;">class</span> CameraGLSurfaceView <span class="keyword" style="font-weight: bold; color: blue;">extends</span> GLSurfaceView <span class="keyword" style="font-weight: bold; color: blue;">implements</span> Renderer, SurfaceTexture.OnFrameAvailableListener { </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">private</span> <span class="keyword" style="font-weight: bold; color: blue;">static</span> <span class="keyword" style="font-weight: bold; color: blue;">final</span> String TAG = <span class="string" style="color: red;">&#8220;yanzi&#8221;</span>; </span> - <span style="color: black;"> Context mContext; </span> - <span style="color: black;"> SurfaceTexture mSurface; </span> - <span style="color: black;"> DirectDrawer mDirectDrawer; </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">public</span> CameraGLSurfaceView(Context context, AttributeSet attrs) { </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">super</span>(context, attrs); </span> - <span style="color: black;"> <span class="comment" style="color: #008200;">// TODO Auto-generated constructor stub</span> </span> - <span style="color: black;"> mContext = context; </span> - <span style="color: black;"> setEGLContextClientVersion(<span class="number" style="color: #c00000;">2</span>); </span> - <span style="color: black;"> setRenderer(<span class="keyword" style="font-weight: bold; color: blue;">this</span>); </span> - <span style="color: black;"> setRenderMode(RENDERMODE_WHEN_DIRTY); </span> - <span style="color: black;"> } </span> - <span style="color: black;"> <span class="annotation" style="color: #646464;">@Override</span> </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">public</span> <span class="keyword" style="font-weight: bold; color: blue;">void</span> onSurfaceCreated(GL10 gl, EGLConfig config) { </span> - <span style="color: black;"> <span class="comment" style="color: #008200;">// TODO Auto-generated method stub</span> </span> - <span style="color: black;"> Log.i(TAG, <span class="string" style="color: red;">&#8220;onSurfaceCreated&#8230;&#8221;</span>); </span> - <span style="color: black;"> mTextureID = createTextureID(); </span> - <span style="color: black;"> mSurface = <span class="keyword" style="font-weight: bold; color: blue;">new</span> SurfaceTexture(mTextureID); </span> - <span style="color: black;"> mSurface.setOnFrameAvailableListener(<span class="keyword" style="font-weight: bold; color: blue;">this</span>); </span> - <span style="color: black;"> mDirectDrawer = <span class="keyword" style="font-weight: bold; color: blue;">new</span> DirectDrawer(mTextureID); </span> - <span style="color: black;"> CameraInterface.getInstance().doOpenCamera(<span class="keyword" style="font-weight: bold; color: blue;">null</span>); </span> - <span style="color: black;"> </span> - <span style="color: black;"> } </span> - <span style="color: black;"> <span class="annotation" style="color: #646464;">@Override</span> </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">public</span> <span class="keyword" style="font-weight: bold; color: blue;">void</span> onSurfaceChanged(GL10 gl, <span class="keyword" style="font-weight: bold; color: blue;">int</span> width, <span class="keyword" style="font-weight: bold; color: blue;">int</span> height) { </span> - <span style="color: black;"> <span class="comment" style="color: #008200;">// TODO Auto-generated method stub</span> </span> - <span style="color: black;"> Log.i(TAG, <span class="string" style="color: red;">&#8220;onSurfaceChanged&#8230;&#8221;</span>); </span> - <span style="color: black;"> GLES20.glViewport(<span class="number" style="color: #c00000;"></span>, <span class="number" style="color: #c00000;"></span>, width, height); </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">if</span>(!CameraInterface.getInstance().isPreviewing()){ </span> - <span style="color: black;"> CameraInterface.getInstance().doStartPreview(mSurface, <span class="number" style="color: #c00000;">1</span>.33f); </span> - <span style="color: black;"> } </span> - <span style="color: black;"> </span> - <span style="color: black;"> </span> - <span style="color: black;"> } </span> - <span style="color: black;"> <span class="annotation" style="color: #646464;">@Override</span> </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">public</span> <span class="keyword" style="font-weight: bold; color: blue;">void</span> onDrawFrame(GL10 gl) { </span> - <span style="color: black;"> <span class="comment" style="color: #008200;">// TODO Auto-generated method stub</span> </span> - <span style="color: black;"> Log.i(TAG, <span class="string" style="color: red;">&#8220;onDrawFrame&#8230;&#8221;</span>); </span> - <span style="color: black;"> GLES20.glClearColor(<span class="number" style="color: #c00000;">1</span>.0f, <span class="number" style="color: #c00000;">1</span>.0f, <span class="number" style="color: #c00000;">1</span>.0f, <span class="number" style="color: #c00000;">1</span>.0f); </span> - <span style="color: black;"> GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); </span> - <span style="color: black;"> mSurface.updateTexImage(); </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">float</span>[] mtx = <span class="keyword" style="font-weight: bold; color: blue;">new</span> <span class="keyword" style="font-weight: bold; color: blue;">float</span>[<span class="number" style="color: #c00000;">16</span>]; </span> - <span style="color: black;"> mSurface.getTransformMatrix(mtx); </span> - <span style="color: black;"> mDirectDrawer.draw(mtx); </span> - <span style="color: black;"> } </span> - <span style="color: black;"> </span> - <span style="color: black;"> <span class="annotation" style="color: #646464;">@Override</span> </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">public</span> <span class="keyword" style="font-weight: bold; color: blue;">void</span> onPause() { </span> - <span style="color: black;"> <span class="comment" style="color: #008200;">// TODO Auto-generated method stub</span> </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">super</span>.onPause(); </span> - <span style="color: black;"> CameraInterface.getInstance().doStopCamera(); </span> - <span style="color: black;"> } </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">private</span> <span class="keyword" style="font-weight: bold; color: blue;">int</span> createTextureID() </span> - <span style="color: black;"> { </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">int</span>[] texture = <span class="keyword" style="font-weight: bold; color: blue;">new</span> <span class="keyword" style="font-weight: bold; color: blue;">int</span>[<span class="number" style="color: #c00000;">1</span>]; </span> - <span style="color: black;"> </span> - <span style="color: black;"> GLES20.glGenTextures(<span class="number" style="color: #c00000;">1</span>, texture, <span class="number" style="color: #c00000;"></span>); </span> - <span style="color: black;"> GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[<span class="number" style="color: #c00000;"></span>]); </span> - <span style="color: black;"> GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, </span> - <span style="color: black;"> GL10.GL_TEXTURE_MIN_FILTER,GL10.GL_LINEAR); </span> - <span style="color: black;"> GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, </span> - <span style="color: black;"> GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); </span> - <span style="color: black;"> GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, </span> - <span style="color: black;"> GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); </span> - <span style="color: black;"> GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, </span> - <span style="color: black;"> GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); </span> - <span style="color: black;"> </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">return</span> texture[<span class="number" style="color: #c00000;"></span>]; </span> - <span style="color: black;"> } </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">public</span> SurfaceTexture _getSurfaceTexture(){ </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">return</span> mSurface; </span> - <span style="color: black;"> } </span> - <span style="color: black;"> <span class="annotation" style="color: #646464;">@Override</span> </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">public</span> <span class="keyword" style="font-weight: bold; color: blue;">void</span> onFrameAvailable(SurfaceTexture surfaceTexture) { </span> - <span style="color: black;"> <span class="comment" style="color: #008200;">// TODO Auto-generated method stub</span> </span> - <span style="color: black;"> Log.i(TAG, <span class="string" style="color: red;">&#8220;onFrameAvailable&#8230;&#8221;</span>); </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">this</span>.requestRender(); </span> - <span style="color: black;"> } </span> - <span style="color: black;"> </span> - <span style="color: black;">} </span> - <span style="color: black;"></span> </span> 关于这个类进行简单说明: ...

2014年10月27日 · 16 分钟 · 天边的星星

玩转Android Camera开发(四):预览界面四周暗中间亮,只拍摄矩形区域图片(附完整源码)

杂家前文曾写过一篇关于只拍摄特定区域图片的demo,只是比较简陋,在坐标的换算上不是很严谨,而且没有完成预览界面四周暗中间亮的效果,深以为憾,今天把这个补齐了。 在上代码之前首先交代下,这里面存在着换算的两种模式。第一种,是以屏幕上的矩形区域为基准进行换算。举个例子,屏幕中间一个 矩形框为100dip*100dip.这里一定要使用dip为单位,否则在不同的手机上屏幕呈现的矩形框大小不一样。先将这个dip换算成px,然后根据屏幕的宽和高的像素计算出矩形区域,传给Surfaceview上铺的一层View,这里叫MaskView(蒙板),让MaskView进行绘制。然后拍照时,通过屏幕矩形框的大小和屏幕的大小与最终拍摄图片的PictureSize进行换算,得到图片里的矩形区域图片,然后截取保存。第二种模式是,预先知道想要的图片的长宽,如我就是想截400*400(单位为px)大小的图片。那就以此为基准,换算出屏幕上呈现的Rect的长宽,然后让MaskView绘制。究竟用哪一种模式,按需选择。本文以第一种模式示例。下面上代码: 在杂家的前文基础上进行封装,首先封装一个MaskView,用来绘制四周暗中间亮的效果,或者你可以加一个滚动条,这都不是事。 一、MaskView.java **[java]** [view plain](http://blog.csdn.net/yanzi1225627/article/details/34931759#)[copy](http://blog.csdn.net/yanzi1225627/article/details/34931759#)[print](http://blog.csdn.net/yanzi1225627/article/details/34931759#)[?](http://blog.csdn.net/yanzi1225627/article/details/34931759#)[![在CODE上查看代码片](https://code.csdn.net/assets/CODE_ico.png)](https://code.csdn.net/snippets/407657)[![派生到我的代码片](https://code.csdn.net/assets/ico_fork.svg)](https://code.csdn.net/snippets/407657/fork) <div> </div> </div> - <span style="color: black;"><span style=<span class="string" style="color: red;">&#8220;font-family:Comic Sans MS;font-size:18px;&#8221;</span>><span class="keyword" style="font-weight: bold; color: blue;">package</span> org.yanzi.ui; </span> - <span style="color: black;"> </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> org.yanzi.util.DisplayUtil; </span> - <span style="color: black;"> </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.content.Context; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.graphics.Canvas; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.graphics.Color; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.graphics.Paint; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.graphics.Paint.Style; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.graphics.Point; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.graphics.Rect; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.util.AttributeSet; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.util.Log; </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">import</span> android.widget.ImageView; </span> - <span style="color: black;"> </span> - <span style="color: black;"><span class="keyword" style="font-weight: bold; color: blue;">public</span> <span class="keyword" style="font-weight: bold; color: blue;">class</span> MaskView <span class="keyword" style="font-weight: bold; color: blue;">extends</span> ImageView { </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">private</span> <span class="keyword" style="font-weight: bold; color: blue;">static</span> <span class="keyword" style="font-weight: bold; color: blue;">final</span> String TAG = <span class="string" style="color: red;">&#8220;YanZi&#8221;</span>; </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">private</span> Paint mLinePaint; </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">private</span> Paint mAreaPaint; </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">private</span> Rect mCenterRect = <span class="keyword" style="font-weight: bold; color: blue;">null</span>; </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">private</span> Context mContext; </span> - <span style="color: black;"> </span> - <span style="color: black;"> </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">public</span> MaskView(Context context, AttributeSet attrs) { </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">super</span>(context, attrs); </span> - <span style="color: black;"> <span class="comment" style="color: #008200;">// TODO Auto-generated constructor stub</span> </span> - <span style="color: black;"> initPaint(); </span> - <span style="color: black;"> mContext = context; </span> - <span style="color: black;"> Point p = DisplayUtil.getScreenMetrics(mContext); </span> - <span style="color: black;"> widthScreen = p.x; </span> - <span style="color: black;"> heightScreen = p.y; </span> - <span style="color: black;"> } </span> - <span style="color: black;"> </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">private</span> <span class="keyword" style="font-weight: bold; color: blue;">void</span> initPaint(){ </span> - <span style="color: black;"> <span class="comment" style="color: #008200;">//绘制中间透明区域矩形边界的Paint</span> </span> - <span style="color: black;"> mLinePaint = <span class="keyword" style="font-weight: bold; color: blue;">new</span> Paint(Paint.ANTI_ALIAS_FLAG); </span> - <span style="color: black;"> mLinePaint.setColor(Color.BLUE); </span> - <span style="color: black;"> mLinePaint.setStyle(Style.STROKE); </span> - <span style="color: black;"> mLinePaint.setStrokeWidth(5f); </span> - <span style="color: black;"> mLinePaint.setAlpha(<span class="number" style="color: #c00000;">30</span>); </span> - <span style="color: black;"> </span> - <span style="color: black;"> <span class="comment" style="color: #008200;">//绘制四周阴影区域</span> </span> - <span style="color: black;"> mAreaPaint = <span class="keyword" style="font-weight: bold; color: blue;">new</span> Paint(Paint.ANTI_ALIAS_FLAG); </span> - <span style="color: black;"> mAreaPaint.setColor(Color.GRAY); </span> - <span style="color: black;"> mAreaPaint.setStyle(Style.FILL); </span> - <span style="color: black;"> mAreaPaint.setAlpha(<span class="number" style="color: #c00000;">180</span>); </span> - <span style="color: black;"> </span> - <span style="color: black;"> </span> - <span style="color: black;"> </span> - <span style="color: black;"> } </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">public</span> <span class="keyword" style="font-weight: bold; color: blue;">void</span> setCenterRect(Rect r){ </span> - <span style="color: black;"> Log.i(TAG, <span class="string" style="color: red;">&#8220;setCenterRect&#8230;&#8221;</span>); </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">this</span>.mCenterRect = r; </span> - <span style="color: black;"> postInvalidate(); </span> - <span style="color: black;"> } </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">public</span> <span class="keyword" style="font-weight: bold; color: blue;">void</span> clearCenterRect(Rect r){ </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">this</span>.mCenterRect = <span class="keyword" style="font-weight: bold; color: blue;">null</span>; </span> - <span style="color: black;"> } </span> - <span style="color: black;"> </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">int</span> widthScreen, heightScreen; </span> - <span style="color: black;"> <span class="annotation" style="color: #646464;">@Override</span> </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">protected</span> <span class="keyword" style="font-weight: bold; color: blue;">void</span> onDraw(Canvas canvas) { </span> - <span style="color: black;"> <span class="comment" style="color: #008200;">// TODO Auto-generated method stub</span> </span> - <span style="color: black;"> Log.i(TAG, <span class="string" style="color: red;">&#8220;onDraw&#8230;&#8221;</span>); </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">if</span>(mCenterRect == <span class="keyword" style="font-weight: bold; color: blue;">null</span>) </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">return</span>; </span> - <span style="color: black;"> <span class="comment" style="color: #008200;">//绘制四周阴影区域</span> </span> - <span style="color: black;"> canvas.drawRect(<span class="number" style="color: #c00000;"></span>, <span class="number" style="color: #c00000;"></span>, widthScreen, mCenterRect.top, mAreaPaint); </span> - <span style="color: black;"> canvas.drawRect(<span class="number" style="color: #c00000;"></span>, mCenterRect.bottom + <span class="number" style="color: #c00000;">1</span>, widthScreen, heightScreen, mAreaPaint); </span> - <span style="color: black;"> canvas.drawRect(<span class="number" style="color: #c00000;"></span>, mCenterRect.top, mCenterRect.left &#8211; <span class="number" style="color: #c00000;">1</span>, mCenterRect.bottom + <span class="number" style="color: #c00000;">1</span>, mAreaPaint); </span> - <span style="color: black;"> canvas.drawRect(mCenterRect.right + <span class="number" style="color: #c00000;">1</span>, mCenterRect.top, widthScreen, mCenterRect.bottom + <span class="number" style="color: #c00000;">1</span>, mAreaPaint); </span> - <span style="color: black;"> </span> - <span style="color: black;"> <span class="comment" style="color: #008200;">//绘制目标透明区域</span> </span> - <span style="color: black;"> canvas.drawRect(mCenterRect, mLinePaint); </span> - <span style="color: black;"> <span class="keyword" style="font-weight: bold; color: blue;">super</span>.onDraw(canvas); </span> - <span style="color: black;"> } </span> - <span style="color: black;"> </span> - <span style="color: black;"> </span> - <span style="color: black;"> </span> - <span style="color: black;">} </span> - <span style="color: black;"></span> </span> 说明如下: ...

2014年10月27日 · 16 分钟 · 天边的星星

Android 工具包 xUtils

xUtils简介 xUtils 包含了很多实用的android工具。 xUtils 最初源于Afinal框架,进行了大量重构,使得xUtils支持大文件上传,更全面的http请求协议支持(10种谓词),拥有更加灵活的ORM,更多的事件注解支持且不受混淆影响… xUitls最低兼容android 2.2 (api level 8) 目前xUtils主要有四大模块: DbUtils模块: - android中的orm框架,一行代码就可以进行增删改查; - 支持事务,默认关闭; - 可通过注解自定义表名,列名,外键,唯一性约束,NOT NULL约束,CHECK约束等(需要混淆的时候请注解表名和列名); - 支持绑定外键,保存实体时外键关联实体自动保存或更新; - 自动加载外键关联实体,支持延时加载; - 支持链式表达查询,更直观的查询语义,参考下面的介绍或sample中的例子。 ViewUtils模块: - android中的ioc框架,完全注解方式就可以进行UI,资源和事件绑定; - 新的事件绑定方式,使用混淆工具混淆后仍可正常工作; - 目前支持常用的20种事件绑定,参见ViewCommonEventListener类和包com.lidroid.xutils.view.annotation.event。 HttpUtils模块: - 支持同步,异步方式的请求; - 支持大文件上传,上传大文件不会oom; - 支持GET,POST,PUT,MOVE,COPY,DELETE,HEAD,OPTIONS,TRACE,CONNECT请求; - 下载支持301/302重定向,支持设置是否根据Content-Disposition重命名下载的文件; - 返回文本内容的请求(默认只启用了GET请求)支持缓存,可设置默认过期时间和针对当前请求的过期时间。 BitmapUtils模块: - 加载bitmap的时候无需考虑bitmap加载过程中出现的oom和android容器快速滑动时候出现的图片错位等现象; - 支持加载网络图片和本地图片; - 内存管理使用lru算法,更好的管理bitmap内存; - 可配置线程加载线程数量,缓存大小,缓存路径,加载显示动画等&#8230; 使用xUtils快速开发框架需要有以下权限: ``` <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ``` 混淆时注意事项: 添加Android默认混淆配置${sdk.dir}/tools/proguard/proguard-android.txt 不要混淆xUtils中的注解类型,添加混淆配置:-keep class * extends java.lang.annotation.Annotation { *; } 对使用DbUtils模块持久化的实体类不要混淆,或者注解所有表和列名称@Table(name=”xxx”),@Id(column=”xxx”),@Column(column=”xxx”),@Foreign(column=”xxx”,foreign=”xxx”);

2014年10月24日 · 1 分钟 · 天边的星星

Android 快速开发框架:ThinkAndroid

ThinkAndroid是包含Android mvc和简易sqlite orm以及ioc模块,它封装了Android httpclitent中的http模块, 具有快速构建文件缓存功能,无需考虑什么格式的文件,都可以非常轻松的实现缓存,它实现了图片缓存,在android中 加载的图片的时候oom的问题和快速滑动的时候图片加载位置错位等问题都可以轻易的解决掉。他还包括了一个手机开发中 经常应用的实用工具类,如日志管理,配置文件管理,android下载器模块,网络切换检测等等工具。 ThinkAndroid的开发宗旨是简洁,快速的进行Android应用程序的开发 目前ThinkAndroid主要有以下模块: MVC模块:实现视图与模型的分离。 ioc模块:android中的ioc模块,完全注解方式就可以进行UI绑定、res中的资源的读取、以及对象的初始化。 数据库模块:android中的orm框架,使用了线程池对sqlite进行操作。 http模块:通过httpclient进行封装http数据请求,支持异步及同步方式加载。 缓存模块:通过简单的配置及设计可以很好的实现缓存,对缓存可以随意的配置 图片缓存模块:imageview加载图片的时候无需考虑图片加载过程中出现的oom和android容器快速滑动时候出现的图片错位等现象。 配置器模块:可以对简易的实现配对配置的操作,目前配置文件可以支持Preference、Properties对配置进行存取。 日志打印模块:可以较快的轻易的是实现日志打印,支持日志打印的扩展,目前支持对sdcard写入本地打印、以及控制台打印 下载器模块:可以简单的实现多线程下载、后台下载、断点续传、对下载进行控制、如开始、暂停、删除等等。 网络状态检测模块:当网络状态改变时,对其进行检测。 github项目地址:https://github.com/white-cat/ThinkAndroid 其他框架 一.框架如下几种: 1.Roboguice 2.Spring for Android 3.afinal 4.xUtils 二.Roboguice说明 项目地址:https://github.com/roboguice/roboguice 要依赖三个包,加起来接近800K比较大; 控件和service都可以用IOC注入; 事件不能绑定 activity要继承RoboActivity 三.Spring for Android说明 四.afinal说明 - 项目地址:[https://github.com/yangfuhai/afinal](https://github.com/yangfuhai/afinal) - 依赖包只有152k - 页面控件可以注入,service不可注入 - 事件能绑定 - 提供sqlite,http,图片工具类 - activity要继承FinalActivity 五.xUtils说明 - 项目地址:[https://github.com/wyouflf/xUtils](https://github.com/wyouflf/xUtils) - 依赖包有274k,项目比较活跃 - 页面控件可以注入,service不可注入 - 事件能绑定 - 提供sqlite,http,图片工具类 - activity不要继承,但要侵入代码 - 是afinal项目改进而来,支持大数据上传 &nbsp; ion 项目地址:https://github.com/koush/ion

2014年10月24日 · 1 分钟 · 天边的星星

Android 网络开发框架的选择

在看android基础的时候,关于网络操作一般都会介绍HttpClient以及HttpConnection这两个包。前者是apache的开源库,后者是android自带的api。既然提到了他们,都二者进行一个比较,谷歌在官方文档已经说明了,建议在2.3以及以上版本使用HttpConnection。具体原因呢,是因为对2.1和2.2版本,HttpURLConnection有那么几个Bug,所以建议用Apache的HTTP Client;之后的版本,建议用HttpURLConnection。Apache的HTTP Client比较强大,拥有庞大而灵活的API,这个实现很稳定,并且Bug很少。然而,也就是因为太庞大了,以至于很难在保证兼容性的情况下改进它,故android 开发团队不应该维护该库而是转投更为轻量级的httpurlconnection。 当我们开发企业级应用的时候,一般都会选择使用已经封装好的http框架。开源的比较流行的有: 1、volley 2、android-async-http 3、retrofit 4、okhttp 5、androidquery 6、AndroidAsync 等。他们各有优劣,不同的框架有不同的效率,在使用的时候可以因地制宜地测试,根据效果来选择使用哪个,之前个人则比较喜欢用android-async-http,。如今Google推出了官方的针对Android平台上的网络通信库volley,能使网络通信更快,更简单,更健壮,Volley在提供了高性能网络通讯功能的同时,对网络图片加载也提供了良好的支持,完全可以满足简单REST客户端的需求, 我们没有理由不跟上时代的潮流。另外,但volley的扩展性很强,可以根据需要定制你自己的网络请求。所以,最后推荐还是使用volley进行开发,当然其他几个库也是非常具有学习以及参考意义的,可以将他们的精髓之处汲取到volley框架的拓展开发之中,做出自己理想的http通讯框架。 推荐博客: http://instructure.github.io/blog/2013/12/09/volley-vs-retrofit/ http://blog.csdn.net/t12x3456/article/details/9221611 http://blog.csdn.net/guolin_blog/article/details/12452307

2014年10月24日 · 1 分钟 · 天边的星星

Android敏捷开发指南(下)

本文延续上期话题,深入到测试、持续集成和部署等环节,紧密结合移动开发方法和技术,围绕Android平台的开发讨论提供更高质量移动产品的解决方案。 **通过清晰的架构实现测试驱动** 通过[《程序员》杂志9月刊文章](http://www.programmer.com.cn/13757/)的分析,我们可以看到,每一种工具都很难从完全意义上解决工程当中追求快速和高质量的要求。那么就需要通过整体架构实践,更好地解决这方面的问题。以下两种结构方法可供参考。 **平台和领域的分层结构** 如图1所示,这是一种最基础的分层结构。它将和Android平台相关的内容都包裹在了平台层,而将领域、状态、逻辑等和平台无关的内容完全隔离开来。创建项目时,将平台层和测试层分别对领域层产生依赖,以这样的方式达成对项目和对测试的独立构建。 这样做的好处是,领域层可以通过Java的JUnit进行完全测试,而项目各个类之间的因为有明显的项目级别分层,使得代码的考量更加清晰,方便于检验最核心关键的业务逻辑。且测试速度非常快,便于快速迭代。 但其带来的不便之处就是,随着项目代码的日益复杂,其与Android内部结合就越紧密,这时往往越难将业务逻辑很清晰的剥离。部分的业务逻辑将被分片散落在一些平台相关的调用当中。因此我们认为这样的一种层次逻辑更多地适用于简单的项目之中。 ![](http://ipad-cms.csdn.net/cms/attachment/201210/5061286bc38e9.jpg) 图1 平台和领域的分层结构 **MVP(Passive View)架构** 许多文章对Passive View是属于MVP还是MVC有各种不同角度的争论。这里我并不想过多地在词语上做过多纠缠,主要是希望通过描述这样的一种结构来树立比较好的对于Android开发的架构模型。 具体到Passive View的实现,如图2所示,描述了一个基本的原理。 随着项目的不断扩大复杂化,我们需要更加清晰化的代码架构。所以,Passive View对平台和领域的分层结构的层次结构有了更深层次的改进。引入Passive View模式的意义实际上还是从测试出发。出于对JUnit快速性能的青睐,所以依然尽量把层次划分为依赖Android内核的部分和非依赖部分。对于前 半部分,可以用基于Roboletric的上层测试完成,相对应的后半部分则可以使用标准Java进行单元测试。这样做既加快了测试速度,又得以保障测试的覆盖率。 ![](http://ipad-cms.csdn.net/cms/attachment/201210/5061290a0cd3f.jpg) 图2 Passive View架构实现原理图 更多详细的架构资料,请大家参考《GUI Architecture》一文,相信会得到许多很好的想法。 **测试驱动开发的实现** 之所以要强调代码框架结构,最重要的原因就是寻找快捷方便的途径进行TDD开发。在上一篇文章中已提到了Android常见的集中单元测试工具。之所以将框 架设计方式引入测试驱动开发过程,目的是将应用代码分开处理、分而治之。最大限度让各工具间扬长避短。最终实现高效率测试驱动的目的。 事实上,在目前的开发领域,我们也看到了许多正在应运而生的开发框架,例如Android Spring框架。这些都是下一步需要努力和完善的目标。 **业务行为驱动的功能测试方案** 如果说单元测试驱动开发帮助我们从开发角度完成了对代码质量的控制,那么基于业务行为的功能测试则为从业务到实现的统一、软件质量的保障提供了重要的解决方法。 功能(验收)测试:就功能测试而言,所指基本上就是测试科目中常提到的黑盒测试。只不过这里加上了更多具有上下文的信息,使得一次完整的黑盒测试更具有实质上的功能意义。 在 Android的开发中,通常是模拟真机或模拟器的用户操作(例如点击或滑动),来检测操作的结果是否符合预期。这个过程可以很好地代替人工的操作,更快 速和大量完成重复性的测试工作,特别是在发展较快、平台和系统版本覆盖较广的项目中,可以起到节约人力成本、提高测试覆盖与准确性的作用。 在常用的工具中,可以选取上一篇文章中介绍过的Robotium或Native Driver来实现。 前者社区发展速度快,实现功能相对更加完善,例如实现了一些手势操作效果等。不过问题是Robotium的组织目前正在倾向闭源化,有些最好的新功能已无法在开源版本中寻找到。 后者的原理是通过植入应用后门的方式,实现对应用的发出指令和进行资源获取。其物理模型比较适合不能再目标设备安装测试应用,或者希望远程控制的情况。开源状况良好,只是近一年,Native Driver的团队把重点发在其他平台上,对Android的更新比较慢。 基于场景的的跨平台验收测试:基于场景的验收测试,好处是可以在业务人员和开发人员之间建立验收机制。通过一系列关于场景的自然语言描述,完成测试的原始编 码。Cucumber是一个在行为驱动开发(BDD)领域比较常见的工具。其基本方法是通过执行文字形功能描述语言来实现对软件的自动化测试。图3展示了 一个常见的Cucumber测试场景。在Android开发中,通常是解析Cucumber,根据内容分情况拆分正具体的操作步骤。将这些步骤用基于 NativeDriver的Java代码进行实现,通过NativeDriver从指令端到设备端的Android自动测试框架完成控制与校验的自动化测 试。 ![](http://ipad-cms.csdn.net/cms/attachment/201210/50612a3536eaf.jpg) 图3 常见的Cucumber测试场景 当然,这些步骤也可以通过Robotium的远程控制(RC)调用来实现。其原理类似,如图4所示。 该方案还有额外的好处—用同样的方法,通过Cucumber可以实现对UIAutomation等其他平台自动化测试工具的控制。实现一份用例,多个平台的测试解决方案。图5给出了一个iOS平台上的基于Cucumber的跨平台测试实例。 ![](http://ipad-cms.csdn.net/cms/attachment/201210/50612a618a4e3.jpg) 图4 Cucumber测试实例 ![](http://ipad-cms.csdn.net/cms/attachment/201210/50612aa72955d.jpg) 图5 iOS平台上基于Cucumber的跨平台测试实例 **从持续集成到部署** **Jenkins最基本的持续集成** 在做好了各种测试实践后,需要考虑如何保证这种实践在每一次提交都产生效果,并且提供对代码库的不断保护。这里引入常用敏捷实践中持续集成的概念。 持 续集成主要是为了能够更好为产品在整个开发过程中提供交付保障。相对比传统服务器领域,移动开发中,碰到最多的问题是环境的不同与测试包的相对封闭。以前 通过改一个配置实现服务器实时测试修改的方法,很难直接应用于移动开发。此外,作为整个服务边缘的移动应用,往往会遇到大量不同的测试环境。 所以如图6所描述的,需要在持续集成服务器上部署相关的应用包,完成对不同环境的开发与测试。这样的测试通常是由持续集成服务器引导的不同物理移动设备来完成,以保证最终测试包的可交付性。尤其适用于对于网络和性能等相关的测试。 ![](http://ipad-cms.csdn.net/cms/attachment/201210/50612ae03a67e.jpg) 图6 利用相关应用包完成对不同环境的开发与测试 当然,如果细心的话,也可以看出图6中对于产品版本与配置管理的一些端倪。 **OTA自动化部署** 持续集成帮助我们有了稳定的应用包。有了这些包,我们现在就差最后的一步—自动化部署。在移动的真实交付中,由于安装包需要下载到移动终端完成。所以自动化部署也必须要模拟这一过程完成交付。 这里我们推荐使用持续集成服务器关于邮件服务推送的应用插件来完成。具体的过程通常为,在完成编译并顺利通过测试后,持续集成服务器会根据制定终端列表,推送测试内容以供安装。推送者收到请求后,可以根据需要自行安装相应apk文件,达到部署效果。 这样做打通了移动开发最后一步的障碍。并有效沟通了管理、设计、业务与测试的各个部门人员。持续集成管理员还可以通过管理安装邮件列表的形式,自行选择不同版本,环境安装包对不同相关人员的推送。 **让调试可持续化** 有 了开发,有了一系列持续化集成作业,有了作业完成的自动化打包部署,貌似可以休息了。但正如DevOps概念所说,虽然做了许多努力,但我们依然承认会有 错误的出现,那么如何尽早发现错误并准确定位,最终完成就显得尤为重要。在不同阶段,我们尝试使用不同的方法来进行调试。下面介绍我们的两个实践方法。 开发后调试。旧有的logcat是一个很好的调试工具,它在开发中起到了重要的作用,但对于做探索测试的QA们而言,大量繁琐的Debug信息往往令人看得眼花缭乱。 这 时我们就需要一个更好的工具进行错误栈的准确定位,因此我们引入了Acra。这是一个开源工具包,其功能是自动化地将Android应用的错误报告发送到 GoogleDoc或着Email等的可记录文本中。它的目的是为了帮助Android应用开发者在错误发生时收集行为和技术数据。 上线后的持续关注当然在上线后,Google Play也为我们提供了一个非常有好的平台用以收集各种来自用户的反馈信息。其中包括下载量、评价、应用异常报告等。 &nbsp; ![](http://ipad-cms.csdn.net/cms/attachment/201210/50612b2c496ca.jpg) &nbsp; &nbsp; ![](http://ipad-cms.csdn.net/cms/attachment/201210/50612b2d5d71a.jpg) &nbsp; ![](http://ipad-cms.csdn.net/cms/attachment/201210/50612b35d1eac.jpg) 图7 移动循环开发流程 在 应用异常报告中,详细记录了整个错误堆栈的信息,更包括错误信息的总数量、周发生次数,以及版本时间等信息。因为Android平台设备性能、具体型号等 多方面内容,并不一定所有的错误信息都一定去进行处理,但这个平台提供了一个持续关注上线情况的基础,为产品的紧急处理、故障修复和在开发提供了重要的信 息来源。 这里还要强调,测试优先的TDD实践,在每一个错误修复前,都应该把测试补好,这是同样错误不会再次出现的最重要保障。 **总结** 本文提出了移动开发过程中常见的问题,并分析了基于开发与维护过程中的一些具体实践方法,尝试从移动应用的开发技术角度提供解决方案。 除此之外,事实上和常见的软件开发一样,还有诸如配置管理、发布管理等也是必不可少的。我们总结了如图7所示的一个移动开发循环。通过对不同方面的配置技术实现,完成移动的全流程开发,供读者做更深的思考和方法演进。 转自:http://www.yidin.net/?p=6821

2014年10月24日 · 1 分钟 · 天边的星星

Android敏捷开发指南(上)

本文紧密结合移动开发方法与技术,围绕Android平台的开发探讨提供更高质量移动产品的解决方案。作者中分析了移动开发中常见的问题,从两方面阐述了ThoughtWorks使用的测试开发方案和相应的架构方法与常用工具应用,并进一步阐述了为移动开发流程所提供的持续发布方案。 随着云计算、移动互联等一系列新技术概念的崛起,新一轮的IT经济正在不断扩大发展。带来无限机遇的同时,也提出了许多有别于传统开发的挑战。近几年来,我一直在尝试各种移动项目,虽然它们在应用领域、技术类型以及工作模式等方面各不相同,但我在摸索中逐渐总结出了一些比较具有共性的问题。 移动项目中的常见问题 为了实现较好的用户体验,反复的设计与验证导 致产品发布时间延长。移动应用由于其多样性的应用场景,使产品设计侧重于适应不同目标的展现方式与操作习惯。设计实现的方式与使用用户群、企业服务模式、新的科技实现手段,以及各种碎片分化的目标支持设备等一系列因素密切相关。在产品实现初期,许多的内容和形式都需要在已有开发原型的基础上,进行紧密结合用户体验的测试。不少团队花了很长时间完成了目标,可测试后又要经历反复且大量的修改。这对产品的如期发布提出了巨大的挑战,发布时间也会因此一拖再拖。 即便是产品磕磕绊绊地发布了,设计的改变往往也是最让人头痛的问题。 ![](http://ipad-cms.csdn.net/cms/attachment/201209/503b37ab7f95a.jpg) 图1 某产品四个阶段首页面对比图 市场导向性强,业务需求变化快与缩短产品交付周期需求之间产生矛盾。经过了一系列的市场分析、产品设计、项目研发过程后,一个移动产品终于投放到了市场。但这完全不能看做是一个项目的交付完成,恰恰相反,这只是一个新阶段的开始。在残酷的市场竞争中,用户不是产品的被动消费者,而是需求的提出者,会在各种下 载市场(例App Store、Google Play)发出评论对应用进行评估,从而直接影响应用的市场占有率。同时随着一系列用户体验数据的收集分析和整理,业务部门的需求递增,新功能点开始一个 个被搬上研发经理的台面。这给开发团队应对需求改变以及对递增的代码结构升级能力提出了更高要求。图1展示了一个产品在过去一年多时间里首页面的变化。 可以看出在项目所经历的四个比较大的阶段中,仅一个首界面的功能,也经历了从最开始的普通列表界面,到后来增加地图功能、书签的过程。在第四个阶段中,为了满足用户注册登录等需求,首页面还进行了基于左侧滑动菜单的导航转型与登录反馈等的功能扩充。每个重大阶段的转型,都来自最真实的市场评论数据,并结合 Omniture(通过收集用户数据行为分析的工具)等产品的体验数据分析与业务增长需要进行开发。 为了实现更加精细的体验效果并兼容 Android的各个版本(例如图1中列表和地图间的切换需要通过动画三维翻转实现等),所有这一切都必须在同一个Activity内完成交互。这给大量的页面逻辑、状态和视图层级关系的升级改造带来了很大的难度。与此相对应的坏消息是,移动应用对短周期的快速发布有着强烈的需求。即使是一个好的应用,如 果没有及时保持稳定频率的更新,很快就会被接踵而来的竞争者追赶,最后落到被用户遗忘的境地。从某种角度讲,除了产品新功能的推出外,应用程序的更新也具 备某种广告的职能,去强化品牌在消费人群中的地位,稳定和扩大市场的占有率。 移动团队虽小,但要求更良好的产品集成性。同一个产品,一般根 据支持平台的数量以及并行发布需求,配置有多个小团队和数据整合(API)团队。每个团队的开发因为进度不同和平台特点的不同,往往在整合过程中提出各自不同的集成需求(包括数据集成和逻辑集成),例如Android的内存性能不好,要求服务端的图片质量与剪裁要和iOS有所区别;再例如有时为了降低网络 性能对体验的影响,会更改设计,将逻辑分散在API整合段和设备端。这就为团队间的整合埋下了风险。事实上,这在多团队的并行开发中,并不是个别现象。 多样化的设备和版本、长期的维护开发,带来快速升高的测试成本。随着开发功能的增加,页面布局、操作响应和交互处理大量逻辑模块增加,以及越来越分化的设备 和系统版本,给测试工作带来了相当大的难度。在之前我做咨询时,一个项目经理告诉我,他有一个运行了一年半的项目,当时还有一周就要上线,可仍有60个Bug,5名测试工程师因为对质量没有信心而不停加班,开发也为修改一个个错误都头痛不已。 通过技术方案寻求解决途径 为了解决上述常见移动项目的问题,在项目实践中,我们试图通过合理地运用技术方案来帮助完成高质量移动软件的目标。 实现高可维护性的代码,减少代码扩展过程中的腐化和变动带来的副作用。随着功能增加,越来越多的逻辑模块被堆砌在同一个单元内,导致代码可读性下降,维护复杂度提高。这时的修改都存在破坏原有功能的潜在风险。 如果解决这两个问题,将在很大程度上提高应用在开发过程中对产品需求改变和开发周期控制的适应能力。实践中,我们使用元素组件化开发和测试驱动开发解决方 案。组件化的基本目的就是将代码的可读性置于编码过程中,通过形成独立子元素组件来代理自身的功能逻辑,并以此结构化XML的布局资源,提高可读性。同 时,通过测试驱动的开发方式为代码的粒度质量提供原始保障,让代码演进过程减少编码副作用破坏其他功能的现象,从而提升代码的可维护性。 通过功能自动化测试,保证开发过程中测试成本的相对稳定和质量保障。这里要分两个部分来谈。第一部分是通过提高自动化测试的比例,减少由人工重复完成的测 试。在应对多平台、多版本、反复回归测试时,自动化测试对质量的保障就显得尤为重要;第二部分是由于对需求理解偏差,产生的质量问题和因此增加的返工。这 里就需要引入基于业务行为驱动开发(BDD)的自动化测试管理。让需求成为可验证的执行代码,将会巨大限度的缩小业务需求、开发和测试之间的鸿沟。 当然,我在从事多个项目开发咨询的过程中,也曾遇到大量的需求变更,导致自动化测试废弃,从而提高成本的案例。这时往往要注意协调自动化测试金字塔,即单元测试、功能测试、UI界面测试等几部分的比例关系,实现质量与成本的平衡。 让随时可工作的产品来提高团队的交付能力。面对不断变更的需求与任何时候都可能出现的产品延时,提高团队的整体交付能力,就显得格外的重要。作为重要一环的持续集成和可用多点环境下测试,便成为这其中不可或缺的重中之重。而如果产品的各个平台都可以保证相对稳定的持续集成与发布,那么这样的方案也自然成为消 除团队合作壁垒的重要技术保障。 加快用户体验验证周期,增加修改频率降低单次修改的调整规模。优秀的用户体验,永远是前端工程师最关心的部分。这就需要能够尽早地去做更深入的分析与验证,更快地发现问题,并接进行修改与调整。同时缩短反馈回到开发端的周期,将大大降低代码的修改难度,提高产品效率。而自动化的编译、集成与部署操作流程就是用一个非常有效的方法来完成闭环回路。可持续集成与部署技术为缩短周期提供了根本上的保证。 刚刚提到了许多用来移动项目问题的解决方案。那么下面就让我们来看看在Android开发领域,这些方案是如何具体实施的。 结构化组件 所谓组件化就是将应用内部的UI元素充分拆分成相互独立子部件(例如常见的Android的Widget组件)。大的子部件由多个小的子部件组成。 这样的好处是除了在代码层面上更易于修改外,同时也通过对类和方法的命名实现了XML代码的结构化。图2作为一个简单的示例,大致表现了图1中第四个阶段首页面的组成方式。可以看出XML代码中的每一个子组件都是一个相应的视图组件,这些组件通过Java类和XML的对应完成一个子视图功能。XML中大体可 以表达出页面视图的一个可读性组成方法,而对应的Java文件则代表了每个视图的生成细节模型和交互响应逻辑。 另外需要说明的是,我们曾尝试了多种消息传递机制。最后证明,组件的单一顺序传递是一种相对最稳定和易理解的传递方法,子视图的交互消息只能传递给其父视图,然后由父视图传递给其他 Have Fragances breakage [http://www.jaibharathcollege.com/cialis-samples.html](http://www.jaibharathcollege.com/cialis-samples.html) which product. Very they [cheap canadian viagra](http://www.lolajesse.com/cialis.html) good… Circles slightly [cialis buy](http://www.clinkevents.com/cialis-buy) rinsed spread download s, [cnadian viagra india](http://www.lolajesse.com/cialis-50-mg.html) over-priced swears is next. Product [canada viagra pharmacies scam](http://www.jaibharathcollege.com/canada-viagra-pharmacies-scam.html) Face never clothes [viagra without prescription clinkevents.com](http://www.clinkevents.com/viagra-online-sales) 50 arrived some Honest [canadian healthcare viagra](http://alcaco.com/jabs/canadian-healthcare-viagra.php) Continue inner keeping [http://transformingfinance.org.uk/bsz/vipps-pharmacies-viagra/](http://transformingfinance.org.uk/bsz/vipps-pharmacies-viagra/) the not? Of getting without [purchase levitra](http://transformingfinance.org.uk/bsz/purchase-levitra/) difference skin ! and [http://tietheknot.org/leq/can-i-buy-fluconazole-over-the-counter.html](http://tietheknot.org/leq/can-i-buy-fluconazole-over-the-counter.html) delivery the organic lemony casing [citaloprim without prescription spnam2013.org](http://spnam2013.org/rpx/citaloprim-without-prescription) out. Products likely flattering [motilium syrup](http://thegeminiproject.com.au/drd/motilium-syrup.php) like still disappearing [click](http://theater-anu.de/rgn/prednizone-sales/) labeling? Of as [buy generic cialis best price](http://www.allprodetail.com/kwf/buy-generic-cialis-best-price.php) upon pointed joints use. Toxic [asacol](http://spnam2013.org/rpx/asacol) but try not [doxycycline dosage for gonorrhea](http://www.adriamed.com.mk/ewf/doxycycline-dosage-for-gonorrhea) bothered commented this [e check pharmacy](http://theater-anu.de/rgn/e-check-pharmacy/) However alcohol [cheap meds](http://www.alanorr.co.uk/eaa/cheap-meds.php) Mango blonde miraculous growing them. something little. [1945mf-china.com viagra soft tabs 100 mg](http://www.1945mf-china.com/viagra-soft-tabs-100-mg/) me my greasy. I friend [rehabistanbul.com cialis tablets](http://www.rehabistanbul.com/cialis-tablets) if and in can [generic cialis canadian](http://www.1945mf-china.com/buy-cialis-where/) hydrated to s applicators. Crease [viagra next day delivery](http://alcaco.com/jabs/viagra-next-day-delivery.php) Average on holds would. After [http://www.rehabistanbul.com/viagra-100mg-england](http://www.rehabistanbul.com/viagra-100mg-england) out several good is… 子视图。即如图2中ContentRotableScreen接收到一个自己不能处理的响应事件,应该将消息传达给SlidableContainer, 再统一分发给需要处理事件的NavMenuScreen作相应的动作。 ![](http://ipad-cms.csdn.net/cms/attachment/201209/503b381343736.jpg) 图2 某产品四个阶段首页面的组成方式 这样做既保证了消息传递的准确性,又维护了一个统一的代码结构,方便实现代码的可读性和可维护性。 <span style="color: #3366ff;">**单元测试工具**</span> 单元测试是测试驱动开发的主体测试构成,旨在从代码粒度上实现对应用质量的把握,是可维护性代码的核心。其具体粒度大小取决于在代码出现问题后,能在多大程 度上准确定位问题。这也是单元测试最大的意义所在。这份意义所带来的是更高的代码可维护性、更稳定的代码可重构性、更便捷的可扩展性。而这一切为稳定结构 变化、减弱代码腐化影响、技术改进所带来的代码变更奠定了良好的基础。 为了实现比较良好的单元测试,需要一系列代码结构优化和测试工具使用的辅助。 在做深入说明Android系统开发结构之前,先来看一下在该平台开发时所常见的工具和相应的优缺点对比,如表1所示。 ![](http://ipad-cms.csdn.net/cms/attachment/201209/503b386b1f62f.jpg) 表1 各测试工具优缺点比较 根据现有经验,我们曾尝试了表1中四种单元测试方法。很难说哪一个方案是最好的,在目前的项目实践中,我们联合应用Robolectric和Java JUnit,为了避免Robolectric速度、模拟功能不全和质量检测工具等问题,需要对Robolectric的代码进行修改,并尽量减少对其的应 用。转而通过一些结构上的应用,大量采用Java JUnit测试。 **JUnit。**鼎鼎大名的Java测试框架,无数应运而生的mock框架支持,使其无论是可用性还是易用性方面在Java领域无人能及。利用JUnit可以实现非常快捷单元测试。也是最常见的一种单元测试形式。 **Robolectric。**这是由Pivotal Labs开发的一套开源的Android单元测试框架。其通过一系列对底层Android元素的替换来实现对原有元素调用的模拟,从而实现脱离模拟器的测 试。非常值得一提的是,在测试服务器请求时,Robolectric的数据模拟和延时发送模拟,给多线程状态下的测试提供了很好的解决方法。 **Robotium。** 因为其对整体应用的黑盒操作特性,绝大多数技术文章将其作为功能测试工具,因此后文在叙述功能测试时也有提及。但因为其已有代码库,进行测试前需要组合编 译,而且可以完成方法级别的功能测试,因此本文还是将其描述重点在单元测试时和其他工具进行对比说明。但并不等于它就是单元测试。 **Android JUnit。**这是一种最常见的单元级别测试。它由Android官方提供,通过虚拟机自身提供的测试接口完成。图3源于Android开发者官网,基本上 阐述了整个测试框架各个组成部分。其中,最下面方框中描述的即为框架中基于JUnit的测试部分。可以看到,其通过Android的内部测试包,调用测试 执行模块完成对目标应用的测试。 该测试最大的好处是其与Android系统结合紧密,贴近真实环境。但其弊端也正是因为使用大量基于平台的虚拟,导致测试运行速度相对偏慢,影响测试效率。又因为开发过程中,单元测试是最大,也是最常规的测试,所以这种影响带来的效率降低就显得特别严重。 ![](http://ipad-cms.csdn.net/cms/attachment/201209/5049558cb74a5.jpg) 图3 Android JUnit测试框架 **小结** 本文我们介绍了移动开发中的常见问题、技术解决方案的基本思路,以及在具体实现中所要涉及的结构化组件和单元测试工作。在[下期《程序员》](http://www.programmer.com.cn/13778/)中,我将继续讲解技术方案的具体实现方法,包括如何通过框架选取实现测试驱动方案,业务行为驱动的功能测试方案、持续集成、部署,以及如何让调试可持续化。 转自:http://www.programmer.com.cn/13757/

2014年10月24日 · 2 分钟 · 天边的星星

关于android webview里HTML5的地理位置定位,在别的浏览器中打开可以定位,在我自己的webview却不能

我也遇到了同样的问题,我查看了下源码解决了,主要代码如下: //启用数据库 webSettings.setDatabaseEnabled(true); String dir = this.getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath(); //启用地理定位 webSettings.setGeolocationEnabled(true); //设置定位的数据库路径 webSettings.setGeolocationDatabasePath(dir); //最重要的方法,一定要设置,这就是出不来的主要原因 webSettings.setDomStorageEnabled(true) //配置权限(同样在WebChromeClient中实现) public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { callback.invoke(origin, true, false); super.onGeolocationPermissionsShowPrompt(origin, callback); } 配置权限: &lt;uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /&gt; &lt;uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /&gt;

2014年10月19日 · 1 分钟 · 天边的星星

Android手机应用UI设计的10个建议

每个开发循环迟早到会走到应用几近完成这个点。接下来要做什么呢?你可曾听说过“用户体验”这个词?以下这10个技巧能够使新手机应用在发布前提升质量,最大化发掘该应用的潜力,从而最小化用户差评和低下载量这种不良结果。 **1、首次开启体验 ** 优秀的网站和手机应用有诸多相似之处。这两者都能够迅速吸引用户或访问者。如果没有做到这点,用户很可能会转而寻找其他替代品。多数用户不愿意浪费时间来弄清楚要如何运行应用或阅读复杂的教程。他们会选择放弃该应用。 首次开启应用时,每个人的脑中都会浮现出相同的3个问题:我在哪里?我现在能够做什么?我接下来能够做什么? 努力使应用立即对这些问题做出回答。如果你能够在前数秒的时间里告诉用户这是款适合他们的产品,那么他们势必会进行更深层次的发掘。 (Gowalla有着良好的首次开启体验。护照缓缓打开,让你可以立即查看个人信息、即时建议以及更多的动作和通知。) 2、便捷的输入方式 想想看你是如何使用手机设备的:开发者的手机安静地躺在平坦的桌面上,连接到配有大型键盘的PC上,或许还完全打开背光功能。现在,想想其他人如何使用他们的智能手机:走在熙熙攘攘的大街上,一手拿着杯咖啡,另一手拿着设备,努力弄清楚他们最喜欢球队的表现情况。 在多数时间里,人们只使用1个拇指来执行应用的导航。不要执拗于多点触摸以及类似的复杂输入方法,要多考虑滚动和触摸方式。让人们可以迅速地完成屏幕和信息间的切换和导航。让他们可以快速获得所需的信息,珍惜用户每次的输入操作。 (你只需要简单的触碰和输入文字就可以给Taskos应用添加新任务。当然,你还可以修改许多设置,但这些都只是可选操作。) 3、对比度 你的开发环境或许是有着大型屏幕且光照适当的房间,但用户使用应用的环境可能并非如此。尽管我们不愿意,但是我们确实常需要在阳光强烈的环境下使用手机设备。这种情况会对我们观 看屏幕产生很大的影响,界面设计时应当考虑到这点。在上述不佳条件下,可能会导致细节丢失,颜色分辨不清,某些元素因阳光反射而完全消失。 这并不意味着你只能将界面设计成黑白样式,抛弃UI设计中所有漂亮的细节。这仅仅意味着,重要元素应当有足够的对比度,使之在此类条件下能轻易识别。如果你想要给代码元素上色,那么要添加简单文字标签之类的选项。如果你想用小细节和信息来改善应用外观,这也是可以的,只是要确保你的UI没有这些元素时依然能够运转。 为界面设置清晰的等级,大而明亮地呈现最有价值的功能,将任何不重要的内容完全移除。 (虽然SoundHound的屏幕上有着许多选项,但是主要功能用明亮和加粗的字体清晰地呈现在界面顶部。) 4、不要让用户等待 没有人喜欢等待,在移动领域中尤其如此。我们将设备带上火车,在汽车上快速回复邮件,或者在走出屋子的时候查看天气预报。我们利用时间间隙来做这些小事情,来换取更多时间做真正喜欢做的事。不要让人们等待你的应用做某件事情。提升应用表现,改变UI,让用户所需结果的呈现变得更快。 当然,所有人都能够理解,有些任务需要花一定时间来执行,或者应用需要从网络上下载某些容量较大的数据包。但是不要让用户毫无意义地等待。要让他们感觉到任务正在执行中。为按键添加“选择”或“按动”的状态,加载时间较短时可以添加旋转符号,加载时间较长时可以使用进度条。但是,绝不要让用户面对空无一物的屏幕。 等待总是令人苦恼的。至少要让用户知道他们还需要等待多长时间。 (Google Reader应用在设备顶端显示一个小的旋转符号,每当应用在后台加载内容时这个符号就会出现,这样你就会意识到自己或许需要等待一段时间。) 5、不要忘记横向呈现方式 有时,你或许会忘记手机设备不只有单一的纵向呈现。虽然多数人能够适应只支持纵向模式的应用,但确实有某些人喜欢横向使用他们的设备,尤其是那些有着实体键盘的设备。随着Android平板电脑的流行,这类用户的数量可能会逐渐增加。 不要认为横向模式只需简单地加宽应用界面。横向使用设备有着完全不同的用户体验。在这种情况下,你可以用两个拇指与屏幕互动。输入变得更为简单,而且多数情况下你会由左向右阅读,不是由上向下。事实上,如果你的应用需要大量的阅读和文字输入,那么绝对要有良好的横向模式。 对用户来说,横向体验是完全不同的。你可以利用这种更宽的布局,以完全不同的方式呈现信息。比如,之前位于屏幕上方的按键可以移动到屏幕一侧。利用更宽的屏幕,地图、图表和图片可以呈现新的信息。 (先构建和改善一种屏幕方向,然后再制作另一种。注意每种布局的利弊,睿智地加以利用和改良。YouToube应用官方版本为不同的方向模式设计了不同的布局,两者都在各自的纵横比下完美地运转。) 6、应用生态系统 尽管你能够设计出为用户多种不同目标服务的独特应用,但它永远都只是整个动作系列的一个步骤。 想想看你的智能手机所具备的功能:电话记录、联系人、短信息、邮件、浏览器、拍摄照片和视频、GPS和地图等。利用这些功能。对于所有这些已构建的模块,你无需自行制作。用户已经很熟悉这些标准工具,不要在这些内容上浪费精力。 以下是个简单但极为普遍的动作流程:接到邀请你前往某个地点的电话。查看时间。查看天气。用Google Maps搜索该地点。用Foursquare签到。那么,你的应用要同整个流程中的哪个部分绑定呢? 没有用户会单纯为了你的应用而摆弄自己的手机设备。但是如果你成功制作了一款优秀的软件,他们会愿意将其整合到日常的手机使用流程中。让用户能够便捷地使用分享或在网络上搜索有趣信息等功能,使他们交替使用你的应用和其他应用。 (许多应用会直接绑定Android的分享机制。你可以将此作为应用的优势。) 7、让你的应用更为独特 目前,Android Market上有数十万款应用。你或许会时常问自己,如何从如此多的同类应用中突出重围。如果你想要构建的又是一款无聊的黑白数独游戏,或者是基于官方代码范例的记录应用,那就很难获得可观的下载量。 不要认为目前市场上已经没有优秀应用的发展空间。用户偏好的应用类型各不相同。有些人偏爱几乎能够做所有事情的记录应用,有些人需要的只是带有同步功能的文本编辑应用,还有些人只是想要个有着清楚UI的记录应用。 无论你选择的是哪个方向,要构建带有一定特征的应用。操作系统和核心应用已经为用户提供了所有基本功能。制作某些能够用内置解决方案吸引用户使用产品,这样才能够脱颖而出。将你的应用视为住在智能手机中的小机器人。它与你交流,告诉你有趣的事情,帮助你完成日常事务。你希望自己的机器人聪明专业,还是精明可爱,抑或是滑稽搞笑? 在应用构建的开始就要记住这一点。人们喜欢与他们的个性相符的应用。如果你想要构建照片分享应用,可以为其添加各种主题和徽章。如果想要构建的是款定位服务应用,可以考虑将其简化成只具有最基本的功能,让所有内容自动化完成。应用设计愿景的微小改变可能会改变整个应用以及用户的使用方式。 (Feedly也是款整合Google Reader的新闻阅读器,但是它使用类似于杂志的呈现方式和清晰的界面设计,这就是该应用与其他阅读器的不同之处。) 8、遵守平台指导原则 尽管你的目标是制作出独特的应用,但是并非意味着应用的每个部分都要完全与众不同。谷歌就Android应用的设计和开发提供了许多指导性原则。熟悉这些原则。人们能够用来研究现代智能手机的时间比你想象的要少。不要让应用中遍布自定义互动元素,这会让他们的操作更为困难。 学习使用Android设备需要用户适应触摸、输入、摇动甚至不时按动硬件按键等操作。他们需要识别输入区域、选择框、模式对话框和菜单等样式。你真的还想给他们增加更多的负担吗? 使用简单和直观的列表。在应用开启屏幕中,用大图标来呈现主要功能。添加标题作为最常用功能的入口,让用户能够随时返回开启屏幕。如果你无法显著提升某些操作的功能,那么就保持原样。人们会认同应用和整个操作系统的一致性。 认真研究谷歌的界面和决策。熟悉整个原则,并在开发应用时用上这些原则。但是,不可过于死板。如果你能够改良某些元素,而且你确信自己的做法比原则建议的更好,那么就勇敢去做! (Catch Notes用户的多数动作可利用应用中的大图标功能实现,这款应用遵从了基本原则,因而运转良好。) 9、测试 所有的用户都各不相同,我们必须正视这个问题。你可以在应用中投入尽可能多的精力,但是你不可能令所有人满意。甚至连将应用制作成适合多数人的需求都是件很困难的事情。 不要误解我的说法。你在发布应用前,必须考虑到不同人可能会有不同的使用方式。你需要不同的人来测试应用,由此找出最恼人的问题和漏洞。大公司往往耗资数千美元进行可用性研究,在昂贵的实验室中让数百名不同类型的用户测试应用。 虽然这是个提升应用UI的绝妙方法,但多数独立和小型开发商无法承担如此多的费用。但是,也不要以此为借口而放弃应用测试。你可以开展成本低廉的测试,寻找不同的用户群体,由此来大幅改善你的应用,让其能够满足更多用户的需求。 将应用原型安装到你的开发设备上,花点钱购买些小礼物,开展应用测试。先从同事和好友开始,然后再以你从未见过的陌生人为对象。多数人都愿意花点时间来体验全新的东西,只要你足够礼貌甚至愿意为他们费时测试应用提供奖励。 让他们像你预期那样使用应用,然后细致地观察他们的使用过程。告诉他们目标是什么,但要尽量少提供帮助,但也别让他们卡在某个地方。很快,你就会发现应用的纰漏和瓶颈。 10、发布到市场上 你已经制作完成了自己的首个应用。感觉很棒,不是吗? 不要犯许多开发者犯下的某些错误。诚然,你想要将应用发布到市场上,看看用户会有何评价。但是,最后这几个步骤会让你的首次发布更为成功。 确认完成对应用的测试后,我们还需要考虑些小问题。 你上传到Android Market的应用还应该带有以下4种资产: ...

2014年10月10日 · 1 分钟 · 天边的星星

一个高效的UI才是一个拉风的UI

Android是一个运行在移动终端上的操作系统,跟传统PC最大的不同所在就是移动终端的资源紧缺问题“比较”明显,当然对于一些屌丝机型,应该用“非常“来形容才靠谱。所以经常会出现在一些比较缺乏青春活力的老型机上,运行一些软件被异常终止的情况;然而作为互联网厂家来说,广大的屌丝机用户肯定是一大笔用户资源,这是能放弃的市场吗?!当然不行o(╯□╰)o,所以我们要尽可能得提高软件的效率来赢取客户的回眸一笑了,屌丝也是客户! 这篇博客主要介绍如何在UI设计上提高效率,减少资源的利用,毕竟在终端资源短缺的今天,效率始终为王。我们评判一个UI界面不是认为有多复杂才给力,或者说有多炫才靠谱,一个简约而又不平凡的高效UI界面才是一个灰常牛逼的界面设计。 引入 在android应用中,采用硬编码方式编写界面并不是一个提倡的方法。当然硬编码编写的界面比基于XML文件的软编码界面高效灵活一些,但是非常不容易维护,错综复杂的代码更会让程序埋雷重重,说不定哪天就把应用炸的惨不忍睹。所以如果非常必要非常肯定要采用代码编写硬编码界面之外,其他情况还是采用易于维护的XML来编写比较好。 所以文中对于UI优化设计归结到底也就是对XML布局文件的优化设计。 在谷歌给我们的开发环境中,存在这么一个非常好用的工具——hierarchyviewer,估计很多人都没搭理过这个藏在偏僻角落的小工具吧;它能非常容易的帮我们分析UI界面的结构和构造效率,这个工具的位置就在sdk/tools/文件夹。 楼下上图: 大家好,我是图~ 这是分析的是一个布局上只有一个TextView组件的XML界面,图告诉我们,构造这个界面总共用了四个组件,也就是需要绘制四次组件,自然每一次绘制组件都需要耗费资源。 下面步入狂拽酷炫吊炸天的主题部分。。。。 尽量用最少的步骤完成布局 我是社会好青年,我为国家省资源;当然作为组件来说也需要这个觉悟,每个组件的绘制都会多多少少耗费终端的资源。所以我们在这里可不能听老祖宗的话:韩信点兵多多益善了,精兵简政才是UI设计的唯一出路。不相信?行!下面就开始给个对比的例子。 假设项目需要搞这么一个按钮: 这不简单吗?几行代码不是分分钟的事情吗? 1 <div class="line number2 index1 alt1"> 2 </div> <div class="line number3 index2 alt2"> 3 </div> <div class="line number4 index3 alt1"> 4 </div> <div class="line number5 index4 alt2"> 5 </div> <div class="line number6 index5 alt1"> 6 </div> <div class="line number7 index6 alt2"> 7 </div> <div class="line number8 index7 alt1"> 8 </div> <div class="line number9 index8 alt2"> 9 </div> <div class="line number10 index9 alt1"> 10 </div> <div class="line number11 index10 alt2"> 11 </div> <div class="line number12 index11 alt1"> 12 </div> <div class="line number13 index12 alt2"> 13 </div> <div class="line number14 index13 alt1"> 14 </div> <div class="line number15 index14 alt2"> 15 </div> <div class="line number16 index15 alt1"> 16 </div> <div class="line number17 index16 alt2"> 17 </div> <div class="line number18 index17 alt1"> 18 </div> </td> <td class="code"> <div class="container"> <div class="line number1 index0 alt2"> `&lt;RelativeLayout` </div> <div class="line number2 index1 alt1"> ` ``android:layout_width=``"wrap_content"` </div> <div class="line number3 index2 alt2"> ` ``android:layout_height=``"wrap_content"` </div> <div class="line number4 index3 alt1"> ` ``android:gravity=``"center"` `&gt;` </div> <div class="line number5 index4 alt2"> ` ``&lt;Button` </div> <div class="line number6 index5 alt1"> ` ``android:id=``"@+id/button1"` </div> <div class="line number7 index6 alt2"> ` ``android:layout_width=``"wrap_content"` </div> <div class="line number8 index7 alt1"> ` ``android:layout_height=``"wrap_content"` </div> <div class="line number9 index8 alt2"> ` ``android:background=``"@drawable/btn_backgroup"` </div> <div class="line number10 index9 alt1"> ` ``/&gt;` </div> <div class="line number11 index10 alt2"> ` ``&lt;ImageView` </div> <div class="line number12 index11 alt1"> ` ``android:id=``"@+id/imageView1"` </div> <div class="line number13 index12 alt2"> ` ``android:layout_width=``"wrap_content"` </div> <div class="line number14 index13 alt1"> ` ``android:layout_height=``"wrap_content"` </div> <div class="line number15 index14 alt2"> ` ``android:layout_alignParentLeft=``"true"` </div> <div class="line number16 index15 alt1"> ` ``android:layout_centerVertical=``"true"` </div> <div class="line number17 index16 alt2"> ` ``android:src=``"@drawable/header_back"` `/&gt;` </div> <div class="line number18 index17 alt1"> `&lt;/RelativeLayout&gt;` </div> </div> </td> </tr> </table> </div> 也别急着看代码,多累多伤眼睛呀,直接上个hierarchyviewer里面的图来瞧瞧呗 ...

2014年10月10日 · 7 分钟 · 天边的星星