Android原生开发,自定义View手撸一个抖音“潜艇大挑战

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移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

最后

都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。

技术发展的这么快,从哪些方面开始学习,才能达到高级工程师水平,最后进阶到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 小时用来学习。

不出半年,你就能看出变化!

  • 30
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值