ARCore 使用 SceneForm 框架 —— 自定义每个单元格都有点击响应的 3D 规则镂空多面体

通常情况下,显示一个 3D 模型,只要有对应的资源就可以实现了,但是这个仅仅是通常情况,肯定会有特殊情况的,这不刚好憋了好长时间,需要憋出一个特殊情况的大招;实现一个规则锥形多面体,不过在此基础上支持配置成圆柱形规则多面体

先看看效果图
在这里插入图片描述在这里插入图片描述

多面体实现思路

看起来是一个很复杂的东西,不过拆解出来后,思路就会变得比较简单了

先确定一个加载点的位置,然后加载若干个面,每个面都是相对于加载点的位置显示,根据一定规则,确定每一个面的尺寸大小,最后旋转加载点,就完成了上述的效果

多面体类

多面体的配置文件,是实现的关键,内部会支持设置行、列、宽、高、递减模式、递减比例

// 多面体的配置
class ObjCylinderConfig(y : Float) : ObjConfig() {
    var ifDebug = false                     //是否开启调试模式
    val DEFAULT_ROW = 3                     //常量-默认行数
    val DEFAULT_COLUMN = 15                 //常量-默认列数
    var context : Context? = null
    // 默认宽度 1 米
    // 开启递减模式,最大圆所在的行半径为 0.5 米
    // 未开启递减模式,所有行的圆都是 0.5 米
    var y = 1f
        get() = field
        set(value) {
            field = value
        }

    var widthScaleHeight : Float = 3f/4             // 单元格的宽高比(宽屏 3:4)
    var checkSizeScale : Float = 1f                 // 单元格的矫正比例
    var rowDecreaseScale = 0.9f                     // 每一行的尺寸缩小比例,从中间行开始
    var ifRowViewDecreaseScale = true               // 每行尺寸是否缩小的开关
    var checkRowSpace = 0f;                         // 用于调整行间距,根据情况自行设置
    var checkRowScale = 3/7f;                       // 用于调整行间距比例,避免每行行间距过宽,存在逐行递减,所以需要动态根据比例计算
    var objCylinderAdapter : ObjCylinderAdapter? = null // 网格内容适配器
    var tmpModel : ModelRenderable? = null          // 用于标识中心点的调试点
    var debugPointExtent = 0.02f                    // 调试点是一个球体,代表该球体的半径
    private var nodeSet : MutableList<MutableList<Node>> = ArrayList() //管理多面体所有的节点集合
    var listener : OnItemClickListener? = null      // 点击事件监听器

    var row = DEFAULT_ROW                           // 多面体的行数
        get() = field
        set(value) {
            if (value <= 0) {
                field = DEFAULT_ROW
            } else {
                field = value
            }
        }
    var column = DEFAULT_COLUMN                     // 多面体的列数
        get() = field
        set(value) {
            if (value <= 0) {
                field = DEFAULT_COLUMN
            } else {
                field = value
            }
        }

    // 对应列数的角度
    fun getCellRowAngle() : Float {
        return 360f / column
    }

    // 根据列数,计算最大半径所在的行,较为合适的网格宽度,可设置
    var itemWidth = Math.abs(Math.sin(getCellRowAngle().toDouble() / 2).toFloat()) * y/2 * checkSizeScale * 5f/3
    // 根据宽高比,计算最大半径所在的行,较为合适的高度,可以设置宽高比调整
    var itemHeight = itemWidth * widthScaleHeight

    // 构造函数
    init {
        this.y = y
    }

    // 构造函数
    constructor() : this(1f) {
    }

    // 加载调试模式下球体的纹理资源
    fun loadDebugPointModel(resId : Int) {
        Texture.builder().setSource(context, resId).build()
            .thenAccept(
                { texture ->
                    MaterialFactory.makeOpaqueWithTexture(context, texture)
                        .thenAccept { material ->
                            tmpModel = ShapeFactory.makeSphere(debugPointExtent, Vector3(0f, 0f, 0f), material)
                            tmpModel!!.isShadowCaster = false
                        }
                }
            )
    }


