简介
由于工作原因所以从头研究应用层的绘制,所以这篇笔记是从应用层着手探究绘制的基本使用以及原理还有涉及图表的粗略设计方法。
核心类&方法
Canvas.class
Canvas介绍
Canvas是一个工具类,这个工具类的目的是提供一系列的绘制基础图形的API,通过Canvas内部的drawXX()方法可以将一些如:线条,圆,点以及方块的图形绘制在用户显示的界面中。当然Canvas既然已经设计出来,那么肯定有对应的拓展功能,就不单纯是画一些东西,还会有save(),restore()方法等,利用这些方法可以将一种状态进行保存,然后最后清楚这种状态之后的处理过程。
发现问题
Canvas绘制的内容要用什么作为载体,答案是:通过内存...可能这个答案是个废话,但是仔细想,在应用层什么对于绘制过程中的内存具有代表性质,想想可能发现经常因为Bitmap而造成内存泄露,所以Bitmap就是这里的内存代表。所以Canvas要将一些图形绘制到Bitmap中最后经过Surface进行显示。我们之前查看过Surface的源码,里面对应有缓冲区,所以这里讲Bitmap对应的内存数据需要进行拷贝到缓冲区中,然后进行显示。
Canvas基本变换
canvas.save();//锁画布(为了保存之前的画布状态)
canvas.translate(10, 10);//把当前画布的原点移到(10,10),后面的操作都以(10,10)作为参照点,默认原点为(0,0)
drawScene(canvas);
canvas.restore();//把当前画布返回(调整)到上一个save()状态之前
canvas.save();//锁画布(为了保存之前的画布状态)
canvas.translate(160, 10);//把当前画布的原点移到(160,10),后面的操作都以(160,10)作为参照点,
canvas.clipRect(10, 10, 90, 90);//这里的真实坐标为左上(170,170)、右下(250,250)
canvas.clipRect(30, 30, 70, 70, Region.Op.DIFFERENCE);
drawScene(canvas);
canvas.restore();
不要小看这几个方法,设计的点接下来一个个分析。
canvas.save()&canvas.restore()
canvas.save()这个方法就是之前说的将对应的状态进行保存,canvas.restore()这个方法将保存的状态进行清除。具体在哪里应用呢,比如我现在需要平移,然后绘制平移完了,之后需要回到平移之后的状态,这种情况就需要进行回滚。
canvas.translate()
这个方法是平移,在onDraw()的方法中需要做的就是根据自己的位置,对canvas进行坐标对应,一般情况下我们都使用到的坐标系是绘制控件本身与canva的坐标对应。但是如果我们不断变换坐标,不断进行绘制,这个时候我们需要对每一个绘制的图形进行参数位置的设置这样很麻烦,所以我们就想到了相对坐标,那么平移就会把上一次的坐标变成当前的坐标再一次以这个坐标为基准进行绘制。这样就方便很多。
drawScaen(canvas)
这个方法并不是Android API内部的方法,而是我在这里写的一个方法,写这个方法目的是一个设计的概念,我们想,我们不能每一次得到任何一个形状就在自定义的View.onDraw中直接绘制,这样重复的工作会很多。那么怎么弄,我们应该使用继承关系,将重复的工作 不断抽取,最后将drawScaen(canvas)单独放到一个类中,这个类是我们设计的图形或者一个界面。这样的话我们就很久java开发规范中继承关系,与拥有关系将图形的绘制解耦合。
绘制源码分析
ViewRootImpl.java
private void performDraw() {
try {
//进行绘制操作
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
}
...
}
}
private void draw(boolean fullRedrawNeeded) {
//surface用来操作应用窗口的绘图表面
Surface surface = mSurface;
if (surface == null || !surface.isValid()) {
return;
}
if (!sFirstDrawComplete) {
synchronized (sFirstDrawHandlers) {
sFirstDrawComplete = true;
for (int i=0; i<sFirstDrawHandlers.size(); i++) {
post(sFirstDrawHandlers.get(i));
}
}
}
scrollToRectOrFocus(null, false);
if (mAttachInfo.mViewScrollChanged) {
mAttachInfo.mViewScrollChanged = false;
mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
}
int yoff;
//计算窗口是否处于滚动状态
final boolean scrolling = mScroller != null && mScroller.computeScrollOffset();
if (scrolling) {
yoff = mScroller.getCurrY();
} else {
yoff = mScrollY;
}
if (mCurScrollY != yoff) {
mCurScrollY = yoff;
fullRedrawNeeded = true;
}
//描述窗口是否正在请求大小缩放
float appScale = mAttachInfo.mApplicationScale;
boolean scalingRequired = mAttachInfo.mScalingRequired;
//描述窗口的脏区域,即需要重新绘制的区域
Rect dirty = mDirty;
if (mSurfaceHolder != null) {
// The app owns the surface, we won't draw.
dirty.setEmpty();
return;
}
//用来描述是否需要用OpenGL接口来绘制UI,当应用窗口flag等于WindowManager.LayoutParams.MEMORY_TYPE_GPU
//则表示需要用OpenGL接口来绘制UI
if (mUseGL) {
if (!dirty.isEmpty()) {
Canvas canvas = mGlCanvas;
if (mGL != null && canvas != null) {
mGL.glDisable(GL_SCISSOR_TEST);
mGL.glClearColor(0, 0, 0, 0);
mGL.glClear(GL_COLOR_BUFFER_BIT);
mGL.glEnable(GL_SCISSOR_TEST);
mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
mAttachInfo.mIgnoreDirtyState = true;
mView.mPrivateFlags |= View.DRAWN;
int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
try {
canvas.translate(0, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired
? DisplayMetrics.DENSITY_DEVICE : 0);
mView.draw(canvas);
if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) {
mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING);
}
} finally {
canvas.restoreToCount(saveCount);
}
mAttachInfo.mIgnoreDirtyState = false;
mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
checkEglErrors();
if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) {
int now = (int)SystemClock.elapsedRealtime();
if (sDrawTime != 0) {
nativeShowFPS(canvas, now - sDrawTime);
}
sDrawTime = now;
}
}
}
//如果窗口处于滚动状态,则应用窗口需要马上进行下一次全部重绘,调用scheduleTraversals()方法
if (scrolling) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
return;
}
//是否需要全部重绘,如果是则将窗口的脏区域设置为整个窗口区域,表示整个窗口曲云都需要重绘
if (fullRedrawNeeded) {
mAttachInfo.mIgnoreDirtyState = true;
dirty.union(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(TAG, "Draw " + mView + "/"
+ mWindowAttributes.getTitle()
+ ": dirty={" + dirty.left + "," + dirty.top
+ "," + dirty.right + "," + dirty.bottom + "} surface="
+ surface + " surface.isValid()=" + surface.isValid() + ", appScale:" +
appScale + ", width=" + mWidth + ", height=" + mHeight);
}
if (!dirty.isEmpty() || mIsAnimating) {
Canvas canvas;
try {
int left = dirty.left;
int top = dirty.top;
int right = dirty.right;
int bottom = dirty.bottom;
//调用Surface.lockCanvas()来创建画布
canvas = surface.lockCanvas(dirty);
if (left != dirty.left || top != dirty.top || right != dirty.right ||
bottom != dirty.bottom) {
mAttachInfo.mIgnoreDirtyState = true;
}
// TODO: Do this in native
canvas.setDensity(mDensity);
} catch (Surface.OutOfResourcesException e) {
Log.e(TAG, "OutOfResourcesException locking surface", e);
// TODO: we should ask the window manager to do something!
// for now we just do nothing
return;
} catch (IllegalArgumentException e) {
Log.e(TAG, "IllegalArgumentException locking surface", e);
// TODO: we should ask the window manager to do something!
// for now we just do nothing
return;
}
try {
if (!dirty.isEmpty() || mIsAnimating) {
long startTime = 0L;
if (DEBUG_ORIENTATION || DEBUG_DRAW) {
Log.v(TAG, "Surface " + surface + " drawing to bitmap w="
+ canvas.getWidth() + ", h=" + canvas.getHeight());
//canvas.drawARGB(255, 255, 0, 0);
}
if (Config.DEBUG && ViewDebug.profileDrawing) {
startTime = SystemClock.elapsedRealtime();
}
// If this bitmap's format includes an alpha channel, we
// need to clear it before drawing so that the child will
// properly re-composite its drawing on a transparent
// background. This automatically respects the clip/dirty region
// or
// If we are applying an offset, we need to clear the area
// where the offset doesn't appear to avoid having garbage
// left in the blank areas.
if (!canvas.isOpaque() || yoff != 0) {
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
}
dirty.setEmpty();
mIsAnimating = false;
mAttachInfo.mDrawingTime = SystemClock.uptimeMillis();
mView.mPrivateFlags |= View.DRAWN;
if (DEBUG_DRAW) {
Context cxt = mView.getContext();
Log.i(TAG, "Drawing: package:" + cxt.getPackageName() +
", metrics=" + cxt.getResources().getDisplayMetrics() +
", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo());
}
int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
try {
canvas.translate(0, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired
? DisplayMetrics.DENSITY_DEVICE : 0);
mView.draw(canvas);
} finally {
mAttachInfo.mIgnoreDirtyState = false;
canvas.restoreToCount(saveCount);
}
if (Config.DEBUG && ViewDebug.consistencyCheckEnabled) {
mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING);
}
if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) {
int now = (int)SystemClock.elapsedRealtime();
if (sDrawTime != 0) {
nativeShowFPS(canvas, now - sDrawTime);
}
sDrawTime = now;
}
if (Config.DEBUG && ViewDebug.profileDrawing) {
EventLog.writeEvent(60000, SystemClock.elapsedRealtime() - startTime);
}
}
} finally {
//UI绘制完成后,调用urface.unlockCanvasAndPost(canvas)S来请求SurfaceFlinger进行UI的渲染
surface.unlockCanvasAndPost(canvas);
}
}
if (LOCAL_LOGV) {
Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost");
}
if (scrolling) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
}
}
- 调用Scroller.computeScrollOffset()方法计算应用是否处于滑动状态,并获得应用窗口在Y轴上的即时滑动位置yoff。
- 根据AttachInfo里描述的数据,判断窗口是否需要缩放。
- 根据成员变量React mDirty的描述来判断窗口脏区域的大小,脏区域指的是需要全部重绘的窗口区域。
- 根据成员变量boolean mUserGL判断是否需要用OpenGL接口来绘制UI,当应用窗口flag等于WindowManager.LayoutParams.MEMORY_TYPE_GPU则表示需要用OpenGL接口来绘制UI.
- 如果不是用OpenGL来绘制,则用Surface来绘制,先调用Surface.lockCanvas()来创建画布,UI绘制完成后,再调用urface.unlockCanvasAndPost(canvas)S来请求SurfaceFlinger进行UI的渲染
其实我们的核心有以下几个:
- 通过Surface.lockCanvas()创建画布,所以我们应该在Surface中寻找答案,这个我们以后肯定会分析,但是目前是应用层,先把上层大概分析一下,要不然拿不动工作我就得走人了。
- 在一个就是通过drawSoftware()方法进行软绘制,这个就是不开启硬件加速时候走的流程。
我们在压缩这个小结:
- 绘制背景
- 调用onDraw绘制自身内容
- 调用dispatchDraw()绘制子控件
- 绘制滚动条
接下来就是我们自定义绘制了,也是应用层那些图表的核心,所以今天吓得我赶紧看github上有没有开源的项目解解渴。还真有!
后续
今天运气巨好无比,导入人家的代码都使劲给我报错。我现在就把这些错分享出来
- Android Studio出现Error:No service of type Factory available in ProjectScopeServices.
定位到根目录的build.gradle
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3'
换成
1.4.1
原因是啥,原因是有人给google提了一个pach...
- 还会报哪个错误
将导入的依赖项目中android{},dependencies{},apply plugin:,对应的其他东西都删掉,对没错删掉。
- apply plugin: 'com.jfrog.bintray'找不到这个
找不到,如果项目中没用的话,直接注释掉
//apply plugin: 'com.jfrog.bintray'
今天下午大概看了一下人家写的流程,就是将View继承到底,一层包一层,将Canvas分离到每一个细节类中,这里的细节类就是那些饼状图,圆柱图什么的,它是每一种对应一个类。然后对数据也进行了封装,过程中使用了高内聚,使用变量比较多,然后都是进行设置。所以感觉人家的架构能力很好,尔等渣渣只能膜拜效仿,但是相信不出这个国庆,我也能看个八九不离五六哈哈,日后也写一个框架,不求功能多,只求架构好。
先给大家几张截图