Sceneform: GLB model not casting shadows - android

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()

Related

val cannot be reassigned while trying to update Polyline

In the code below I get an error Val cannot be reassigned, I dont know how can I recreate the object ? i am new to kotlin and I am just trying to update a code that i found somewhere... trying to understand how to modify it.
I understand it is final and it is being called from another class like this
another class
val polyLine = RoadManager.buildRoadOverlay(road)
PanRoad = newRoad(
polyLine = polyLine,
colorRoad = colorRoad,
roadWidth = roadWidth,
showPoiMarker = showPoiMarker,
listInterestPoints = listInterestPoints,
)
I tried changin it to val to var like this var polyLine = RoadManager.buildRoadOverlay(road) still nothing.
Below is the class that is giving errors
private fun newRoad(
polyLine: Polyline,
colorRoad: Int?,
showPoiMarker: Boolean,
listInterestPoints: List<GeoPoint>,
roadWidth: Float,
bitmapIcon: Bitmap? = null,
): PanRoad {
print(message = "pannam createRoad called")
polyLine.setOnClickListener { _, _, eventPos ->
methodChannel.invokeMethod("receiveSinglePress", eventPos?.toHashMap())
true
}
/// set polyline color
val polygonPaint = Paint().apply {
color = Color.BLUE
style = Paint.Style.FILL
}
val outlinerPaint = Paint().apply {
color = Color.DKGRAY
style = Paint.Style.STROKE
strokeWidth = 2f
}
polyLine.outlinePaint = Paint().apply {
color = colorRoad ?: Color.GREEN
style = Paint.Style.STROKE
strokeWidth = 2f
strokeCap = Cap.ROUND
strokeJoin = Paint.Join.ROUND
}
val iconsRoads = customRoadMarkerIcon
when {
(iconsRoads.isEmpty() && bitmapIcon != null) -> {
iconsRoads[Constants.STARTPOSITIONROAD] = bitmapIcon
iconsRoads[Constants.MIDDLEPOSITIONROAD] = bitmapIcon
iconsRoads[Constants.ENDPOSITIONROAD] = bitmapIcon
}
iconsRoads.isNotEmpty() && bitmapIcon != null -> {
iconsRoads[Constants.MIDDLEPOSITIONROAD] = bitmapIcon
if (!iconsRoads.containsKey(Constants.STARTPOSITIONROAD)) {
iconsRoads[Constants.STARTPOSITIONROAD] = bitmapIcon
}
if (!iconsRoads.containsKey(Constants.ENDPOSITIONROAD)) {
iconsRoads[Constants.ENDPOSITIONROAD] = bitmapIcon
}
}
}
val PanRoad = PanRoad(
context,
map!!,
interestPoint = if (showPoiMarker) listInterestPoints else emptyList(),
showInterestPoints = showPoiMarker
)
PanRoad.let { roadF ->
if (showPoiMarker) {
roadF.markersIcons = iconsRoads
}
polyLine.outlinePaint.strokeWidth = roadWidth
roadF.road = polyLine
folderRoad.items.add(roadF)
}
return PanRoad
}
The error occurs at polyLine.outlinePaint
I assume the Polyline class you're working with is from org.osmdroid.views.overlay, which only provides a getOutlinePaint as can be seen in its JavaDoc. However, there is no setOutlinePaint. From Kotlins point of view outlinePaint is therefore a val which cannot be reassigned.
It think instead of reassigning the outlinePaint field you instead want to retrieve a reference to the Paint object stored in the field and modify it directly. Something like the following might work, however I'm unable to verify this at the moment.
val outlinePaint = polyLine.outlinePaint
outlinePaint.color = Color.GREEM
outlinePaint.style = Paint.Style.STROKE
or if using apply is preferred
polyLine.outlinePaint.apply {
color = Color.GREEM
style = Paint.Style.STROKE
}

I have a line chart that I want to use to show temperature, but I want to show icons and time in x axis