    // 构建所有基于父节点的面,从而形成多面体
    fun loadAllFace(parent : Node) {

        // 没有行,不加载任何一个面
        if (row == 0) {
            return
        }
        nodeSet.clear()

        // 找到在当前总行数下,中间行的 ID,中间行可能是一行,也可能是时两行
        var centreRow : MutableList<Int> = findCenterRow()

        // 根据行数,逐行绘制每一行所有的面
        for (i in 0 .. (row  - 1)) {
            // 当前行所有面所在节点的集合
            var tmpList : MutableList<Node> = ArrayList()
            var rowCellSizeScale = caculateCurrentRowSizeScaleInfo(i, centreRow)
            var checkHeight = caculateCurrentRowCheckHeight(i, centreRow)
            var checkAxisX = caculateCurrentRowAxisX(i, centreRow)
            var center = caculateLookAtPostion(checkAxisX, checkHeight, i, centreRow)

            // 逐个构造当前行所有的节点
            for (j in 0 .. (column - 1)) {

                var node = Node()
                node.setParent(parent)
                // 根据当前节点的序号,以及偏转角度,计算当前节点所在位置
                var pos = Vector3(checkAxisX, 0f + checkHeight, 0f)
                val rowRot = Vector3(0f, getCellRowAngle() * j, 0f)
                val rowRotQuaternion = Quaternion.eulerAngles(rowRot)
                val showPos = Quaternion.rotateVector(rowRotQuaternion, pos)

                // 为当前节点加载平面模型
                ViewRenderable.builder()
                    .setView(context, objCylinderAdapter!!.layoutId)
                    .setSizer(DpToMetersViewSizer(ParamKey.DPPERMETER)) // 设置尺寸比例
                    .build()
                    .thenAccept { it ->
                        it.isShadowCaster = false

                        // 通过适配器,获取加载了显示内容的模型
                        if (objCylinderAdapter != null) {
                            var tmp = it//objCylinderAdapter!!.getCellViewRenderable(i, j)
                            // 控制模型中心点的位置是节点,如果不设置,中心点会在模型的底部
                            tmp.horizontalAlignment = ViewRenderable.HorizontalAlignment.CENTER
                            tmp.verticalAlignment = ViewRenderable.VerticalAlignment.CENTER
                            // 设置模型的初始尺寸
                            tmp.view.layoutParams.height = (itemHeight * ParamKey.DPPERMETER).toInt()
                            tmp.view.layoutParams.width = (itemWidth * ParamKey.DPPERMETER).toInt()

                            tmp = objCylinderAdapter!!.getCellViewRenderable(tmp, i, j)
                            node.renderable = tmp
                        }
                        // 设置显示的相对位置
                        node.localPosition = showPos
                        // 根据比例和模型的初始尺寸,设置模型实际的大小
                        node.localScale = Vector3(rowCellSizeScale, rowCellSizeScale, 1f)

                        // 根据中心点,设置模型朝向的方向
                        val direction = Vector3.subtract(center, node.localPosition)
                        node.localRotation = Quaternion.lookRotation(direction, Vector3.up())

                        // 向调用方提供的点击事件的监听器
                        node.setOnTapListener(object : Node.OnTapListener {
                            override fun onTap(p0: HitTestResult?, p1: MotionEvent?) {
                                if (listener != null) {
                                    listener!!.onItemClicked(i , j)
                                }
                            }
                        })
                        tmpList.add(node)
                    }

                if (ifDebug && tmpModel != null) {  // 开启 debug 模式的时候显示中心点的位置
                    var nodeDebug = Node()
                    nodeDebug.setParent(parent)
                    nodeDebug.localPosition = showPos
                    nodeDebug.renderable = tmpModel
                }
            }
            nodeSet.add(tmpList)
        }
    }

    // 每一行的倾角是通过看向指定点的构造出来的
    // 计算每一行对应的朝向的点
    // xLong 每一行的半径
    // checkHeight 每一行所在的高度
    // currentRowNo 当前行号
    // centreRow 中间行的集合
    private fun caculateLookAtPostion(xLong : Float, checkHeight : Float, currentRowNo : Int, centreRow : MutableList<Int>) : Vector3 {
        if (ifRowViewDecreaseScale) {       // 开启递减模式
            var finalAngel = 0f             // 初始的倾斜角度
            var decreaseFoot = 90 * (1 - rowDecreaseScale)   // 每一行的倾角度数
            var finalCenterHeight = checkHeight
            if (currentRowNo > centreRow.last()) {
                finalAngel = decreaseFoot
            } else if (currentRowNo < centreRow[0]) {
                finalAngel = decreaseFoot * -1
            }

            // 根据朝向角度,计算朝向点的位置
            finalCenterHeight += Math.tan(finalAngel.toDouble()).toFloat() * xLong
            return Vector3(0f, 0f + finalCenterHeight, 0f)
        } else {
            return Vector3(0f, 0f + checkHeight, 0f)
        }
    }

