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

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