I followed the solution here for rotating a TransformableNode on the X axis based on the user's DragGesture, using the Sceneform Android SDK. However, I would also like to rotate on the Y and Z axis as well, similar to how ARCore SceneViewer does it.
How can I achieve that?
What I have currently is on the left (rotates only on X axis), and what is desired is on the right (rotates on all axes, as in ARCore Scene Viewer).
class DragRotationController(transformableNode: BaseTransformableNode, gestureRecognizer: DragGestureRecognizer) :
BaseTransformationController<DragGesture>(transformableNode, gestureRecognizer) {
// Rate that the node rotates in degrees per degree of twisting.
var rotationRateDegrees = 0.5f
public override fun canStartTransformation(gesture: DragGesture): Boolean {
return transformableNode.isSelected
}
public override fun onContinueTransformation(gesture: DragGesture) {
var localRotation = transformableNode.localRotation
val rotationAmountX = gesture.delta.x * rotationRateDegrees
val rotationDeltaX = Quaternion(Vector3.up(), rotationAmountX)
localRotation = Quaternion.multiply(localRotation, rotationDeltaX)
// *** this only rotates on X axis. How do I rotate on all axes? ***
transformableNode.localRotation = localRotation
}
public override fun onEndTransformation(gesture: DragGesture) {}
}
I was able to find a working solution here: https://github.com/chnouman/SceneView
Here are the relevant snippets of code, with some of my adaptations to make it work for .glb files.
I have forked this repo and am working on keyboard input support if anyone's interested in that.
Rendering the object:
private fun renderLocalObject() {
skuProgressBar.setVisibility(View.VISIBLE)
ModelRenderable.builder()
.setSource(this,
RenderableSource.builder().setSource(
this,
Uri.parse(localModel),
RenderableSource.SourceType.GLB)/*RenderableSource.SourceType.GLTF2)*/
.setScale(0.25f)
.setRecenterMode(RenderableSource.RecenterMode.ROOT)
.build())
.setRegistryId(localModel)
.build()
.thenAccept { modelRenderable: ModelRenderable ->
skuProgressBar.setVisibility(View.GONE)
addNodeToScene(modelRenderable)
}
Adding the object to the SceneView:
private fun addNodeToScene(model: ModelRenderable) {
if (sceneView != null) {
val transformationSystem = makeTransformationSystem()
var dragTransformableNode = DragTransformableNode(1f, transformationSystem)
dragTransformableNode?.renderable = model
sceneView.getScene().addChild(dragTransformableNode)
dragTransformableNode?.select()
sceneView.getScene()
.addOnPeekTouchListener { hitTestResult: HitTestResult?, motionEvent: MotionEvent? ->
transformationSystem.onTouch(
hitTestResult,
motionEvent
)
}
}
}
Custom TransformableNode:
class DragTransformableNode(val radius: Float, transformationSystem: TransformationSystem) :
TransformableNode(transformationSystem) {
val dragRotationController = DragRotationController(
this,
transformationSystem.dragRecognizer
)
}
Custom TransformationController:
class DragRotationController(
private val transformableNode: DragTransformableNode,
gestureRecognizer: DragGestureRecognizer
) :
BaseTransformationController<DragGesture>(transformableNode, gestureRecognizer) {
companion object {
private const val initialLat = 26.15444376319647
private const val initialLong = 18.995950736105442
var lat: Double = initialLat
var long: Double = initialLong
}
// Rate that the node rotates in degrees per degree of twisting.
private var rotationRateDegrees = 0.5f
public override fun canStartTransformation(gesture: DragGesture): Boolean {
return transformableNode.isSelected
}
private fun getX(lat: Double, long: Double): Float {
return (transformableNode.radius * Math.cos(Math.toRadians(lat)) * Math.sin(Math.toRadians(long))).toFloat()
}
private fun getY(lat: Double, long: Double): Float {
return transformableNode.radius * Math.sin(Math.toRadians(lat)).toFloat()
}
private fun getZ(lat: Double, long: Double): Float {
return (transformableNode.radius * Math.cos(Math.toRadians(lat)) * Math.cos(Math.toRadians(long))).toFloat()
}
override fun onActivated(node: Node?) {
super.onActivated(node)
Handler().postDelayed({
transformCamera(lat, long)
}, 0)
}
public override fun onContinueTransformation(gesture: DragGesture) {
val rotationAmountY = gesture.delta.y * rotationRateDegrees
val rotationAmountX = gesture.delta.x * rotationRateDegrees
val deltaAngleY = rotationAmountY.toDouble()
val deltaAngleX = rotationAmountX.toDouble()
long -= deltaAngleX
lat += deltaAngleY
//lat = Math.max(Math.min(lat, 90.0), 0.0)
transformCamera(lat, long)
}
private fun transformCamera(lat: Double, long: Double) {
val camera = transformableNode.scene?.camera
var rot = Quaternion.eulerAngles(Vector3(0F, 0F, 0F))
val pos = Vector3(getX(lat, long), getY(lat, long), getZ(lat, long))
rot = Quaternion.multiply(rot, Quaternion(Vector3.up(), (long).toFloat()))
rot = Quaternion.multiply(rot, Quaternion(Vector3.right(), (-lat).toFloat()))
camera?.localRotation = rot
camera?.localPosition = pos
}
fun resetInitialState() {
transformCamera(initialLat, initialLong)
}
public override fun onEndTransformation(gesture: DragGesture) {}
}
Related
I created a ViewRenderable to go underneath my GLB model, but it doesn't seem to result in any shadow being cast on the viewRenderable.
What am I doing wrong?
class ShadowSelectionVisualizer : SelectionVisualizer {
private val footprintNode: Node = Node()
fun setFootprintRenderable(viewRenderable: ViewRenderable) {
val copyRenderable = viewRenderable.makeCopy()
copyRenderable.verticalAlignment = ViewRenderable.VerticalAlignment.CENTER
val rotation1: Quaternion = Quaternion.axisAngle(Vector3(1.0f, 0.0f, 0.0f), 90f)
footprintNode.renderable = copyRenderable
footprintNode.localRotation = rotation1
}
override fun applySelectionVisual(node: BaseTransformableNode) {
if (node.collisionShape is Box) {
val box: Box = node.collisionShape as Box
val vector3: Vector3 = box.size
(footprintNode.renderable as ViewRenderable).sizer = ViewSizer {
val bound = if (vector3.x > vector3.z) vector3.x else vector3.z
Vector3(bound, bound, 0.0f)
}
footprintNode.setParent(node)
}
}
override fun removeSelectionVisual(node: BaseTransformableNode?) {
footprintNode.setParent(null)
}
}
myModelRenderable.isShadowCaster = true
val selectionVisualizer = ShadowSelectionVisualizer()
val transformationSystem = TransformationSystem(resources.displayMetrics, selectionVisualizer)
ViewRenderable.builder()
.setView(
context, R.layout.shadow_receiver
)
.setRegistryId("someId.glb")
.build()
.thenAccept { viewRenderable: ViewRenderable ->
viewRenderable.isShadowCaster = false
viewRenderable.isShadowReceiver = true
selectionVisualizer.setFootprintRenderable(viewRenderable)
}
viewRenderable.isShadowCaster = false
Set this to true. More information here: https://developers.google.com/sceneform/reference/com/google/ar/sceneform/rendering/Renderable#isShadowCaster()
I have initialized my List in the function below in MainActivitywith DataPoint(data class in GraphView)
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var graph_view = GraphView(this)
graph_view.setData(generateRandomDataPoints())
}
fun generateRandomDataPoints(): List<GraphView.DataPoint> {
val random = Random()
return (0..20).map {
GraphView.DataPoint(it, random.nextInt(50) + 1)
}
}
}
GraphView
private val dataSet = arrayListOf<DataPoint>()
private var xMin = 0
private var xMax = 0
private var yMin = 0
private var yMax = 0
private val dataPointPaint = Paint().apply {
color = Color.BLACK
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
Log.e("D SIze", dataSet.size.toString())
for(i in 0 until dataSet.size){
val realX = dataSet[i].xVal.toRealX()
val realY = dataSet[i].yVal.toRealY()
Log.e("TAG", realX.toString())
Log.e("TAG", realY.toString())
Log.e("TAG", "NOT BEingf claeed")
canvas.drawCircle(realX, realY, 17f, dataPointPaint)
}
}
fun setData(newDataSet: List<DataPoint>) {
Log.e("TAG", newDataSet.size.toString())
xMin = newDataSet.minByOrNull { it.xVal }?.xVal ?: 0
xMax = newDataSet.maxByOrNull { it.xVal }?.xVal ?: 0
yMin = newDataSet.minByOrNull { it.yVal }?.yVal ?: 0
yMax = newDataSet.maxByOrNull { it.yVal }?.yVal ?: 0
dataSet.clear() // clear if any point are entered
dataSet.addAll(newDataSet) // add all points fro newDataSet
for(i in 0 .. dataSet.size-1){
Log.e("!!!!", dataSet[i].toString() )
}
invalidate()
}
private fun Int.toRealX() = toFloat() / xMax * width
private fun Int.toRealY() = toFloat() / yMax * height
data class DataPoint(
val xVal: Int,
val yVal: Int)
}
All of my logs work as expected and print's what is expected.
But when onDraw() is called from the invalidate() in the setData() function the log at the start returns 0. Which is why the for loop in onDraw() is never called and no points dots are being printed to the canvas?
I do not know why as all my other logs show that the data has been added for example the for loop in setData() prints
DataPoint(xVal=1, yVal=20)
DataPoint(xVal=3, yVal=37)
DataPoint(xVal=4, yVal=39)
....
Why is this and how can I solve it please?
Note: my question is very specific, apologies if the title isn't clear enough as to what the problem is.
I'm creating a pixel art editor application using Canvas, and the pixel art data is saved into a Room database.
Here's the canvas code:
package com.realtomjoney.pyxlmoose.customviews
import android.content.Context
import android.graphics.*
import android.util.Log
import android.view.MotionEvent
import android.view.View
import androidx.lifecycle.LifecycleOwner
import com.realtomjoney.pyxlmoose.activities.canvas.*
import com.realtomjoney.pyxlmoose.converters.JsonConverter
import com.realtomjoney.pyxlmoose.database.AppData
import com.realtomjoney.pyxlmoose.listeners.CanvasFragmentListener
import com.realtomjoney.pyxlmoose.models.Pixel
import kotlin.math.sqrt
class MyCanvasView(context: Context, val spanCount: Double) : View(context) {
lateinit var extraCanvas: Canvas
lateinit var extraBitmap: Bitmap
val rectangles = mutableMapOf<RectF, Paint?>()
private lateinit var caller: CanvasFragmentListener
private var thisWidth: Int = 0
private var scale: Double = 0.0
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
thisWidth = w
caller = context as CanvasFragmentListener
if (::extraBitmap.isInitialized) extraBitmap.recycle()
extraBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
extraCanvas = Canvas(extraBitmap)
scale = (w / spanCount)
for (i in 0 until spanCount.toInt()) {
for (i_2 in 0 until spanCount.toInt()) {
val rect = RectF((i * scale).toFloat(), (i_2 * scale).toFloat(), (i * scale).toFloat() + scale.toFloat(), (i_2 * scale).toFloat() + scale.toFloat())
rectangles[rect] = null
extraCanvas.drawRect(rect, Paint().apply { style = Paint.Style.FILL; color = Color.WHITE })
}
}
}
private fun drawRectAt(x: Float, y: Float) {
for (rect in rectangles.keys) {
if (rect.contains(x, y)) {
caller.onPixelTapped(this, rect)
invalidate()
}
}
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_MOVE -> drawRectAt(event.x, event.y)
MotionEvent.ACTION_DOWN -> drawRectAt(event.x, event.y)
}
return true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawBitmap(extraBitmap, 0f, 0f, null)
}
fun saveData(): List<Pixel> {
val data = mutableListOf<Pixel>()
for (pair in rectangles) {
data.add(Pixel(pair.value?.color))
}
return data
}
fun loadData(context: LifecycleOwner, index: Int) {
AppData.db.pixelArtCreationsDao().getAllPixelArtCreations().observe(context, {
currentPixelArtObj = it[index]
val localPixelData = JsonConverter.convertJsonStringToPixelList(currentPixelArtObj.pixelData)
var index = 0
for (i in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
for (i_2 in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
val rect = RectF((i * scale).toFloat(), (i_2 * scale).toFloat(), (i * scale).toFloat() + scale.toFloat(), (i_2 * scale).toFloat() + scale.toFloat())
rectangles[rect] = null
extraCanvas.drawRect(rect, Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE })
rectangles[rectangles.keys.toList()[index]] = Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE }
index++
}
}
})
}
}
Here's an example of how a 10 by 10 canvas may look like:
The pixel data is saved into a Room database as a Json String, and whenever we want to access this data we convert the Json String back to a List<Pixel>, et cetera:
Dao:
#Dao
interface PixelArtCreationsDao {
#Insert
suspend fun insertPixelArt(pixelArt: PixelArt)
#Query("SELECT * FROM PixelArt ")
fun getAllPixelArtCreations(): LiveData<List<PixelArt>>
#Query("DELETE FROM PixelArt WHERE objId=:pixelArtId")
fun deletePixelArtCreation(pixelArtId: Int)
#Query("UPDATE PixelArt SET item_bitmap=:bitmap WHERE objId=:id_t")
fun updatePixelArtCreationBitmap(bitmap: String, id_t: Int): Int
#Query("UPDATE PixelArt SET item_pixel_data=:pixelData WHERE objId=:id_t")
fun updatePixelArtCreationPixelData(pixelData: String, id_t: Int): Int
#Query("UPDATE PixelArt SET item_favourited=:favorited WHERE objId=:id_t")
fun updatePixelArtCreationFavorited(favorited: Boolean, id_t: Int): Int
}
PixelArt database:
#Database(entities = [PixelArt::class], version = 1)
abstract class PixelArtDatabase: RoomDatabase() {
abstract fun pixelArtCreationsDao(): PixelArtCreationsDao
companion object {
private var instance: PixelArtDatabase? = null
fun getDatabase(context: Context): PixelArtDatabase {
if (instance == null) {
synchronized(PixelArtDatabase::class) {
if (instance == null) instance = Room.databaseBuilder(context.applicationContext, PixelArtDatabase::class.java, AppData.dbFileName).allowMainThreadQueries().build()
}
}
return instance!!
}
}
}
AppData:
class AppData {
companion object {
var dbFileName = "pixel_art_db"
lateinit var db: PixelArtDatabase
}
}
Model:
#Entity
data class PixelArt(
#ColumnInfo(name = "item_bitmap") var bitmap: String,
#ColumnInfo(name = "item_title") var title: String,
#ColumnInfo(name = "item_pixel_data") var pixelData: String,
#ColumnInfo(name = "item_favourited") var favourited: Boolean,
#ColumnInfo(name = "item_date_created") var dateCreated: String = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").format(LocalDateTime.now())) {
#PrimaryKey(autoGenerate = true) var objId = 0
}
Now, say we have two projects like so with two different spanCount values:
Once we click on the first item, the following occurs:
For some reason it's setting the grid size to be equal to that of the second item, and I'm really trying to understand why this is the case. I've tried for a couple of hours to fix this weird glitch and have had no luck doing so.
But, for some reason when we go to our second item it renders properly:
If we create a new 80 x 80 canvas and then go back to the second creation it will render like so:
I'm assuming that it's setting the spanCount to that of the latest item in the database, but I'm unsure why this is happening.
I suspect it has something to do with the code that takes the List<Pixel> and draws it onscreen:
fun loadData(context: LifecycleOwner, index: Int) {
AppData.db.pixelArtCreationsDao().getAllPixelArtCreations().observe(context, {
currentPixelArtObj = it[index]
val localPixelData = JsonConverter.convertJsonStringToPixelList(currentPixelArtObj.pixelData)
var index = 0
for (i in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
for (i_2 in 0 until sqrt(localPixelData.size.toDouble()).toInt()) {
val rect = RectF((i * scale).toFloat(), (i_2 * scale).toFloat(), (i * scale).toFloat() + scale.toFloat(), (i_2 * scale).toFloat() + scale.toFloat())
rectangles[rect] = null
extraCanvas.drawRect(rect, Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE })
rectangles[rectangles.keys.toList()[index]] = Paint().apply { style = Paint.Style.FILL; isAntiAlias = false; color = localPixelData[index].pixelColor ?: Color.WHITE }
index++
}
}
})
}
Although I'm not entirely sure where the source of the bug is coming from because it seems I'm doing everything right. It's honestly been a brainfuck trying to fix this lol
Any help would be appreciated to fix this annoying glitch so I can finish my pixel art editor app.
This bug was fixed by calling invalidate() on the Fragment's Canvas property after the user taps the back button. It took me a couple of days to get to fix this, so I'm posting an answer here in case someone has a similar bug.
fun CanvasActivity.extendedOnBackPressed() {
canvasFragmentInstance.myCanvasViewInstance.invalidate()
startActivity(Intent(context, MainActivity::class.java))
}
I am currently creating an Android view in which, when the use tap it, I will display a sort of ripple around the coordinate of the tap.
But I'm not sure on how to do it. My first idea was to invalidate the cache and just make the circle bigger each time but it doesn't seem appropriate nor efficient to do this like that.
If anyone faced the same problem before and would love the share some tips on how to do it it would be much appreciated.
I finally found out a solution. Not a perfect one but it works for now.
Here is the code I did. Basically when I need it I change a boolean to true so my onDrawfunction knows that it have to execute the drawFingerPrintfunction.
The drawFingerPrint function, in the other end, just draw a circle that's bigger and bigger between each iteration until it reaches the diameter needed
private fun drawFingerPrint(canvas: Canvas) {
canvas.drawCircle(pointerX, pointerY, radius, paint)
if(radius<= 100F){
radius+=10F
invalidate()
}
else{
radius = 0F
drawAroundFinger = false
invalidate()
}
}
I hope someone else will find this useful sometimes!
Matthieu
As already mentioned #Matthieu, you need to draw circle on the canvas and invalidate the view. Here I provide a more complete example.
So we have an open class RippleView:
open class RippleView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
private var rippleX: Float? = null
private var rippleY: Float? = null
private var rippleRadius: Float? = null
var maxRippleRadius: Float = 100f // Retrieve from resources
var rippleColor: Int = 0x88888888.toInt()
private val ripplePaint = Paint().apply {
color = rippleColor
}
private val animationExpand = object : Runnable {
override fun run() {
rippleRadius?.let { radius ->
if (radius < maxRippleRadius) {
rippleRadius = radius + maxRippleRadius * 0.1f
invalidate()
postDelayed(this, 10L)
}
}
}
}
private val animationFade = object : Runnable {
override fun run() {
ripplePaint.color.let { color ->
if (color.alpha > 10) {
ripplePaint.color = color.adjustAlpha(0.9f)
invalidate()
postDelayed(this, 10L)
} else {
rippleX = null
rippleY = null
rippleRadius = null
invalidate()
}
}
}
}
fun startRipple(x: Float, y: Float) {
rippleX = x
rippleY = y
rippleRadius = maxRippleRadius * 0.15f
ripplePaint.color = rippleColor
animationExpand.run()
}
fun stopRipple() {
if (rippleRadius != null) {
animationFade.run()
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val x = rippleX ?: return
val y = rippleY ?: return
val r = rippleRadius ?: return
canvas.drawCircle(x, y, r, ripplePaint)
}
}
fun Int.adjustAlpha(factor: Float): Int =
(this.ushr(24) * factor).roundToInt() shl 24 or (0x00FFFFFF and this)
inline val Int.alpha: Int
get() = (this shr 24) and 0xFF
Now you can extend any custom view with RippleView and use startRipple method when you get ACTION_DOWN, and stopRipplemethod when you get ACTION_UP:
class ExampleView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : RippleView(context, attrs), View.OnTouchListener {
init {
setOnTouchListener(this)
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (isInsideArea(event.x, event.y)) {
startRipple(event.x, event.y)
}
}
MotionEvent.ACTION_UP -> {
stopRipple()
}
}
return false
}
private fun isInsideArea(x: Float, y: Float): Boolean {
TODO("Not yet implemented")
}
}
I used MapGestureListener to compare each time the coords of the clicked area and the coords of the marker and if they're at the same coords then I'm good to go but it just won't work because of the relative altitude change that doesn't assure the accuracy of getting the clicked position.
mpView.addMapGestureListener(object : MapGestureAdapter() {
override fun onMapClicked(e: MotionEvent?, isTwoFingers: Boolean): Boolean {
val clickedArea=mpView.geoCoordinatesFromPoint(Math.round(e!!.getX()), Math.round(e.getY()))
for (marker : MapMarker in markerList )
{
val dist=clickedArea!!.distanceTo(marker.position)
if (dist< 2)
{
val positionMarker = markerList.indexOf(marker)
val positionLastMarker = markerList.indexOf(mSelectedMarker!!)
val markerNumber = positionMarker +1
val lastMarkerNumber = positionLastMarker + 1
travelStep = travelStepList.get(markerNumber -1)
configTeaser(travelStep)
}
}
return false
}
})
I've managed to do it , i just had to call the "requestObjectsAtPoint" inside the MapGesture listener and do some workaround ,here's the code :
mpView.addMapGestureListener(object : MapGestureAdapter() {
override fun onMapClicked(e: MotionEvent?, isTwoFingers: Boolean): Boolean {
mpView.requestObjectsAtPoint(e!!.getX(),e.getY(), RequestObjectCallback { objects, x, y ->
for (marker : ViewObject in objects )
{
if (marker.objectType==1)
{
if ((marker as MapObject).mapObjectType==1)
{
val positionMarker = markerList.indexOf(marker)
val positionLastMarker = markerList.indexOf(mSelectedMarker!!)
val markerNumber = positionMarker +1
val lastMarkerNumber = positionLastMarker + 1
mSelectedMarker = marker as MapMarker
travelStep = travelStepList.get(markerNumber -1)
configTeaser(travelStep)
}
}
}
})
return true
}
})