    // 找到中间行的行号,可能中间行有两行,可能只有一行
    private fun findCenterRow() : MutableList<Int> {
        var centreRow : MutableList<Int> = ArrayList()
        // 没有唯一中间行
        if (row % 2 == 0) {
            row / 2
            centreRow.add(row / 2 - 1)
            centreRow.add(row / 2)
        } else { // 存在唯一中建行
            centreRow.add(row / 2)
        }
        return centreRow
    }

    // 计算当前行的半径
    private fun caculateCurrentRowAxisX(currentRowNo : Int, centreRow : MutableList<Int>) : Float {
        var xAxisFoot = y / 2                 // 最大半径
        if (ifRowViewDecreaseScale) {               // 开启递减模式
            var ret = 0f;
            if (centreRow.contains(currentRowNo)) {             // 当前行是中间行
                return xAxisFoot * (rowDecreaseScale + 0.05f)   // 微调最大行的半径
//                return xAxisFoot                              // 可直接返回最大行的半径,根据实际情况调整
            } else if (currentRowNo > centreRow.last()) {       // 非中间行,半径按比例指数缩小
                ret = xAxisFoot * Math.pow(rowDecreaseScale.toDouble(), (currentRowNo - centreRow.last()).toDouble()).toFloat()
            } else if (currentRowNo < centreRow[0]) {           // 非中间行,半径按比例指数缩小
                ret = xAxisFoot * Math.pow(rowDecreaseScale.toDouble(), (centreRow[0] - currentRowNo).toDouble()).toFloat()
            }
            return ret
        } else {
            return xAxisFoot        // 未开启递减模式,直接返回最大半径
        }
    }


    // 计算当前行的行高
    private fun caculateCurrentRowCheckHeight(currentRowNo : Int, centreRow : MutableList<Int>) : Float {
        if (!ifRowViewDecreaseScale) {      //未开启递减模式
            var ret = 0f;
            var yAxisFoot = itemHeight * checkRowScale + checkRowSpace
            if (centreRow.size == 2) {  // 双数行,真正中间位置调整
                if (currentRowNo >= centreRow.last()) {
                    ret = (currentRowNo - centreRow.last()) * yAxisFoot
                } else {
                    ret = (currentRowNo - centreRow[0] - 1) * yAxisFoot
                }
            } else {  //单数行,中间行不需要调整
                ret = (currentRowNo - centreRow[0]) * yAxisFoot
            }
            return ret
        } else {        // 开启递减模式
            // 计算当前行与中间行的行差
            var tmp = 0
            if (centreRow.contains(currentRowNo)) {
                if (centreRow.size == 2) {  // 中间行有两行的时候,以行数大的行作为基准行
                    return (currentRowNo - centreRow.last()) * itemHeight * checkRowScale
                } else {
                    return 0f;
                }
            } else if (currentRowNo < centreRow[0]) {
                tmp = currentRowNo - centreRow[0]
            } else {
                tmp = currentRowNo - centreRow.last()
            }

            // 根据行差,计算每一行的高度,每一行的比例都已正数计算,后续会对对称行的正数高度做修正
            var collectRet = 0f
            for (j in 0 .. (Math.abs(tmp) - 1)) {
                collectRet += itemHeight * checkRowScale * Math.pow(rowDecreaseScale.toDouble(), j.toDouble()).toFloat() + checkRowSpace
            }

            // 对以中间行对称行号的高度,取负数修正
            if (tmp < 0) {
                collectRet *= -1
                if (centreRow.size == 2) {
                    collectRet -= itemHeight * checkRowScale
                }
            }
            return collectRet
        }
    }

    // 根据是否尺寸递减开关是否打开,计算每一行单元格的尺寸
    private fun caculateCurrentRowSizeInfo(currentRowNo : Int, centreRow : MutableList<Int>) : RowCellSize {
        var ret = RowCellSize(itemHeight, itemWidth)
        if (ifRowViewDecreaseScale) {       // 尺寸递减开关打开
            var times = caculateCurrentRowSizeScaleInfo(currentRowNo, centreRow)
            ret.cellItemHeight *= times
            ret.cellItemWidth *= times
        }
        return ret
    }

