Android-手撸抖音“潜艇大挑战”,非科班面试之旅

BoatView

自定义View中完成以下几个事情

  • 通过两个资源定时切换,实现探照灯闪烁的效果
  • 通过OverScroller让移动过程更加顺滑
  • 通过一个Rotation Animation,让潜艇在移动时可以调转角度,更加灵动

internal class BoatView(context: Context?) : AppCompatImageView(context) {

private val _scroller by lazy { OverScroller(context) }

private val _res = arrayOf(
R.mipmap.boat_000,
R.mipmap.boat_002
)

private var _rotationAnimator: ObjectAnimator? = null

private var _cnt = 0
set(value) {
field = if (value > 1) 0 else value
}

init {
scaleType = ScaleType.FIT_CENTER
_startFlashing()
}

private fun _startFlashing() {
postDelayed({
setImageResource(_res[_cnt++])
_startFlashing()
}, 500)
}

override fun computeScroll() {
super.computeScroll()

if (_scroller.computeScrollOffset()) {

x = _scroller.currX.toFloat()
y = _scroller.currY.toFloat()

// Keep on drawing until the animation has finished.
postInvalidateOnAnimation()
}

}

/**
* 移动更加顺换
*/
internal fun smoothMoveTo(x: Int, y: Int) {
if (!_scroller.isFinished) _scroller.abortAnimation()
_rotationAnimator?.let { if (it.isRunning) it.cancel() }

val curX = this.x.toInt()
val curY = this.y.toInt()

val dx = (x - curX)
val dy = (y - curY)
_scroller.startScroll(curX, curY, dx, dy, 250)

_rotationAnimator = ObjectAnimator.ofFloat(
this,
“rotation”,
rotation,
Math.toDegrees(atan((dy / 100.toDouble()))).toFloat()
).apply {
duration = 100
start()
}

postInvalidateOnAnimation()
}
}

ForegroundView

  • 通过boat成员持有潜艇对象,并对其进行控制
  • 实现CameraHelper.FaceDetectListener根据人脸识别的回调,移动潜艇到指定位置
  • 游戏开始时,创建潜艇并做开场动画

/**
* 游戏开始时通过动画进入
*/
@MainThread
fun start() {
_isStop = false
if (boat == null) {
boat = Boat(context).also {
post {
addView(it.view, _width, _width)
AnimatorSet().apply {
play(
ObjectAnimator.ofFloat(
it.view,
“y”,
0F,
[email protected] / 2f
)
).with(
ObjectAnimator.ofFloat(it.view, “rotation”, 0F, 360F)
)
doOnEnd { _ -> it.view.rotation = 0F }
duration = 1000
}.start()
}
}
}
}

开场动画

游戏开始时,将潜艇通过动画移动到起始位置,即y轴的二分之一处

/**
* 游戏开始时通过动画进入
*/
@MainThread
fun start() {
_isStop = false
if (boat == null) {
boat = Boat(context).also {
post {
addView(it.view, _width, _width)
AnimatorSet().apply {
play(
ObjectAnimator.ofFloat(
it.view,
“y”,
0F,
[email protected] / 2f
)
).with(
ObjectAnimator.ofFloat(it.view, “rotation”, 0F, 360F)
)
doOnEnd { _ -> it.view.rotation = 0F }
duration = 1000
}.start()
}
}
}
}

4、相机(Camera)

相机部分主要有TextureView和CameraHelper组成。TextureView提供给Camera承载preview;工具类CameraHelper主要完成以下功能:

  • 开启相机:通过CameraManger打开摄像头
  • 摄像头切换:切换前后置摄像头,
  • 预览:获取Camera提供的可预览尺寸,并适配TextureView显示
  • 人脸识别:检测人脸位置,进行TestureView上的坐标变换

适配PreviewSize

相机硬件提供的可预览尺寸与屏幕实际尺寸(即TextureView尺寸)可能不一致,所以需要在相机初始化时,选取最合适的PreviewSize,避免TextureView上发生画面拉伸等异常

