SurfaceView简介与使用
SurfaceView 在 Android 系统中,是一种特殊的视图。它拥有独立的绘图表面,即它不与宿主窗口共享同一个绘图表面。
由于拥有独立的绘图表面,因此 SurfaceView 的 UI 就可以在一个独立的线程中进行绘制,又由于不会占用主线程资源, 运用 SurfaceView 可以实现复杂而高效的 UI,另一方面又不会导致用户输入得不到及时响应。 比较适合应用在视频播放,图片浏览,对画面要求高的游戏上面。
SurfaceView相当于是在屏幕里面,而屏幕给它开了一个洞。给它设置背景色就能把它盖住。
使用 SurfaceView 的原因之一,是能够在子线程中更新图像。减轻 UI 线程的压力。 所有 SurfaceView 和 SurfaceHolder.Callback 的方法都应该在 UI 线程里调用,一般来说就是应用程序主线程。 渲染线程所要访问的各种变量应该作同步处理。要确保绘图线程仅在 surface 可用期间进行绘图。 SurfaceView 主要由 SurfaceHolder 来控制,holder 相当于一个控制器。
核心要点
- View:必须在UI的主线程中更新画面,用于被动更新画面。
- SurfaceView:UI线程和子线程中都可以。在一个新启动的线程中重新绘制画面,主动更新画面。
java.lang.Object
android.view.View
android.view.SurfaceView
使用方法
创建一个类继承SurfaceView并实现SurfaceHolder.Callback
接口。
public class MySView extends SurfaceView implements SurfaceHolder.Callback {
.........
}
获取SurfaceHolder
在构造函数中获取 SurfaceHolder。并添加回调。
public MySView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
holder = getHolder();
holder.addCallback(this);
// ......
}
复写方法
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d(TAG, "surfaceCreated");
drawThread = new DrawThread(holder);// 创建一个绘图线程
drawThread.start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.d(TAG, "surfaceChanged");
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
drawThread.closeThread();// 销毁线程
Log.d(TAG, "surfaceDestroyed");
}
当 Activity 不可见时,SurfaceView 会调用 surfaceDestroyed
方法。此时就要销毁绘图线程。
绘图子线程
SurfaceView 中绘图操作是在子线程中进行的。正常来说子线程不能操作 UI。但是我们可以使用 SurfaceHolder 提供的方法锁定画布,绘图完成后释放并更新画布。 下面的线程提供了停止、恢复和结束等功能。
class DrawThread extends Thread {
private SurfaceHolder mmHolder;
private boolean mmRunning;
private boolean mmIsPause;
public DrawThread(SurfaceHolder holder) {
this.mmHolder = holder;
mmRunning = true;
}
@Override
public void run() {
while (mmRunning && !isInterrupted()) {
if (!mmIsPause) {
Canvas canvas = null;
try {
synchronized (mmHolder) {
canvas = holder.lockCanvas(); // 锁定画布,获得返回的画布对象Canvas
canvas.drawColor(bgSurfaceViewColor);// 设置画布背景颜色
// 绘图操作......
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (canvas != null) {
mmHolder.unlockCanvasAndPost(canvas);// 释放画布,并提交改变。
}
pauseThread();
}
} else {
onThreadWait();
}
}
}
public synchronized void pauseThread() {
mmIsPause = true;
}
/**
* 线程等待,不提供给外部调用
*/
private void onThreadWait() {
try {
synchronized (this) {
this.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized void resumeThread() {
mmIsPause = false;
this.notify();
}
public synchronized void closeThread() {
try {
mmRunning = false;
notify();
interrupt();
} catch (Exception e) {
e.printStackTrace();
}
}
}
layout中使用
切记不可在 layout 中设置 background,颜色会直接把 SurfaceView 挡住。
<com.rustfisher.fisherandroidchart.MySView
android:id="@+id/mySurfaceView"
android:layout_width="match_parent"
android:layout_height="230dp" />
Android 自动缩放上下限的折线图
一条折线,根据最大最小值自动缩放上下限。
- 继承View
- 数据使用FloatBuffer存储
- 可改变显示窗口的大小
- 可指定坐标轴,折线和字体颜色
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import java.nio.FloatBuffer;
public class AutoLineChart extends View {
private static final String TAG = "rustApp" + AutoLineChart.class.getSimpleName();
private float yMax = 1.6f;
private float yMin = -1.0f;
float yAxisZoomLimitMax = 1.6f;
float yAxisZoomLimitMin = -1.0f; // 缩小y轴的极限值
// Y轴自动缩放时的增减距离
float axisYPerStep = 0.1f;
// 图表线条在view顶部留出的间距
float viewYStart = 2;
float axisTextSize = 10;
private int onShowPointsCount = 500; // 当前显示的数据个数
int onShowMinPoints = 100; // 至少要显示的数据个数
private int maxPoint = 9000; // 数据存储最大个数
// 坐标轴线条宽度
float axisLineWid = 1f;
int dataLineWid = 4;
// 数据线颜色
private int dataColor = Color.parseColor("#eaffe9");
// 图表中的背景线条颜色
private int mainBgLineColor = Color.parseColor("#535353");
// 坐标轴颜色
private int axisColor = Color.WHITE;
// 坐标值字体颜色
private int axisTextColor = Color.WHITE;
// 背景色
private int viewBgColor = Color.parseColor("#222222");
Rect rectText = new Rect();
private float xStep = 1.0f;
private float viewWidth;
private float viewHeight;
private float botLeftXOnView = 0; // 图表左下点在view中的x坐标
private float botLeftYOnView = 0;
private float originYToBottom = 20; // 图表原点距离view底部的距离
private FloatBuffer dataBuffer;
private Paint bgPaint;
private Paint linePaint;
public AutoLineChart(Context context) {
this(context, null);
}
public AutoLineChart(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AutoLineChart(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public int getMaxPoint() {
return maxPoint;
}
public void setOnShowPointsCount(int onShowPointsCount) {
this.onShowPointsCount = onShowPointsCount;
}
public int getOnShowPointsCount() {
return onShowPointsCount;
}
public int getOnShowMinPoints() {
return onShowMinPoints;
}
public void addData(float data) {
dataBuffer.put(data);
if (dataBuffer.position() > (dataBuffer.capacity() * 2 / 3)) {
float[] bufferArr = dataBuffer.array();
System.arraycopy(bufferArr, dataBuffer.position() - maxPoint, bufferArr, 0, maxPoint);
dataBuffer.position(maxPoint);
// Log.d(TAG, "把当前数据移动到buffer起始位置 " + dataBuffer);
}
invalidate();
}
private void init(Context context) {
dataBuffer = FloatBuffer.allocate(3 * maxPoint); // 分配3倍的空间
bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
bgPaint.setStrokeWidth(axisLineWid);
bgPaint.setStyle(Paint.Style.STROKE);
bgPaint.setColor(mainBgLineColor);
linePaint.setStrokeWidth(dataLineWid);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setColor(dataColor);
botLeftXOnView = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32, context.getResources().getDisplayMetrics());
originYToBottom = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics());
viewYStart = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics());
axisLineWid = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
axisTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10, context.getResources().getDisplayMetrics());
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewWidth = getWidth();
viewHeight = getHeight();
botLeftYOnView = viewHeight - originYToBottom;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(viewBgColor);
xStep = (viewWidth - botLeftXOnView) / (onShowPointsCount - 1);
float maxData = 0.1f;
float minData = 0;
int dataStartIndexInBuffer = 0; // 数据在buffer中的起始下标
if (dataBuffer.position() > onShowPointsCount) {
dataStartIndexInBuffer = dataBuffer.position() - onShowPointsCount;
}
float[] bufferArr = dataBuffer.array();
for (int i = dataStartIndexInBuffer; i < dataBuffer.position(); i++) {
if (bufferArr[i] < minData) {
minData = bufferArr[i];
} else if (bufferArr[i] > maxData) {
maxData = bufferArr[i];
}
}
zoomYAxis(maxData, minData);
drawBgLines(canvas);
drawWave(canvas, dataStartIndexInBuffer);
}
// 缩放Y轴
private void zoomYAxis(float maxData, float minData) {
if (maxData < yAxisZoomLimitMax) {
yMax = yAxisZoomLimitMax;
} else if (maxData < yMax) {
while (maxData < yMax) {
yMax -= axisYPerStep;
}
yMax += axisYPerStep;
} else if (maxData > yMax) {
while (maxData > yMax) {
yMax += axisYPerStep;
}
}
if (minData > yAxisZoomLimitMin) {
yMin = yAxisZoomLimitMin;
} else if (minData > yMin) {
while (minData > yMin) {
yMin += axisYPerStep;
}
yMin -= axisYPerStep;
} else if (minData < yMin) {
yMin -= axisYPerStep;
}
}
private void drawBgLines(Canvas canvas) {
// 画坐标轴
bgPaint.setStyle(Paint.Style.FILL);
bgPaint.setStrokeWidth(axisLineWid);
bgPaint.setTextSize(axisTextSize);
bgPaint.setTextAlign(Paint.Align.RIGHT);
for (float y = 0; y <= yMax; y += 0.5) {
drawYAxis(canvas, y);
}
for (float y = 0; y >= yMin; y -= 0.5) {
drawYAxis(canvas, y);
}
bgPaint.setColor(axisColor);
canvas.drawLine(botLeftXOnView, viewYStart / 2, botLeftXOnView, botLeftYOnView + viewYStart / 2, bgPaint);
// canvas.drawLine(botLeftXOnView, botLeftYOnView, viewWidth, botLeftYOnView, bgPaint); // x轴
}
private void drawYAxis(Canvas canvas, float axisYValue) {
final float yDataRange = yMax - yMin;
final float yAxisRangeOnView = botLeftYOnView - viewYStart;
float aY = botLeftYOnView - (axisYValue - yMin) / yDataRange * yAxisRangeOnView;
bgPaint.setColor(axisColor);
canvas.drawLine(botLeftXOnView - 20, aY, botLeftXOnView, aY, bgPaint);
String axisText = String.valueOf(axisYValue);
bgPaint.getTextBounds(axisText, 0, axisText.length(), rectText); // 获取文本的宽高
canvas.drawText(axisText, botLeftXOnView - rectText.width() / 2, aY + rectText.height() / 2, bgPaint);
bgPaint.setColor(mainBgLineColor);
canvas.drawLine(botLeftXOnView, aY, viewWidth, aY, bgPaint);
}
private void drawWave(Canvas canvas, int dataStartIndexInBuffer) {
final float yDataRange = yMax - yMin;
final float yAxisRangeOnView = botLeftYOnView - viewYStart;
final float yDataStep = yAxisRangeOnView / yDataRange;
float[] dataArr = dataBuffer.array();
for (int i = dataStartIndexInBuffer; i < dataBuffer.position() - 1; i++) {
canvas.drawLine(botLeftXOnView + (i - dataStartIndexInBuffer) * xStep, getYL(dataArr[i], yDataStep),
botLeftXOnView + (i - dataStartIndexInBuffer + 1) * xStep, getYL(dataArr[i + 1], yDataStep),
linePaint);
}
}
private float getYL(final float yData, float yDataStep) {
return botLeftYOnView - (yData - yMin) * yDataStep;
}
}
自定义 View 面试题
1. 讲下 View 的绘制流程?
参考回答:
View 的工作流程主要是指 measure、layout、draw 这三大流程,即测量、布局和绘制,
- measure 确定 View 的 测量宽/高;
- layout 确定 View 的最终宽/高和四个顶点的位置;
- draw 则将 View 绘制到屏幕上;
View的绘制过程遵循如下几步:
- 绘制背景 background.draw(canvas)
- 绘制自己(onDraw)
- 绘制 children(dispatchDraw)
- 绘制装饰(onDrawScollBars)
2. MotionEvent是什么?包含几种事件?什么条件下会产生?
参考回答:
MotionEvent 是手指接触屏幕后所产生的一系列事件。典型的事件类型有如下:
- ACTION_DOWN:手指刚接触屏幕
- ACTION_MOVE:手指在屏幕上移动
- ACTION_UP:手指从屏幕上松开的一瞬间
- ACTION_CANCELL:手指保持按下操作,并从当前控件转移到外层控件时触发
正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件,考虑如下几种情况:
- 点击屏幕后松开,事件序列:DOWN→UP
- 点击屏幕滑动一会再松开,事件序列为DOWN→MOVE→.....→MOVE→UP
3. 描述一下 View 事件传递分发机制?
参考回答:
View 事件分发本质就是对 MotionEvent 事件分发的过程。即当一个 MotionEvent 发生后,系统将这个点击事件传递到一个具体的 View 上
点击事件的传递顺序:Activity(Window)→ViewGroup→ View
事件分发过程由三个方法共同完成:
- dispatchTouchEvent:用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件
- onInterceptTouchEvent:在上述方法内部调用,对事件进行拦截。该方法只在ViewGroup中有,View(不包含 ViewGroup)是没有的。一旦拦截,则执行ViewGroup的onTouchEvent,在ViewGroup中处理事件,而不接着分发给View。且只调用一次,返回结果表示是否拦截当前事件
- onTouchEvent: 在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件
4. 如何解决 View 的事件冲突 ? 举个开发中遇到的例子 ?
参考回答:
常见开发中事件冲突的有 ScrollView 与 RecyclerView 的滑动冲突、RecyclerView 内嵌同时滑动同一方向
滑动冲突的处理规则:
- 对于由于外部滑动和内部滑动方向不一致导致的滑动冲突,可以根据滑动的方向判断谁来拦截事件。
- 对于由于外部滑动方向和内部滑动方向一致导致的滑动冲突,可以根据业务需求,规定何时让外部View拦截事件,何时由内部View拦截事件。
- 对于上面两种情况的嵌套,相对复杂,可同样根据需求在业务上找到突破点。
滑动冲突的实现方法:
- 外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。具体方法:需要重写父容器的onInterceptTouchEvent方法,在内部做出相应的拦截。
- 内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。具体方法:需要配合requestDisallowInterceptTouchEvent方法。
5. scrollTo() 和 scollBy() 的区别?
参考回答:
scollBy 内部调用了 scrollTo,它是基于当前位置的相对滑动;而 scrollTo 是绝对滑动,因此如果使用相同输入参数多次调用 scrollTo 方法,由于 View 的初始位置是不变的,所以只会出现一次View滚动的效果 两者都只能对 View 内容的滑动,而非使 View 本身滑动。可以使用 Scroller 有过度滑动的效果
6. Scroller 是怎么实现 View 的弹性滑动?
参考回答:
-
在 MotionEvent.ACTION_UP 事件触发时调用 startScroll() 方法,该方法并没有进行实际的滑动操作,而是记录滑动相关量(滑动距离、滑动时间)
-
接着调用 invalidate/postInvalidate() 方法,请求 View 重绘,导致 View.draw 方法被执行
-
当 View 重绘后会在 draw 方法中调用 computeScroll 方法,而 computeScroll 又会去向Scroller 获取当前的 scrollX 和 scrollY;然后通过 scrollTo 方法实现滑动;接着又调用postInvalidate 方法来进行第二次重绘,和之前流程一样,如此反复导致 View 不断进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动,直到整个滑动过成结束.