    // 计算当前行网格元素显示的比例
    private fun caculateCurrentRowSizeScaleInfo(currentRowNo : Int, centreRow : MutableList<Int>) : Float {
        var times = 1f
        if (ifRowViewDecreaseScale) {       // 开启尺寸递减
            // 除过中间行,向向下均已 0.9 的比例缩小
            if (currentRowNo < centreRow[0]) {
                times = Math.pow((rowDecreaseScale).toDouble(), (centreRow[0] - currentRowNo).toDouble()).toFloat()
            } else if (currentRowNo > centreRow.last()) {
                times = Math.pow((rowDecreaseScale).toDouble(), (currentRowNo - centreRow.last()).toDouble()).toFloat()
            }
        }
        return times
    }

    data class RowCellSize(var cellItemHeight : Float, var cellItemWidth : Float)
}

圆主体配置类依赖的 ObjConfig

// ObjCylinderConfig 的基类
// 在本 demo 中没有使用,可以去除
open class ObjConfig (){
    var color : Color = Color(255f, 255f, 255f, 255f)
        get() = field
        set(value) {
            field = value
        }

    init {
        color = Color(255f, 255f, 255f, 255f)
    }

}

点击事件的监听器

interface OnItemClickListener {
    fun onItemClicked(row : Int, column : Int)
}

多面体数据内容适配器

效果图里可以看到,在其中一个网格内有图片显示,该图片是通过适配器,加载进模型的,适配器的实现如下

/**
 * 网格元素内容内容填充适配器
 */
class ObjCylinderAdapter(content : MutableMap<String, ItemContent>) {
    // 网格元素使用的布局
    // 当前的布局只有一个 ImageView
    var layoutId = R.layout.cylinder_item_layout
    // 网格元素显示的数据内容集合
    // 当前使用的 key 为 Postion 做了 JSON 处理的字符串,可根据实际情况自行处理
    // value 为要显示的数据内容
    var content : MutableMap<String, ItemContent> = HashMap()
        get() = field
        set(value) {
            if (value == null) {
                field = HashMap()
            } else {
                field = value
            }
        }

    init {
        this.content = content
    }

    // 为每一个网格元素添加内容
    fun getCellViewRenderable(viewRenderable : ViewRenderable, row : Int, column : Int) : ViewRenderable{
        var tmp = viewRenderable

        // 当对应的网格位置存在数据内容的时候,为 ImageView 加载图片资源
        var itemContent = content.get(JSON.toJSONString(Postion(row, column)))
        var img = tmp.view.img as ImageView
        if (itemContent != null) {
            img.setImageResource(itemContent.image!!)
        }
        return tmp
    }

}

content 中的位置信息 String 是 Postion JSON 后的字符串, ItemContent 是每一个网格对应数据内容

// 包含每一格网格要显示的数据内容
class ItemContent {
    // 图片资源 ID,该属性可以根据实际情况做处理
    // 仅与适配器 ObjCylinderAdapter 相关联
    var image : Int? = null

    // 当前元素要显示的位置,对于本 demo 来说可以省略
    var pos : Postion = Postion();
}
class Postion(row : Int, column : Int) {
    // 行号
    var row : Int? = null
    // 列号
    var column : Int? = null

    init {
        this.row = row
        this.column = column
    }

    constructor() : this(0, 0) {

    }
}

主界面加载多面体模型

主界面加载模型就比较简单了,只是设置一下属性,同时让旋转节点开始旋转

class MainActivity : AppCompatActivity() {
    private val TAG = "XXX"
    private var arFragment: CleanArFragment? = null

    private val EXTENTX_CYLINDER = 0.5f // 椭圆主体的宽度
    var config : ObjCylinderConfig = ObjCylinderConfig()
        get() = field
        set(value) {
            if (value == null) {
                field = ObjCylinderConfig();
            } else {
                field = value
            }
        }