class CameraHelper(val mActivity: Activity, private val mTextureView: TextureView) {

private lateinit var mCameraManager: CameraManager
private var mCameraDevice: CameraDevice? = null
private var mCameraCaptureSession: CameraCaptureSession? = null

private var canExchangeCamera = false                                               //是否可以切换摄像头
private var mFaceDetectMatrix = Matrix()                                            //人脸检测坐标转换矩阵
private var mFacesRect = ArrayList()                                         //保存人脸坐标信息
private var mFaceDetectListener: FaceDetectListener? = null                         //人脸检测回调
private lateinit var mPreviewSize: Size

/**
* 初始化
*/
private fun initCameraInfo() {
mCameraManager = mActivity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
val cameraIdList = mCameraManager.cameraIdList
if (cameraIdList.isEmpty()) {
mActivity.toast(“没有可用相机”)
return
}

//获取摄像头方向
mCameraSensorOrientation =
mCameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
//获取StreamConfigurationMap,它是管理摄像头支持的所有输出格式和尺寸
val configurationMap =
mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!

val previewSize = configurationMap.getOutputSizes(SurfaceTexture::class.java) //预览尺寸

// 当屏幕为垂直的时候需要把宽高值进行调换,保证宽大于高
mPreviewSize = getBestSize(
mTextureView.height,
mTextureView.width,
previewSize.toList()
)

//根据preview的size设置TextureView
mTextureView.surfaceTexture.setDefaultBufferSize(mPreviewSize.width, mPreviewSize.height)
mTextureView.setAspectRatio(mPreviewSize.height, mPreviewSize.width)
}

选取preview尺寸的原则与TextureView的长宽比尽量一致,且面积尽量接近。

initFaceDetect()用来进行人脸的Matrix初始化,后文介绍。

人脸识别

为相机预览,创建一个CameraCaptureSession对象,会话通过CameraCaptureSession.CaptureCallback返回TotalCaptureResult,通过参数可以让其中包括人脸识别的相关信息

/**
* 创建预览会话
*/
private fun createCaptureSession(cameraDevice: CameraDevice) {

// 为相机预览,创建一个CameraCaptureSession对象
cameraDevice.createCaptureSession(
arrayListOf(surface),
object : CameraCaptureSession.StateCallback() {

override fun onConfigured(session: CameraCaptureSession) {
mCameraCaptureSession = session
session.setRepeatingRequest(
captureRequestBuilder.build(),
mCaptureCallBack,
mCameraHandler
)
}

},
mCameraHandler
)
}

private val mCaptureCallBack = object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
super.onCaptureCompleted(session, request, result)
if (mFaceDetectMode != CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF)
handleFaces(result)

}
}

通过mFaceDetectMatrix对人脸信息进行矩阵变化,确定人脸坐标以使其准确应用到TextureView。

/**
* 处理人脸信息
*/
private fun handleFaces(result: TotalCaptureResult) {
val faces = result.get(CaptureResult.STATISTICS_FACES)!!
mFacesRect.clear()

for (face in faces) {
val bounds = face.bounds

val left = bounds.left
val top = bounds.top
val right = bounds.right
val bottom = bounds.bottom

val rawFaceRect =
RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
mFaceDetectMatrix.mapRect(rawFaceRect)

var resultFaceRect = if (mCameraFacing == CaptureRequest.LENS_FACING_FRONT) {
rawFaceRect
} else {
RectF(
rawFaceRect.left,
rawFaceRect.top - mPreviewSize.width,
rawFaceRect.right,
rawFaceRect.bottom - mPreviewSize.width
)
}

mFacesRect.add(resultFaceRect)

}

mActivity.runOnUiThread {
mFaceDetectListener?.onFaceDetect(faces, mFacesRect)
}
}

最后,在UI线程将包含人脸坐标的Rect通过回调传出:

mActivity.runOnUiThread {
mFaceDetectListener?.onFaceDetect(faces, mFacesRect)
}

FaceDetectMatrix

mFaceDetectMatrix是在获取PreviewSize之后创建的

/**
* 初始化人脸检测相关信息
*/
private fun initFaceDetect() {

val faceDetectModes =
mCameraCharacteristics.get(CameraCharacteristics.STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES)  //人脸检测的模式

mFaceDetectMode = when {
faceDetectModes!!.contains(CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL) -> CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL
faceDetectModes!!.contains(CaptureRequest.STATISTICS_FACE_DETECT_MODE_SIMPLE) -> CaptureRequest.STATISTICS_FACE_DETECT_MODE_FULL
else -> CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF
}

if (mFaceDetectMode == CaptureRequest.STATISTICS_FACE_DETECT_MODE_OFF) {
mActivity.toast(“相机硬件不支持人脸检测”)
return
}

val activeArraySizeRect =
mCameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)!! //获取成像区域
val scaledWidth = mPreviewSize.width / activeArraySizeRect.width().toFloat()
val scaledHeight = mPreviewSize.height / activeArraySizeRect.height().toFloat()

val mirror = mCameraFacing == CameraCharacteristics.LENS_FACING_FRONT

mFaceDetectMatrix.setRotate(mCameraSensorOrientation.toFloat())
mFaceDetectMatrix.postScale(if (mirror) -scaledHeight else scaledHeight, scaledWidth)// 注意交换width和height的位置!
mFaceDetectMatrix.postTranslate(
mPreviewSize.height.toFloat(),
mPreviewSize.width.toFloat()
)

}

5、控制类(GameController)

三大视图层组装完毕,最后需要一个总控类,对游戏进行逻辑控制

主要完成以下工作:

  • 控制游戏的开启/停止
  • 计算游戏的当前得分
  • 检测潜艇的碰撞
  • 对外(Activity或者Fragment等)提供游戏状态监听的接口

初始化

游戏开始时进行相机的初始化,创建GameHelper类并建立setFaceDetectListener回调到ForegroundView

