class ForegroundView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs),
CameraHelper.FaceDetectListener {
private var _isStop: Boolean = false
internal var boat: Boat? = null
/**
- 游戏停止,潜艇不再移动
*/
@MainThread
fun stop() {
_isStop = true
}
/**
- 接受人脸识别的回调,移动位置
*/
override fun onFaceDetect(faces: Array, facesRect: ArrayList) {
if (_isStop) return
if (facesRect.isNotEmpty()) {
boat?.run {
val face = facesRect.first()
val x = (face.left - _widthOffset).toInt()
val y = (face.top + _heightOffset).toInt()
moveTo(x, y)
}
_face = facesRect.first()
}
}
}
开场动画
游戏开始时,将潜艇通过动画移动到起始位置,即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,
this@ForegroundView.height / 2f
)
).with(
ObjectAnimator.ofFloat(it.view, “rotation”, 0F, 360F)
)
doOnEnd { _ -> it.view.rotation = 0F }
duration = 1000
}.start()
}
}
}
}
相机(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的长宽比尽量一致,且面积尽量接近。
private fun getBestSize(
targetWidth: Int,
targetHeight: Int,
sizeList: List
): Size {
val bigEnough = ArrayList() //比指定宽高大的Size列表
val notBigEnough = ArrayList() //比指定宽高小的Size列表
for (size in sizeList) {
//宽高比 == 目标值宽高比
if (size.width == size.height * targetWidth / targetHeight
) {
if (size.width >= targetWidth && size.height >= targetHeight)
bigEnough.add(size)
else
notBigEnough.add(size)
}
}
//选择bigEnough中最小的值 或 notBigEnough中最大的值
return when {
bigEnough.size > 0 -> Collections.min(bigEnough, CompareSizesByArea())
notBigEnough.size > 0 -> Collections.max(notBigEnough, CompareSizesByArea())
else -> sizeList[0]
}
initFaceDetect()
}
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()
)
}
控制类(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 : 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
}
Activity
Activity的工作简单:
- 权限申请:动态申请Camera权限
- 监听游戏状态:创建GameController,并监听GameState状态
private fun startGame() {
PermissionUtils.checkPermission(this, Runnable {
gameController.start()
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
最后
都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。
技术发展的这么快,从哪些方面开始学习,才能达到高级工程师水平,最后进阶到Android架构师/技术专家?我总结了这 5大块;
我搜集整理过这几年阿里,以及腾讯,字节跳动,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 PDF(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
Java语言与原理;
大厂,小厂。Android面试先看你熟不熟悉Java语言
高级UI与自定义view;
自定义view,Android开发的基本功。
性能调优;
数据结构算法,设计模式。都是这里面的关键基础和重点需要熟练的。
NDK开发;
未来的方向,高薪必会。
前沿技术;
组件化,热升级,热修复,框架设计
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多,GitHub可见;《Android架构视频+学习笔记》
当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。
不出半年,你就能看出变化!
。都是这里面的关键基础和重点需要熟练的。
[外链图片转存中…(img-GlDGMDav-1710776826317)]
NDK开发;
未来的方向,高薪必会。
[外链图片转存中…(img-udkhGUnB-1710776826318)]
前沿技术;
组件化,热升级,热修复,框架设计
[外链图片转存中…(img-1V2vual1-1710776826318)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多,GitHub可见;《Android架构视频+学习笔记》
当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。
不出半年,你就能看出变化!