Android原生开发,自定义View手撸一个抖音“潜艇大挑战”小游戏。,这份1307页Android面试全套真题解析

set(value) {
view.x = value
field = value
}

val y
get() = view.y

internal val view by lazy {
BarView(context) {
it?.apply {
drawBitmap(
bmp,
srcRect,
dstRect,
paint
)
}
}
}

}

internal class BarView(context: Context?, private val block: (Canvas?) -> Unit) :
View(context) {

override fun onDraw(canvas: Canvas?) {
block((canvas))
}
}

障碍物分为上方和下方两种,由于使用了同一张资源,所以绘制时要区别对待,因此定义了两个子类:UpBar和DnBar

/**

  • 屏幕上方障碍物
    */
    class UpBar(context: Context, container: ViewGroup) : Bar(context) {

private val _srcRect by lazy(LazyThreadSafetyMode.NONE) {
Rect(0, (bmp.height * (1 - (h / container.height))).toInt(), bmp.width, bmp.height)
}
override val srcRect: Rect
get() = _srcRect

}

下方障碍物的资源旋转180度后绘制

/**

  • 屏幕下方障碍物
    */
    class DnBar(context: Context, container: ViewGroup) : Bar(context) {

override val bmp = super.bmp.let {
Bitmap.createBitmap(
it, 0, 0, it.width, it.height,
Matrix().apply { postRotate(-180F) }, true
)
}

private val _srcRect by lazy(LazyThreadSafetyMode.NONE) {
Rect(0, 0, bmp.width, (bmp.height * (h / container.height)).toInt())
}

override val srcRect: Rect
get() = _srcRect
}

BackgroundView

接下来创建后景的容器BackgroundView,容器用来定时地创建、并移动障碍物。

通过列表barsList管理当前所有的障碍物,onLayout中,将障碍物分别布局到屏幕上方和下方

/**

  • 后景容器类
    */
    class BackgroundView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {

internal val barsList = mutableListOf()

override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
barsList.flatMap { listOf(it.up, it.down) }.forEach {
val w = it.view.measuredWidth
val h = it.view.measuredHeight
when (it) {
is UpBar -> it.view.layout(0, 0, w, h)
else -> it.view.layout(0, height - h, w, height)
}
}
}

提供两个方法start和stop,控制游戏的开始和结束:

  • 游戏结束时,要求所有障碍物停止移动。
  • 游戏开始后会通过Timer,定时刷新障碍物

/**

  • 游戏结束,停止所有障碍物的移动
    */
    @UiThread
    fun stop() {
    _timer.cancel()
    _anims.forEach { it.cancel() }
    _anims.clear()
    }

/**

  • 定时刷新障碍物:
  • 1. 创建
  • 2. 添加到视图
  • 3. 移动
    */
    @UiThread
    fun start() {
    _clearBars()
    Timer().also { _timer = it }.schedule(object : TimerTask() {
    override fun run() {
    post {
    _createBars(context, barsList.lastOrNull()).let {
    _addBars(it)
    _moveBars(it)
    }
    }
    }

}, FIRST_APPEAR_DELAY_MILLIS, BAR_APPEAR_INTERVAL_MILLIS
)
}

/**

  • 游戏重启时,清空障碍物
    */
    private fun _clearBars() {
    barsList.clear()
    removeAllViews()
    }

刷新障碍物

障碍物的刷新经历三个步骤:

  1. 创建:上下两个为一组创建障碍物
  2. 添加:将对象添加到barsList,同时将View添加到容器
  3. 移动:通过属性动画从右侧移动到左侧,并在移出屏幕后删除

创建障碍物时会为其设置随机高度,随机不能太过,要以前一个障碍物为基础进行适当调整,保证随机的同时兼具连贯性

/**

  • 创建障碍物(上下两个为一组)
    */
    private fun _createBars(context: Context, pre: Bars?) = run {
    val up = UpBar(context, this).apply {
    h = pre?.let {
    val step = when {
    it.up.h >= height - _gap - _step -> -_step
    it.up.h <= _step -> _step
    _random.nextBoolean() -> _step
    else -> -_step
    }
    it.up.h + step
    } ?: _barHeight
    w = _barWidth
    }

val down = DnBar(context, this).apply {
h = height - up.h - _gap
w = _barWidth
}

Bars(up, down)

}

/**

  • 添加到屏幕
    */
    private fun _addBars(bars: Bars) {
    barsList.add(bars)
    bars.asArray().forEach {
    addView(
    it.view,
    ViewGroup.LayoutParams(
    it.w.toInt(),
    it.h.toInt()
    )
    )
    }
    }

/**

  • 使用属性动画移动障碍物
    */
    private fun _moveBars(bars: Bars) {
    _anims.add(
    ValueAnimator.ofFloat(width.toFloat(), -_barWidth)
    .apply {
    addUpdateListener {
    bars.asArray().forEach { bar ->
    bar.x = it.animatedValue as Float
    if (bar.x + bar.w <= 0) {
    post { removeView(bar.view) }
    }
    }
    }

duration = BAR_MOVE_DURATION_MILLIS
interpolator = LinearInterpolator()
start()
})
}

}

前景(Foreground)

Boat

定义潜艇类Boat,创建自定义View,并提供方法移动到指定坐标

/**

  • 潜艇类
    */
    class Boat(context: Context) {

internal val view by lazy { BoatView(context) }

val h
get() = view.height.toFloat()

val w
get() = view.width.toFloat()

val x
get() = view.x

val y
get() = view.y

/**

  • 移动到指定坐标
    */
    fun moveTo(x: Int, y: Int) {
    view.smoothMoveTo(x, y)
    }

}

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根据人脸识别的回调,移动潜艇到指定位置
  • 游戏开始时,创建潜艇并做开场动画

/**

  • 前景容器类
    */
    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通过回调传出:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水!

为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!

Android架构师之路很漫长,一起共勉吧!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水!

为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!

Android架构师之路很漫长,一起共勉吧!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值