    // 初始化显示的数据
    private fun initContent2Show() : MutableMap<String, ItemContent> {
        val content = HashMap<String, ItemContent>()
                val showEle = ItemContent()
                showEle.image = R.drawable.ic_launcher
                showEle.pos = Postion(0, 0)
                content.put(JSON.toJSONString(showEle.pos), showEle)

        return content
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        arFragment = supportFragmentManager.findFragmentById(R.id.ux_fragment) as CleanArFragment?
//        arFragment!!.getArSceneView().getPlaneRenderer().setVisible(false)
        config = ObjCylinderConfig(EXTENTX_CYLINDER)    // 设置最大行半径
        config.objCylinderAdapter = ObjCylinderAdapter(initContent2Show())  // 设置适配器

        config.context = this@MainActivity
        config.ifRowViewDecreaseScale = false
        config.ifDebug = false          // 关闭调试模式
        config.loadDebugPointModel(R.drawable.final_point)  // 设置调试模式的中心点的纹理
        config.widthScaleHeight = 1f    // 设置每一行的宽高比
        config.listener = listener      // 添加点击的监听器


        arFragment!!.setOnTapArPlaneListener { hitResult: HitResult, plane: Plane, motionEvent: MotionEvent ->

            // Create the Anchor.
            val anchor = hitResult.createAnchor()
            val anchorNode = AnchorNode(anchor)

            // 为当前节点添加所有面
            val showNode = RotatingNode()
            showNode.worldPosition = anchorNode.worldPosition
            showNode.setParent(arFragment!!.arSceneView.scene)
            config.loadAllFace(showNode)
            // 开始旋转
            showNode.startAnimation()
        }
    }

    var listener = object : OnItemClickListener {
        override fun onItemClicked(row: Int, column: Int) {
            Log.e(TAG, "row=" + row + ", column=" + column)
        }
    }
}

旋转节点是中心节点使用了旋转动画

/**
 * 旋转节点的实现参照了 https://www.jianshu.com/p/f058bf833af6
 */
class RotatingNode : Node() {
    var rotationAnimation: ObjectAnimator? = null
    var degreesPerSecond = 5.0f
    val speedMultiplier = 1.0f
    val animationDuration = (1000 * 360 / (degreesPerSecond * speedMultiplier)).toLong()

    // 启动动画
    fun startAnimation() {
        if (rotationAnimation != null) {
            return
        }

        rotationAnimation = createAnimator()
        rotationAnimation!!.target = this
        rotationAnimation!!.duration = animationDuration
        rotationAnimation!!.start()
    }

    // 停止动画
    fun stopAnimation() {
        if (rotationAnimation == null) {
            return
        }

        rotationAnimation!!.cancel()
        rotationAnimation = null
    }

    // 返回一个 ObjectAnimator 用来使节点旋转起来
    private fun createAnimator(): ObjectAnimator {
        // 节点的位置和角度信息设置通过Quaternion来设置
        // 创建4个Quaternion 来设置四个关键位置
        val orientation1 = Quaternion.axisAngle(Vector3(0.0f, 1.0f, 0.0f), 0f)
        val orientation2 = Quaternion.axisAngle(Vector3(0.0f, 1.0f, 0.0f), 120f)
        val orientation3 = Quaternion.axisAngle(Vector3(0.0f, 1.0f, 0.0f), 240f)
        val orientation4 = Quaternion.axisAngle(Vector3(0.0f, 1.0f, 0.0f), 360f)
        val rotationAnimation = ObjectAnimator()
        rotationAnimation.setObjectValues(orientation1, orientation2, orientation3, orientation4)
        // 设置属性动画修改的属性为 localRotation
        rotationAnimation.setPropertyName("localRotation")
        // 使用Sceneform 框架提供的估值器 QuaternionEvaluator 作为属性动画估值器
        rotationAnimation.setEvaluator(QuaternionEvaluator())
        //  设置动画重复无限次播放。
        rotationAnimation.repeatCount = ObjectAnimator.INFINITE
        rotationAnimation.repeatMode = ObjectAnimator.RESTART
        rotationAnimation.interpolator = LinearInterpolator()
        rotationAnimation.setAutoCancel(true)
        return rotationAnimation
    }

}

总结

规则多面体的配置文件有点大,看起来比较费劲,解耦处理的不是很好;不过这个配置文件实现了大部分的功能,在使用的时候可以更少的去关心界面是怎么画出来的,修改适配器即可;如有更好的建议,还请指正

贴出来的示例代码主要阐明关键部分的实现,没有贴出所有文件,如有需要可咨询

搬砖不易,转载请标明出处 https://blog.csdn.net/qq_19154605/article/details/103779594

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章