I have an App that lets users drag and draw boxes on a custom view. I want to persist the state of these boxes(list of boxes) across orientation change using onSavedInstanceState(): Parcelable and onRestoreInstanceState(state: Parcelable). However, I don't know how to store a MutableList because the only available function is putParcelableArrayList. Please how do I parcelize a Mutable List to persist the boxes across rotation? I know the docs said its possible but I don't know how to. Here is the code.
#Parcelize
class Box(private val start: PointF) : Parcelable {
// When a user touches BoxDrawingView, a new box will be created and added to the list of existing boxes.
var end: PointF = start
val left: Float
get() = start.x.coerceAtMost(end.x)
val right: Float
get() = start.x.coerceAtLeast(end.x)
val top: Float
get() = start.y.coerceAtMost(end.y)
val bottom: Float
get() = start.y.coerceAtLeast(end.y)
}
My custom View
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PointF
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
/** This Class is where we setup our custom View and write the Implementation for listening to touch events from the USER and draw boxes on the Screen.**/
private const val TAG = "BoxDrawingView"
private const val BOX_STATE = "box"
private const val VIEW_STATE = "view"
class BoxDrawingView(context: Context, attrs: AttributeSet? = null) :
View(context, attrs) {
private var currentBox: Box? = null
private var boxen = mutableListOf<Box>() // list of boxes to be drawn out on the screen
private val boxPaint = Paint().apply {
color = 0x22ff0000
}
private val backGroundPaint = Paint().apply {
color = 0xfff8efe0.toInt()
}
init {
isSaveEnabled = true
}
override fun onSaveInstanceState(): Parcelable {
val bundle = Bundle()
bundle.putParcelableArrayList(BOX_STATE, boxen) // type mismatch error because of mutableList passed to ArrayList
bundle.putParcelable(VIEW_STATE, super.onSaveInstanceState())
return bundle
}
override fun onRestoreInstanceState(state: Parcelable?) {
var viewState = state
if (viewState is Bundle) {
boxen = viewState.getParcelableArrayList<Box>(BOX_STATE)?.toMutableList() ?: mutableListOf()
viewState = viewState.getParcelable(VIEW_STATE)
}
super.onRestoreInstanceState(state)
}
override fun onDraw(canvas: Canvas) {
// Fill in the background
canvas.drawPaint(backGroundPaint)
boxen.forEach { box ->
canvas.drawRect(box.left, box.top, box.right, box.bottom, boxPaint)
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
val current = PointF(event.x, event.y)
var action = ""
when(event.action) {
MotionEvent.ACTION_DOWN -> {
action = "ACTION_DOWN"
// Reset drawing state
currentBox = Box(current).also {
boxen.add(it)
}
}
MotionEvent.ACTION_MOVE -> {
action = "ACTION_MOVE"
// update the currentBox.end as the user moves his/her finger around the screen
updateCurrentBox(current)
}
MotionEvent.ACTION_UP -> {
action = "ACTION_UP"
// tells the last report of the currentBox as the user's finger leaves the screen
updateCurrentBox(current)
currentBox = null
}
MotionEvent.ACTION_CANCEL -> {
action = "ACTION_CANCEL"
currentBox = null
}
}
// this is a log message for each of the 4 Event actions
Log.i(TAG, "$action at x=${current.x}, y=${current.y}")
return true
}
Changing the boxen type to arrayList() worked. Turns out arrayList works as a mutable list under the hood. Also David wasser's answer in the comments also worked.
Related
This is an application with a list of dogs and information about the dog.
I'm trying to run a DetailActivity that contains information about a dog. It is launched after clicking the Show Details button, which has a setOnClickListener and runs the Intent, passing the name, age, hobby and other parameters of the dog to the running DetailActivity. But when I try to take those parameters in the DetailActivity code, they all equal null.
It's actually very simple and I've done the same thing in google course codelab before (now I decided to practice a bit) and I repeat everything as it's written there, but I don't understand what I'm doing wrong.
I'll insert the DetailActivity and DogCardAdapter code below. You can also see all the code at this link on GitHub: https://github.com/theMagusDev/DogglersApp
DetailActivity.kt:
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.example.dogglers.databinding.ActivityDetailBinding
class DetailActivity() : AppCompatActivity() {
private lateinit var binding: ActivityDetailBinding
private val TAG = "DetailActivity"
companion object {
const val DOG_IMAGE = "dogImage"
const val DOG_NAME = "dogName"
const val DOG_AGE = "dogAge"
const val DOG_HOBBIES = "dogHobbies"
const val DOG_SEX = "dogSex"
}
val dogImageResourceId = intent?.extras?.getString(DOG_IMAGE)
val dogName = intent?.extras?.getString(DOG_NAME)
val dogAge = intent?.extras?.getString(DOG_AGE)
val dogHobbies = intent?.extras?.getString(DOG_HOBBIES)
val dogSex = intent?.extras?.getString(DOG_SEX)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Setup view binding
binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
val heOrShe: String = when(dogSex){
"male" -> "He"
else -> "She"
}
binding.dogName.text = dogName
Log.d(TAG, "${dogAge.toString()}, ${dogName.toString()}, $dogHobbies, $heOrShe, $dogImageResourceId")
binding.dogDescription.text = getString(R.string.dog_description, dogName, dogAge, dogSex, dogHobbies)
//binding.dogImage.setImageResource(dogImageResourceId!!.toInt())
binding.dogImage.setImageResource(R.drawable.bella)
Log.d(TAG, "dogDescription and dogImage were set")
title = getString(R.string.details_about, dogName)
}
Logcat:
2022-10-02 08:32:25.545 9660-9660/com.example.dogglers D/DetailActivity: null, null, null, She, null
2022-10-02 08:32:25.558 9660-9660/com.example.dogglers D/DetailActivity: dogDescription and dogImage were set
DogCardAdapter.kt:
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.dogglers.DetailActivity
import com.example.dogglers.DetailActivity.Companion.DOG_AGE
import com.example.dogglers.DetailActivity.Companion.DOG_HOBBIES
import com.example.dogglers.DetailActivity.Companion.DOG_IMAGE
import com.example.dogglers.DetailActivity.Companion.DOG_NAME
import com.example.dogglers.DetailActivity.Companion.DOG_SEX
import com.example.dogglers.R
import com.example.dogglers.const.Layout.GRID
import com.example.dogglers.data.DataSource
/**
* Adapter to inflate the appropriate list item layout and populate the view with information
* from the appropriate data source
*/
class DogCardAdapter(
private val context: Context?,
private val layout: Int
): RecyclerView.Adapter<DogCardAdapter.DogCardViewHolder>() {
// Initialize the data using the List found in data/DataSource
val data = DataSource.dogs
/**
* Initialize view elements
*/
class DogCardViewHolder(view: View?): RecyclerView.ViewHolder(view!!) {
// Declare and initialize all of the list item UI components
val imageView: ImageView = view!!.findViewById(R.id.dog_image)
val dogName: TextView = view!!.findViewById(R.id.dog_name)
val dogAge: TextView = view!!.findViewById(R.id.dog_age)
val dogHobbies: TextView = view!!.findViewById(R.id.dog_hobbies)
var dogSex = "n/a"
val showDetailsButton: Button = view!!.findViewById(R.id.details_button)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DogCardViewHolder {
// Use a conditional to determine the layout type and set it accordingly.
// if the layout variable is Layout.GRID the grid list item should be used. Otherwise the
// the vertical/horizontal list item should be used.
val layoutType = when(layout) {
GRID -> R.layout.grid_list_item
else -> R.layout.vertical_horizontal_list_item
}
// Inflate the layout
val adapterLayout =LayoutInflater.from(parent.context)
.inflate(layoutType, parent, false)
// Null should not be passed into the view holder. This should be updated to reflect
// the inflated layout.
return DogCardViewHolder(adapterLayout)
}
override fun getItemCount(): Int = data.size
override fun onBindViewHolder(holder: DogCardViewHolder, position: Int) {
val resources = context?.resources
// Get the data at the current position
val item = data[position]
// Set the image resource for the current dog
holder.imageView.setImageResource(item.imageResourceId)
// Set the text for the current dog's name
holder.dogName.text = item.name
// Set the text for the current dog's age
holder.dogAge.text = resources?.getString(R.string.dog_age, item.age)
// Set the text for the current dog's hobbies by passing the hobbies to the
// R.string.dog_hobbies string constant.
holder.dogHobbies.text = resources?.getString(R.string.dog_hobbies, item.hobbies)
// Passing an argument to the string resource looks like:
// resources?.getString(R.string.dog_hobbies, dog.hobbies)
// Set the dog's sex variable
holder.dogSex = item.sex
// Declare context var
val context = holder.itemView.context
// Setting up OnClickListener
holder.showDetailsButton.setOnClickListener {
val intent = Intent(context, DetailActivity::class.java)
intent.putExtra(DOG_IMAGE, item.imageResourceId.toInt())
intent.putExtra(DOG_NAME, holder.dogName.toString())
intent.putExtra(DOG_AGE, holder.dogAge.toString())
intent.putExtra(DOG_HOBBIES, holder.dogHobbies.toString())
intent.putExtra(DOG_SEX, holder.dogSex.toString())
context.startActivity(intent)
}
}
}
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))
}
apologies for my limited knowledge of programming and any sloppiness. I have a reyclerview with alarm objects that I can add and it creates them. When I add say 4 alarms, and delete three of them. The last alarms checkbox is checked by itself. I can not in anyway use the checkbox.setChecked() method for some reason. android studio is not recognizing it, if anyone could please let me know why that is. Also if you know of a solution to the auto check on the last alarm object please.
package com.example.alarmclock
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.Checkable
import android.widget.EditText
import android.widget.TextView
import androidx.core.widget.doAfterTextChanged
import androidx.core.widget.doBeforeTextChanged
import androidx.recyclerview.widget.RecyclerView
import java.security.Key
class AlarmAdapter (private val alarmList: MutableList<Alarm>) : RecyclerView.Adapter<AlarmAdapter.ViewHolder>() {
//start viewholder
inner class ViewHolder(alarm: View) : RecyclerView.ViewHolder(alarm) {
val alarmLabel = itemView.findViewById<EditText>(R.id.alarmLabel)
val editTextTime = itemView.findViewById<EditText>(R.id.editTextTime)
val textView1 = itemView.findViewById<TextView>(R.id.textView1)
val deleteCheckBox = itemView.findViewById<Button>(R.id.deleteAlarmCheckBox)
//val deleteButton = itemView.findViewById<Button>(R.id.deleteAlarmButton)
//val addButton = itemView.findViewById<Button>(R.id.addAlarmButton)
val mondayCheckBox = itemView.findViewById<Button>(R.id.mondayCheckBox)
val tuesdayCheckBox = itemView.findViewById<Button>(R.id.tuesdayCheckBox)
val wednesdayCheckBox = itemView.findViewById<Button>(R.id.wednesdayCheckBox)
val thursdayCheckBox = itemView.findViewById<Button>(R.id.thursdayCheckBox)
val fridayCheckBox = itemView.findViewById<Button>(R.id.fridayCheckBox)
val saturdayCheckBox = itemView.findViewById<Button>(R.id.saturdayCheckBox)
val sundayCheckBox = itemView.findViewById<Button>(R.id.sundayCheckBox)
val amCheckBox = itemView.findViewById<Button>(R.id.amCheckBox)
val pmCheckBox = itemView.findViewById<Button>(R.id.pmCheckBox)
}//end viewholder
fun addAlarm (alarm: Alarm) {
alarmList.add(alarm)
notifyItemInserted(alarmList.size - 1)
}
fun returnAlarmList (): MutableList<Alarm> {
return alarmList
}
fun removeAlarms() {
alarmList.removeAll {
alarm -> alarm.deleteCheck == true
}
//notifyDataSetChanged()
}
fun deleteAlarm (deletedAlarmList: List<Int> ) {
val deletedListIterator = deletedAlarmList.iterator()
val alarmListIterator = alarmList.iterator()
while (deletedListIterator.hasNext()){
while (alarmListIterator.hasNext()){
if (deletedListIterator.next() == alarmListIterator.next().alarmId){
alarmList.remove(alarmListIterator.next())
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val context = parent.context
val inflater = LayoutInflater.from(context)
val alarmView = inflater.inflate(R.layout.alarms, parent, false)
return ViewHolder(alarmView)
}
override fun getItemCount(): Int {
return alarmList.size
}
override fun onBindViewHolder(holder: AlarmAdapter.ViewHolder, position: Int) {
val alarm: Alarm = alarmList[position]
val alarmLabel = holder.alarmLabel
var textView1 = holder.textView1
var editTextTime = holder.editTextTime
var mondayCheckBox = holder.mondayCheckBox
var tuesdayCheckBox = holder.tuesdayCheckBox
var wednesdayCheckBox = holder.wednesdayCheckBox
var thursdayCheckBox = holder.thursdayCheckBox
var fridayCheckBox = holder.fridayCheckBox
var saturdayCheckBox = holder.saturdayCheckBox
var sundayCheckBox = holder.sundayCheckBox
var amCheckBox = holder.amCheckBox
var pmCheckBox = holder.pmCheckBox
var deleteAlarmCheckBox = holder.deleteCheckBox
var lastCharacter = ""
var secondLastCharacter = ""
deleteAlarmCheckBox.setOnClickListener {
alarm.deleteCheck = !alarm.deleteCheck
}
alarmLabel.doAfterTextChanged {
alarm.alarmLabel = alarmLabel.text.toString()
textView1.text = alarm.alarmLabel
}
editTextTime.doAfterTextChanged {
//lastCharacter = editTextTime.text.get(editTextTime.text.length-1).toString()
textView1.text = lastCharacter
if (editTextTime.text.length == 2 && secondLastCharacter != ":"){
//if (lastCharacter != ":") {
editTextTime.setText(editTextTime.text.toString().plus(":"))
editTextTime.setSelection(editTextTime.text.length)
//}
}
editTextTime.doBeforeTextChanged { _, _, _, _ ->
if (editTextTime.length() != 0) {
secondLastCharacter = editTextTime.text.get(editTextTime.text.length - 1).toString()
}
}
if (editTextTime.text.length == 5 ){
alarm.hour = editTextTime.text.get(0).toString().plus(editTextTime.text.get(1).toString())
if (alarm.hour.toInt() < 10) alarm.hour = "0".plus(alarm.hour)
///////
var inputedTimeList = editTextTime.text.toList()
val timeIterator = inputedTimeList.iterator()
}
}
mondayCheckBox.setOnClickListener {
alarm.monday = !alarm.monday
textView1.text = alarm.monday.toString()
}
tuesdayCheckBox.setOnClickListener {
alarm.tuesday = !alarm.tuesday
}
wednesdayCheckBox.setOnClickListener {
alarm.wednesday = !alarm.wednesday
}
thursdayCheckBox.setOnClickListener {
alarm.thursday = !alarm.thursday
}
fridayCheckBox.setOnClickListener {
alarm.friday = !alarm.friday
}
saturdayCheckBox.setOnClickListener {
alarm.saturday = !alarm.saturday
}
sundayCheckBox.setOnClickListener {
alarm.sunday = !alarm.sunday
}
amCheckBox.setOnClickListener {
alarm.amPm = !alarm.amPm
}
}
}
The answer is quite simple, RecyclerView items are reused, so be sure that you set the all the values onBindViewHolder, because after your item is deleted, the actual view is not, so previously set values might be preset although they are not correct according to your data.
The easiest way would be to have isChecked Boolean value store in the Alarm object, onBindViewHolder always set the isChecked param on the Checkbox according to the value returned from the Alarm and when you change the isChecked inside the Checkbox listener - make sure you also update the value inside the Alarm object.
Another solution would be calling notifyDatasetChanged() on the RecyclerView, but it's definitely not the best solution especially if you have dynamic row deletion (and possibly a neat animation).
P.S. consider using viewBinding in your project, it will save you time writing all that ugly findViewById code :)))
I am building an Android app in Kotlin and i am really new to this.
My app gets data from a sensor that sends 250 data in one sec (250Hz) and save them in a local file and at the same time send them to a server via HTTP requests.
Also there is the possibility to watch these data in app via a Graph, i choose mpAndroid to plot the data into a linear chart.
I have already everything working 100%, except for the live chart, it works but it has a very slow rendering. It can go up to 3 or 4 minutes of delay since the start of the plotting and i need it as close as possible.
Basically my updateGraph method is triggered by few booleans values. When i click on the button to show the graph the boolean value changes to true and the method that receives data from the sensor starts collecting data into an array.
The updateGraph method is called in the onResume method of the LiveGraph Activity, it gets the array and add the data to the Y axis to plot them and actually it does, but as said with a big delay. I am sure the data are right because on the web server i can see them properly.
This is my main thread:
fun renderChartThread() {
val thread = Thread(Runnable {
runOnUiThread {
renderLineChartOnline()
}
})
thread.start()
}
This is the renderLineChartOnline method:
fun renderLineChartOnline(){
isRendering = true
var mChart: LineChart = graph
yArray.add(Entry(0.toFloat(), 1.20!!.toFloat()))
set1 = LineDataSet(yArray, "Saved Session")
set1.setDrawCircles(false);
set1.setDrawValues(false);
set1.setLineWidth(2f)
val data = LineData(set1)
mChart.setData(data)
mChart.getAxisRight().setEnabled(false);
val xAxis = mChart.xAxis
xAxis.position = XAxis.XAxisPosition.BOTTOM_INSIDE
xAxis.setDrawGridLines(false)
xAxis.granularity = 1f
xAxis.textSize = 8f
xAxis.valueFormatter = IndexAxisValueFormatter(xLabel)
}
This is the onResume method
override fun onResume() {
super.onResume()
if (MainActivity.isLiveView == true) {
var mChart: LineChart = graph
//Getting the data from the device activity
dataPoints = SocketActivity.liveBRTDataPoint
updateLiveDataset(dataPoints, mChart)
}
mHandler.postDelayed(mTimer2, 1000)
}
And as last this is my updateGraph method:
fun updateLiveDataset(var1: ArrayList<Float>, mChart: LineChart) {
var i = 0
mTimer2 = object : Runnable {
override fun run() {
i++
yArray.add(Entry(i.toFloat(), var1[i]!!.toFloat()))
// limit the number of visible entries
mChart.setVisibleXRangeMaximum(750f)
mChart.setVisibleXRangeMinimum(750f)
set1 = LineDataSet(yArray, "Live View")
set1.setDrawCircles(false)
set1.setDrawValues(false)
set1.setLineWidth(2f)
mChart.getAxisRight().setEnabled(false);
data = LineData(set1)
mChart.data = data
mChart.setAutoScaleMinMaxEnabled(true);
mChart.axisLeft.removeAllLimitLines()
mChart.axisLeft.resetAxisMaximum()
mChart.axisLeft.resetAxisMinimum()
mChart.notifyDataSetChanged();
mChart.moveViewToX(var1.size.toFloat())
mChart.invalidate()
mChart.notifyDataSetChanged()
mHandler.postDelayed(mTimer2, 4)
}
}
}
Does anyone have any suggestion on how to speed up this process?
Ok, i found a solution that plot real time data and I would like to post to you the solution, maybe can be helpful to someone else. But this gained another problem, i am missing data in the plot process, off course, because the variable that calls the real time array is not synchronized with the function that receives the data from the device.
This is my updated up code.
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.WindowManager
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.components.XAxis
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.formatter.IndexAxisValueFormatter
import kotlinx.android.synthetic.main.activity_live_graph.*
import okhttp3.Response
import java.io.File
import java.io.InputStream
class LiveGraph : AppCompatActivity() {
companion object{
var isRendering:Boolean = false
var isEcg:Boolean = false
var isBrt:Boolean = false
}
lateinit var chart: LineChart
var dataPoints = ArrayList<Float>()
var pre_filter = ArrayList<Float>()
var yArray = ArrayList<Entry>()
lateinit var set1: LineDataSet
var xLabel = ArrayList<String>()
var data = LineData()
private val mHandler: Handler = Handler()
private var mTimer2: Runnable? = null
lateinit var file: String
var thread: Thread? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_live_graph)
// lock the current device orientation
val currentOrientation = this.resources.configuration.orientation
if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
this.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
} else {
this.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
// Keep screen awake
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
chart = graph
run(chart)
}
fun renderChartOnline(data: ArrayList<Float>, chart: LineChart) {
var i = 0
if (data.size>0){
for (d in data) {
i +=1
yArray.add(Entry(i.toFloat(), d.toFloat()))
var sec = i/250.toFloat()
val mainHandler = Handler(Looper.getMainLooper())
if (sec > 60){
var min = sec/60.toFloat()
xLabel.add(min.toString()+"min")
} else{
xLabel.add(sec.toString()+"sec")
}
}
}else{
yArray.add(Entry(0.toFloat(), 1.20!!.toFloat()))
}
set1 = LineDataSet(yArray, "Saved Session")
set1.setDrawCircles(false);
set1.setDrawValues(false);
set1.setLineWidth(2f)
val data = LineData(set1)
chart.setData(data)
chart.getAxisRight().setEnabled(false);
val xAxis = chart.xAxis
xAxis.position = XAxis.XAxisPosition.BOTTOM_INSIDE
xAxis.setDrawGridLines(false)
xAxis.granularity = 1f // only intervals of 1 day
xAxis.textSize = 8f
xAxis.valueFormatter = IndexAxisValueFormatter(xLabel)
chart.invalidate()
}
fun run(chart: LineChart){
runOnlineGraph()
val mainHandler = Handler(Looper.getMainLooper())
}
fun runOnlineGraph(){
isRendering = true
feedMultiple()
}
private fun feedMultiple() {
if (thread != null) thread!!.interrupt()
val runnable = Runnable { addEntry() }
thread = Thread(Runnable {
while (true) {
runOnUiThread(runnable)
try {
Thread.sleep(4)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
})
thread!!.start()
}
override fun onResume() {
super.onResume()
dataPoints = SocketActivity.liveECGDataPoint
renderChartOnline(dataPoints, chart)
}
private fun addEntry() {
/***
* Whit this i get the data saved in the Socket process,
* but this gives me the problem of missing datas received,
* if i use an array the delay increase too much then,
* so i m still looking for a solution to this point
*/
dataPoints = SocketActivity.liveDataForGraph
val data = chart.data
if (data != null) {
var set = data.getDataSetByIndex(0)
// set.addEntry(...); // can be called as well
if (set1 == null) {
data.addDataSet(set)
}
for (i in dataPoints){
data.addEntry(
Entry(
set.entryCount.toFloat(),
i
), 0
)
data.notifyDataChanged()
// let the chart know it's data has changed
chart.notifyDataSetChanged()
// move to the latest entry
chart.moveViewToX(data.entryCount.toFloat())
// limit the number of visible entries
chart.setVisibleXRangeMaximum(750f)
chart.setVisibleXRangeMinimum(750f)
chart.getAxisRight().setEnabled(false);
// move to the latest entry
chart.moveViewToX(data.entryCount.toFloat())
chart.setAutoScaleMinMaxEnabled(true);
chart.axisLeft.removeAllLimitLines()
chart.axisLeft.resetAxisMaximum()
chart.axisLeft.resetAxisMinimum()
chart.notifyDataSetChanged(); // let the chart know it's data changed
chart.invalidate()
}
}
override fun onDestroy() {
super.onDestroy()
mHandler.removeCallbacks(mTimer2);
isRendering = false
}
}
I'm trying to scrub a seekbar of a 3rd party app with Accessibility Services. This is what I'm using to scrub.
val arguments = Bundle()
arguments.putFloat(AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE, 50.0.toFloat())
seekBarNode?.performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.id, arguments)
Since I'm scrubbing a video, the SeekBar position changes but the content does not change.
Does anyone know what the issue is here? Or are there any alternatives to scrub a 3rd party SeekBar with accessibility services?
Also, I've read about GestureDescription to perform swipes. But I don't know how to use that to perform SeekBar scrub.
Try and click the center of the seek bar with dispatchGesture:
fun AccessibilityService.tapCenterOfNode(node: AccessibilityNodeInfo, onDone: (Boolean) -> Any){
this.dispatchPath(
drawPath = pathOnPoint(node.centerInScreen()),
pathDuration = 10,
onDone = {
success -> Log.d("dispatch", "success? $success")
}
)
}
fun AccessibilityService.dispatchPath(drawPath: Path, pathDuration: Long, onDone: (Boolean) -> Any) {
val stroke = GestureDescription.StrokeDescription(drawPath, 0, pathDuration)
val gesture = GestureDescription.Builder().addStroke(stroke).build()
this.dispatchGesture(gesture, object : AccessibilityService.GestureResultCallback() {
override fun onCompleted(gestureDescription: GestureDescription) {
super.onCompleted(gestureDescription)
onDone(true)
}
override fun onCancelled(gestureDescription: GestureDescription) {
super.onCancelled(gestureDescription)
onDone(false)
}
}, null)
}
fun AccessibilityNodeInfo.centerInScreen(): Pair<Float, Float> =
Pair(this.getBoundsInScreen().exactCenterX(), this.getBoundsInScreen().exactCenterY())
fun AccessibilityNodeInfo.getBoundsInScreen(): Rect {
val rect = Rect()
getBoundsInScreen(rect)
return rect
}
fun pathOnPoint(point: Pair<Float, Float>) = Path().apply {
val (x, y) = point
moveTo(x, y)
lineTo(x, y)
}