what I want to achieve
what I have
I want to show icons on the xAxis above time but line chart takes icon and places them on the value where temperature is shown. I have searched a lot but could not find the answer. Any help would be appreciated. I have tried a lot of things but all in vein.
private fun setTempChart(hour: ArrayList<Hour>, id: String) {
val entries: MutableList<Entry> = ArrayList()
for (i in hour.indices) {
val code = hour[i].condition.code
val icon =
if (hour[i].is_day == 1) requireActivity().setIconDay(code) else requireActivity().setIconNight(
code
)
entries.add(Entry(i.toFloat(), sharedPreference.temp?.let {
hour[i].temp_c.setCurrentTemperature(
it
).toFloat()
}!!))
}
val dataSet = LineDataSet(entries, "")
dataSet.apply {
lineWidth = 0f
setDrawCircles(false)
setDrawCircleHole(false)
isHighlightEnabled = false
valueTextColor = Color.WHITE
setColors(Color.WHITE)
valueTextSize = 12f
mode = LineDataSet.Mode.CUBIC_BEZIER
setDrawFilled(true)
fillColor = Color.WHITE
valueTypeface = typeface
isDrawIconsEnabled
setDrawIcons(true)
valueFormatter = object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return String.format(Locale.getDefault(), "%.0f", value)
}
}
}
val lineData = LineData(dataSet)
chart.apply {
description.isEnabled = false
axisLeft.setDrawLabels(false)
axisRight.setDrawLabels(false)
legend.isEnabled = false
axisLeft.setDrawGridLines(false)
axisRight.setDrawGridLines(false)
axisLeft.setDrawAxisLine(false)
axisRight.setDrawAxisLine(false)
setScaleEnabled(false)
data = lineData
setVisibleXRange(8f, 8f)
animateY(1000)
xAxis.apply {
setDrawAxisLine(false)
textColor = Color.WHITE
setDrawGridLines(false)
setDrawLabels(true)
position = XAxis.XAxisPosition.BOTTOM
textSize = 12f
valueFormatter = MyAxisFormatter(hour, id)
isGranularityEnabled = true
granularity = 1f
labelCount = entries.size
}
}
}
I am using MPAndroidChart library
There isn't a nice built-in way to draw icons like that, but you can do it by making a custom extension of the LineChartRenderer and overriding drawExtras. Then you can get your icons from R.drawable.X and draw them on the canvas wherever you want. There is some work to figure out where to put them to line up with the data points, but you can copy the logic from drawCircles to find that.
Example Custom Renderer
inner class MyRenderer(private val context: Context,
private val iconY: Float,
private val iconSizeDp: Float,
chart: LineDataProvider,
animator: ChartAnimator,
viewPortHandler: ViewPortHandler)
: LineChartRenderer(chart, animator, viewPortHandler) {
private var buffer: FloatArray = listOf(0f,0f).toFloatArray()
override fun drawExtras(c: Canvas) {
super.drawExtras(c)
val iconSizePx = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
iconSizeDp,
resources.displayMetrics
)
// get the icons you want to draw
val cloudy = ContextCompat.getDrawable(context, R.drawable.cloudy)
val sunny = ContextCompat.getDrawable(context, R.drawable.sunny)
if( cloudy == null || sunny == null ) {
throw RuntimeException("Missing drawables")
}
// Determine icon width in pixels
val w = iconSizePx
val h = iconSizePx
val dataSets = mChart.lineData.dataSets
val phaseY = mAnimator.phaseY
for(dataSet in dataSets) {
mXBounds.set(mChart, dataSet)
val boundsRange = mXBounds.range + mXBounds.min
val transformer = mChart.getTransformer(dataSet.axisDependency)
for(j in mXBounds.min .. boundsRange) {
val e = dataSet.getEntryForIndex(j) ?: break
buffer[0] = e.x
buffer[1] = iconY * phaseY
transformer.pointValuesToPixel(buffer)
if( !mViewPortHandler.isInBoundsRight(buffer[0])) {
break
}
if( !mViewPortHandler.isInBoundsLeft(buffer[0]) ||
!mViewPortHandler.isInBoundsY(buffer[1])) {
continue
}
// Draw the icon centered under the data point, but at a fixed
// vertical position. Here the icon "sits on top" of the
// specified iconY value
val left = (buffer[0]-w/2).roundToInt()
val right = (buffer[0]+w/2).roundToInt()
val top = (buffer[1]-h).roundToInt()
val bottom = (buffer[1]).roundToInt()
// Alternately, use this to center the icon at the
// "iconY" value
//val top = (buffer[1]-h/2).roundToInt()
//val bottom = (buffer[1]+h/2).roundToInt()
// Use whatever logic you want to select which icon
// to use at each position
val icon = if( e.y > 68f ) {
sunny
}
else {
cloudy
}
icon.setBounds(left, top, right, bottom)
icon.draw(c)
}
}
}
}
Using the Custom Renderer
val iconY = 41f
val iconSizeDp = 40f
chart.renderer = MyRenderer(this, iconY, iconSizeDp,
chart, chart.animator, chart.viewPortHandler)
(other formatting)
val chart = findViewById<LineChart>(R.id.chart)
chart.axisRight.isEnabled = false
val yAx = chart.axisLeft
yAx.setDrawLabels(false)
yAx.setDrawGridLines(false)
yAx.setDrawAxisLine(false)
yAx.axisMinimum = 40f
yAx.axisMaximum = 80f
val xAx = chart.xAxis
xAx.setDrawLabels(false)
xAx.position = XAxis.XAxisPosition.BOTTOM
xAx.setDrawGridLines(false)
xAx.setDrawAxisLine(false)
xAx.axisMinimum = 0.5f
xAx.axisMaximum = 5.5f
xAx.granularity = 0.5f
val x = listOf(0,1,2,3,4,5,6)
val y = listOf(60,65,66,70,65,50,55)
val e = x.zip(y).map { Entry(it.first.toFloat(), it.second.toFloat())}
val line = LineDataSet(e, "temp")
line.setDrawValues(true)
line.setDrawCircles(false)
line.circleRadius = 20f // makes the text offset up
line.valueTextSize = 20f
line.color = Color.BLACK
line.lineWidth = 2f
line.setDrawFilled(true)
line.fillColor = Color.BLACK
line.fillAlpha = 50
line.mode = LineDataSet.Mode.CUBIC_BEZIER
line.valueFormatter = object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return "%.0f F".format(value)
}
}
chart.data = LineData(line)
chart.description.isEnabled = false
chart.legend.isEnabled = false
Gives the desired effect