class GameController(
private val activity: AppCompatActivity,
private val textureView: AutoFitTextureView,
private val bg: BackgroundView,
private val fg: ForegroundView
) {

private var camera2HelperFace: CameraHelper? = null
/**
* 相机初始化
*/
private fun initCamera() {
cameraHelper ?: run {
cameraHelper = CameraHelper(activity, textureView).apply {
setFaceDetectListener(object : Android-手撸抖音“潜艇大挑战”,非科班面试之旅_第1张图片
CameraHelper.FaceDetectListener {
override fun onFaceDetect(faces: Array, facesRect: ArrayList) {
if (facesRect.isNotEmpty()) {
fg.onFaceDetect(faces, facesRect)
}
}
})
}
}
}

游戏状态

定义GameState,对外提供状态的监听。目前支持三种状态

  • Start:游戏开始
  • Over:游戏结束
  • Score:游戏得分

sealed class GameState(open val score: Long) {
object Start : GameState(0)
data class Over(override val score: Long) : GameState(score)
data class Score(override val score: Long) : GameState(score)
}

可以在stop、start的时候,更新状态

/**
* 游戏状态
*/
private val _state = MutableLiveData()
internal val gameState: LiveData
get() = _state

/**
* 游戏停止
*/
fun stop() {
bg.stop()
fg.stop()
_state.value = GameState.Over(_score)
_score = 0L
}

/**
* 游戏开始
*/
fun start() {
initCamera()
fg.start()
bg.start()
_state.value = GameState.Start
handler.postDelayed({
startScoring()
}, FIRST_APPEAR_DELAY_MILLIS)
}

计算得分

游戏启动时通过startScoring开始计算得分并通过GameState上报。

目前的规则设置很简单,存活时间即游戏得分

/**
* 开始计分
*/
private fun startScoring() {
handler.postDelayed(
{
fg.boat?.run {
bg.barsList.flatMap { listOf(it.up, it.down) }
.forEach { bar ->
if (isCollision(
bar.x, bar.y, bar.w, bar.h,
this.x, this.y, this.w, this.h
)
) {
stop()
return@postDelayed
}
}
}
_score++
_state.value = GameState.Score(_score)
startScoring()
}, 100
)
}

检测碰撞

isCollision根据潜艇和障碍物当前位置,计算是否发生了碰撞,发生碰撞则GameOver

/**
* 碰撞检测
*/
private fun isCollision(
x1: Float,
y1: Float,
w1: Float,
h1: Float,
x2: Float,
y2: Float,
w2: Float,
h2: Float
): Boolean {
if (x1 > x2 + w2 || x1 + w1 < x2 || y1 > y2 + h2 || y1 + h1 < y2) {
return false
}
return true
}

6、Activity

Activity的工作简单:

  • 权限申请:动态申请Camera权限
  • 监听游戏状态:创建GameController,并监听GameState状态

private fun startGame() {
PermissionUtils.checkPermission(this, Runnable {
gameController.start()
gameController.gameState.observe(this, Observer {
when (it) {
is GameState.Start ->
score.text = “DANGER\nAHEAD”
is GameState.Score ->
score.text = " i t . s c o r e   /   10 f   m " i s   G a m e S t a t e . O v e r   − > A l e r t D i a l o g . B u i l d e r ( t h i s ) . s e t M e s s a g e ( " 游 戏 结 束 ! 成 功 推 进   {it.score / 10f} m" is GameState.Over -> AlertDialog.Builder(this) .setMessage("游戏结束!成功推进  it.score / 10f m"is GameState.Over >AlertDialog.Builder(this).setMessage(" {it.score / 10f} 米! ")
.setNegativeButton(“结束游戏”) { _: DialogInterface, _: Int ->
finish()
}.setCancelable(false)
.setPositiveButton(“再来一把”) { _: DialogInterface, _: Int ->
gameController.start()
}.show()
}
})
})
}

ame() {
PermissionUtils.checkPermission(this, Runnable {
gameController.start()
gameController.gameState.observe(this, Observer {
when (it) {
is GameState.Start ->
score.text = “DANGER\nAHEAD”
is GameState.Score ->
score.text = " i t . s c o r e   /   10 f   m " i s   G a m e S t a t e . O v e r   − > A l e r t D i a l o g . B u i l d e r ( t h i s ) . s e t M e s s a g e ( " 游 戏 结 束 ! 成 功 推 进   {it.score / 10f} m" is GameState.Over -> AlertDialog.Builder(this) .setMessage("游戏结束!成功推进  it.score / 10f m"is GameState.Over >AlertDialog.Builder(this).setMessage(" {it.score / 10f} 米! ")
.setNegativeButton(“结束游戏”) { _: DialogInterface, _: Int ->
finish()
}.setCancelable(false)
.setPositiveButton(“再来一把”) { _: DialogInterface, _: Int ->
gameController.start()
}.show()
}
})
})
}

你可能感兴趣的:(程序员,架构,移动开发,android)