通常情況下,顯示一個 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