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()
}
刷新障碍物
障碍物的刷新经历三个步骤:
- 创建:上下两个为一组创建障碍物
- 添加:将对象添加到barsList,同时将View添加到容器
- 移动:通过属性动画从右侧移动到左侧,并在移出屏幕后删除
创建障碍物时会为其设置随机高度,随机不能太过,要以前一个障碍物为基础进行适当调整,保证随机的同时兼具连贯性
/**
- 创建障碍物(上下两个为一组)
*/
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移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。
最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
【算法合集】
【延伸Android必备知识点】
【Android部分高级架构视频学习资源】
Android精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
分。
还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
【Android核心高级技术PDF文档,BAT大厂面试真题解析】
[外链图片转存中…(img-u0GjzAOd-1712550675233)]
【算法合集】
[外链图片转存中…(img-oLfp28Dg-1712550675233)]
【延伸Android必备知识点】
[外链图片转存中…(img-uppVhjVR-1712550675233)]
【Android部分高级架构视频学习资源】
Android精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!