How to rotate an 3D model using DragGesture in Android SceneForm

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) {}
}

Password generator displays kotlin.Unit

I'm trying to make random password generator based on user input and everything is fine until i use .toCharArray().shuffle() function, but without shuffling it's too predictable beacuse it puts letters in pre-determined positions. Is there any way this code would work? Any workaround? I already tried stringbuilder but it bypasses user input so I don't know what to do now.
val chars= "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~##$%^&*()!"
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser)
{
when(seekBar)
{
sbNumberOfLetters ->
{
tvLetterCount.text = progress.toString()
smallLetters = progress
}
sbNumberOfCapitalLetters ->
{
tvCapitalsCount.text = progress.toString()
capitalLetterNumber = progress
}
sbNumberOfNumerals ->
{
tvNumeralsCount.text = progress.toString()
numeralsNumber = progress
}
sbNumberOfSpecialChars ->
{
tvSpecialCharsCount.text = progress.toString()
specialCharNumber = progress
}
}
}
}
private fun generatePassword() {
for (y in 1..numeralsNumber)
{
var randomLetter = Random.nextInt(0, 9)
listOfLetters.add(chars[randomLetter].toString())
}
for (w in 1..smallLetters)
{
var randomLetter = Random.nextInt(10, 36)
listOfLetters.add(chars[randomLetter].toString())
}
for (x in 1..capitalLetterNumber)
{
var randomLetter = Random.nextInt(36, 62)
listOfLetters.add(chars[randomLetter].toString())
}
for (z in 1..specialCharNumber)
{
var randomLetter = Random.nextInt(63, 73)
listOfLetters.add(chars[randomLetter].toString())
}
password = (listOfLetters.joinToString(separator = "",)).toCharArray().shuffle().toString()
tvGeneratedPassword.text = password
listOfLetters.clear()
}
shuffle returns Unit, so calling toString() on Unit will return kotlin.Unit which is defined in Unit. Try this:
listOfLetters.shuffle()
val password = listOfLetters.joinToString(separator = "")
tvGeneratedPassword.text = password
Might as well use the tools that Kotlin provides to write expressive and maintainable code ( are the indexes for Random.nextInt in the code you provided correct? ) . Here is one alternative (here lists and not arrays are used and shuffled() returns a new list ) :
abstract sealed class RandomChars {
open val chars: CharArray = charArrayOf()
fun get(count: Int) = (1..count).map { chars[Random.nextInt(0, chars.size)] }
}
object RandomDigits : RandomChars() {
override val chars = "0123456789".toCharArray()
}
object RandomLowerCase : RandomChars() {
override val chars = "abcdefghijklmnopqrstuvwxyz".toCharArray()
}
object RandomUpperCase : RandomChars() {
override val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray()
}
object RandomSpecial : RandomChars() {
override val chars = "~##$%^&*()!".toCharArray()
}
fun main() {
val password =
(RandomDigits.get(1) + RandomLowerCase.get(2) + RandomUpperCase.get(2) + RandomSpecial.get(1))
.shuffled()
.joinToString(separator = "")
println(password) // e.g. %Oa7Mt
}
Here is another, more functional approach:
val charGenerator =
{ alphabet: CharArray -> { count: Int -> (1..count).map { alphabet[Random.nextInt(0, alphabet.size)] } } }
val randomDigits = charGenerator("0123456789".toCharArray())
val randomLowecase = charGenerator("abcdefghijklmnopqrstuvwxyz".toCharArray())
val randomUppercase = charGenerator("ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray())
val randomSpecial = charGenerator("~##$%^&*()!".toCharArray())
fun main() {
val password =
(randomDigits(1) + randomLowecase(2) + randomUppercase(2) + randomSpecial(1))
.shuffled()
.joinToString(separator = "")
println(password) // e.g. %Oa7Mt
}

MPChart won't display hourly weather data

I'm creating a Weather app and i'm trying to display an MPChart which will use the hourly temps as Y points & the Hour (in "HH" format) as the XAxisLabels/ X points.
I'm creating the LineData for the chart in my Fragment's viewModel and expose it to the Fragment via liveData. I also create the XAxisLabels for the chart on my Fragment. The LineData seem to contain the right entries (viewModel: [Entry, x: 0.0 y: 39.07, Entry, x: 1.0 y: 38.68, Entry, x: 2.0 y: 38.16, Entry, x: 3.0 y: 37.29, Entry, x: 4.0 y: 36.2, Entry, x: 5.0 y: 35.22,........]), but the chart isn't even displaying any of the data. What am I missing here?
Here's what is rendered:
Here's the code:
ViewModel.kt
class WeatherViewModel(
private val forecastRepository: ForecastRepository,
private val weatherUnitConverter: WeatherUnitConverter,
context: Context
) : ViewModel() {
val hourlyWeatherEntries = forecastRepository.getHourlyWeather()
val hourlyChartData = hourlyWeatherEntries.switchMap { hourlyData ->
liveData {
Log.d("ViewModel hourly data", "$hourlyData")
val task = viewModelScope.async(Dispatchers.Default) {
createChartData(hourlyData)
}
emit(task.await())
}
}
/**
* Creates the line chart's data and returns them.
* #return The line chart's data (x,y) value pairs
*/
private fun createChartData(hourlyWeather: List<HourWeatherEntry>?): LineData {
if (hourlyWeather == null) return LineData()
Log.d("ViewModel class", hourlyWeather.toString())
val unitSystemValue = preferences.getString(UNIT_SYSTEM_KEY, "si")!!
val values = arrayListOf<Entry>()
for (i in hourlyWeather.indices) { // init data points
// format the temperature appropriately based on the unit system selected
val hourTempFormatted = when (unitSystemValue) {
UnitSystem.SI.name.toLowerCase(Locale.ROOT) -> hourlyWeather[i].temperature
UnitSystem.US.name.toLowerCase(Locale.ROOT) -> weatherUnitConverter.convertToFahrenheit(
hourlyWeather[i].temperature
)
else -> hourlyWeather[i].temperature
}
// Create the data point
values.add(
Entry(
i.toFloat(),
hourTempFormatted.toFloat(),
appContext.resources.getDrawable(
determineSummaryIcon(hourlyWeather[i].icon),
null
)
)
)
}
Log.d("MainFragment viewModel", "$values")
// create a data set and customize it
val lineDataSet = LineDataSet(values, "")
val color = appContext.resources.getColor(R.color.black, null)
val offset = MPPointF.getInstance()
offset.y = -35f
lineDataSet.apply {
valueFormatter = YValueFormatter()
setDrawValues(true)
fillDrawable = appContext.resources.getDrawable(R.drawable.gradient_night_chart, null)
setDrawFilled(true)
setDrawIcons(true)
setCircleColor(color)
mode = LineDataSet.Mode.HORIZONTAL_BEZIER
this.color = color // line color
iconsOffset = offset
lineWidth = 3f
valueTextSize = 9f
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
valueTypeface = appContext.resources.getFont(R.font.work_sans_medium)
}
}
// create a LineData object using our LineDataSet
val data = LineData(lineDataSet)
data.apply {
setValueTextColor(R.color.colorPrimary)
setValueTextSize(15f)
}
return data
}
}
MainFragment.kt
class MainFragment : Fragment() {
// Lazy inject the view model
private val viewModel: WeatherViewModel by viewModel()
private lateinit var hourlyChart: LineChart
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpChart()
lifecycleScope.launch {
// Shows a lottie animation while the data is being loaded
//scrollView.visibility = View.GONE
//lottieAnimView.visibility = View.VISIBLE
bindUIAsync().await()
// Stops the animation and reveals the layout with the data loaded
//scrollView.visibility = View.VISIBLE
//lottieAnimView.visibility = View.GONE
}
}
#SuppressLint("SimpleDateFormat")
private fun bindUIAsync() = lifecycleScope.async(Dispatchers.Main) {
// fetch hourly chart line data
val hourlyChartData = viewModel.hourlyChartData
hourlyChartData.observe(viewLifecycleOwner, Observer { lineData ->
if(lineData == null) {
Log.d("HOURLY CHART DATA", "Line data = null")
return#Observer
}
hourlyChart.data = lineData
hourlyChart.invalidate()
})
// fetch hourly weather
val hourlyWeather = viewModel.hourlyWeatherEntries//viewModel.hourlyWeatherEntries
// Observe the hourly weather live data
hourlyWeather.observe(viewLifecycleOwner, Observer { hourly ->
if (hourly == null) return#Observer
val xAxisLabels = arrayListOf<String>()
val sdf = SimpleDateFormat("HH")
for (i in hourly.indices) {
val formattedLabel = sdf.format(Date(hourly[i].time * 1000))
xAxisLabels.add(formattedLabel)
}
setChartAxisLabels(xAxisLabels)
Log.d(TAG, "label count = ${hourlyChart.xAxis.labelCount.toString()}")
})
return#async true
}
private fun setChartAxisLabels(labels: ArrayList<String>) {
// Populate the X axis with the hour labels
hourlyChart.xAxis.valueFormatter = IndexAxisValueFormatter(labels)
}
/**
* Sets up the chart with the appropriate
* customizations.
*/
private fun setUpChart() {
hourlyChart.apply {
description.isEnabled = false
setNoDataText("Data is loading...")
// enable touch gestures
setTouchEnabled(true)
dragDecelerationFrictionCoef = 0.9f
// enable dragging
isDragEnabled = true
isHighlightPerDragEnabled = true
setDrawGridBackground(false)
axisRight.setDrawLabels(false)
axisLeft.setDrawLabels(false)
axisLeft.setDrawGridLines(false)
// disable zoom functionality
setScaleEnabled(false)
setPinchZoom(false)
isDoubleTapToZoomEnabled = false
// disable the chart's legend
legend.isEnabled = false
// append extra offsets to the chart's auto-calculated ones
setExtraOffsets(0f, 0f, 0f, 10f)
data = LineData()
data.isHighlightEnabled = false
setVisibleXRangeMaximum(6f)
setBackgroundColor(resources.getColor(R.color.bright_White, null))
}
// X Axis setup
hourlyChart.xAxis.apply {
position = XAxis.XAxisPosition.BOTTOM
textSize = 14f
setDrawLabels(true)
setDrawAxisLine(false)
setDrawGridLines(false)
isEnabled = true
granularity = 1f // one hour
spaceMax = 0.2f // add padding start
spaceMin = 0.2f // add padding end
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
typeface = resources.getFont(R.font.work_sans)
}
textColor = resources.getColor(R.color.black, null)
}
// Left Y axis setup
hourlyChart.axisLeft.apply {
setDrawLabels(true)
setDrawGridLines(false)
setPosition(YAxis.YAxisLabelPosition.OUTSIDE_CHART)
// temperature values range (higher than probable temps in order to scale down the chart)
axisMinimum = 0f
axisMaximum = when (getUnitSystemValue()) {
UnitSystem.SI.name.toLowerCase(Locale.ROOT) -> 50f
UnitSystem.US.name.toLowerCase(Locale.ROOT) -> 150f
else -> 50f
}
}
// Right Y axis setup
hourlyChart.axisRight.apply {
setDrawGridLines(false)
isEnabled = false
}
}
}

Categories

Resources