StaggeredGridLayoutManager with auto-fit span count - android-recyclerview

I need a StaggeredGridLayoutManager that follows these two requirements:
Automatically calculate the number of columns based on a set minimum dp size for each column, with columns expanding to perfectly fit the available space (e.g: if I define 150dp minimum column with, and the app runs on a 480dp width device, the grid view will contain 3 columns, and the remaining 30dp will be split evenly, so that each column is 160dp wide)
Grid layout has to recalculate span count on orientation change, as the number of columns that fit the screen in portrait and landscape mode are usually quite far apart
Until now, I haven't had the need for the grid items to be staggered, so I used an extension of GridLayoutManager to provide this functionality:
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Recycler
import kotlin.math.max
import kotlin.math.min
class GridAutoFitLayoutManager : GridLayoutManager {
private var mColumnWidth = 0
private var mMaximumColumns: Int
private var mLastCalculatedWidth = -1
#JvmOverloads
constructor(
context: Context?,
columnWidthDp: Int,
maxColumns: Int = 99
) : super(context, 1) //Initially set spanCount to 1, will be changed automatically later.
{
mMaximumColumns = maxColumns
setColumnWidth(columnWidthDp)
}
private fun setColumnWidth(newColumnWidth: Int) {
if (newColumnWidth > 0 && newColumnWidth != mColumnWidth) {
mColumnWidth = newColumnWidth
}
}
override fun onLayoutChildren(
recycler: Recycler,
state: RecyclerView.State
) {
val width = width
val height = height
if (width != mLastCalculatedWidth && mColumnWidth > 0 && width > 0 && height > 0) {
val totalSpace: Int = if (orientation == RecyclerView.VERTICAL) {
width - paddingRight - paddingLeft
} else {
height - paddingTop - paddingBottom
}
val spanCount = min(
mMaximumColumns,
max(1, totalSpace / mColumnWidth)
)
setSpanCount(spanCount)
mLastCalculatedWidth = width
}
super.onLayoutChildren(recycler, state)
}
}
Using the layout manager in the recycler view:
recyclerView.layoutManager = GridAutoFitLayoutManager(context, resources.getDimension(R.dimen.grid_column_width).toInt())
Lastly, the item view layouts are set to match_parent on width, so they'll occupy as much space as the layout manager will let them.
However, I'm having trouble getting this to work with StaggeredGridLayoutManager. Here's my code:
import android.content.Context
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Recycler
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import kotlin.math.max
import kotlin.math.min
class GridAutoFitStaggeredLayoutManager : StaggeredGridLayoutManager {
private var mColumnWidth = 0
private var mMaximumColumns: Int
private var mLastCalculatedWidth = -1
#JvmOverloads
constructor(
context: Context?,
columnWidthDp: Int,
maxColumns: Int = 99
) : super(1, LinearLayout.VERTICAL) //Initially set spanCount to 1, will be changed automatically later.
{
mMaximumColumns = maxColumns
setColumnWidth(columnWidthDp)
}
private fun setColumnWidth(newColumnWidth: Int) {
if (newColumnWidth > 0 && newColumnWidth != mColumnWidth) {
mColumnWidth = newColumnWidth
}
}
override fun onLayoutChildren(
recycler: Recycler,
state: RecyclerView.State
) {
val width = width
val height = height
if (width != mLastCalculatedWidth && mColumnWidth > 0 && width > 0 && height > 0) {
val totalSpace: Int = if (orientation == RecyclerView.VERTICAL) {
width - paddingRight - paddingLeft
} else {
height - paddingTop - paddingBottom
}
val spanCount = min(
mMaximumColumns,
max(1, totalSpace / mColumnWidth)
)
setSpanCount(spanCount)
mLastCalculatedWidth = width
}
super.onLayoutChildren(recycler, state)
}
}
As soon as the recycler view is loaded, I get the following exception:
java.lang.IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling
The issue is because I'm calling setSpanCount() while the layout is resolving, which wasn't a problem with GridLayoutManager, but not allowed in StaggeredGridLayoutManager. My question is, if I can't set the span count during onLayoutChildren(), when can I set it?
I'm aware I can just calculate how many columns will fit prior to initializing the layout manager, and pass the span count to the StaggeredGridLayoutManager constructor. However, that won't react to orientation changes, unless I add additional logic to the activity that re-creates the layout manager, which I'd prefer to avoid. Is there a way to make my GridAutoFitStaggeredLayoutManager calculate span count and handle orientation changes on its own?

Had a closer look at the cause of the exception, and this should prevent the error:
import android.content.Context
import android.os.Handler
import android.util.DisplayMetrics
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.Recycler
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import kotlin.math.max
import kotlin.math.min
class GridAutoFitStaggeredLayoutManager : StaggeredGridLayoutManager {
companion object {
private fun setInitialSpanCount(context: Context?, columnWidthDp: Int) : Int {
val displayMetrics: DisplayMetrics? = context?.resources?.displayMetrics
return ((displayMetrics?.widthPixels ?: columnWidthDp) / columnWidthDp)
}
}
private var mContext: Context?
private var mColumnWidth = 0
private var mMaximumColumns: Int
private var mLastCalculatedWidth = -1
#JvmOverloads
constructor(
context: Context?,
columnWidthDp: Int,
maxColumns: Int = 99
) : super(setInitialSpanCount(context, columnWidthDp), VERTICAL)
{
mContext = context
mMaximumColumns = maxColumns
mColumnWidth = columnWidthDp
}
override fun onLayoutChildren(
recycler: Recycler,
state: RecyclerView.State
) {
if (width != mLastCalculatedWidth && width > 0) {
recalculateSpanCount()
}
super.onLayoutChildren(recycler, state)
}
private fun recalculateSpanCount() {
val totalSpace: Int = if (orientation == RecyclerView.VERTICAL) {
width - paddingRight - paddingLeft
} else {
height - paddingTop - paddingBottom
}
val newSpanCount = min(
mMaximumColumns,
max(1, totalSpace / mColumnWidth)
)
queueSetSpanCountUpdate(newSpanCount)
mLastCalculatedWidth = width
}
private fun queueSetSpanCountUpdate(newSpanCount: Int) {
if(mContext != null) {
Handler(mContext!!.mainLooper).post { spanCount = newSpanCount }
}
}
}

Related

how to update Textview in main activity with button in adapter?

i want to update the value of my textview that is in the PosActivity when the button in MyAdapter is click ( to increase/decrease the quantity and to delete the card from recycler view). and i can't find anything on how to do it.
here is what i tried.
in MyAdapter :
package com.mycodlabs.pos.ui.sale.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.mycodlabs.pos.R
import com.mycodlabs.pos.domain.inventory.ProductModel
import kotlinx.android.synthetic.main.pos_item_card.view.*
import java.math.BigDecimal
class MyAdapter(mUx: Context, var selectedItems: ArrayList<ProductModel>) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
val mUx = mUx
// var quantity = 1
var totalPrice = BigDecimal.ZERO
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var pclose = itemView.removeItempos
var pname = itemView.pos_name
var pprice = itemView.pos_price
var pqty = itemView.pos_qty
var pminus = itemView.cart_minus_img
var pplus = itemView.cart_plus_img
// val pimage = itemView.pos_image
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.pos_item_card, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val pos: ProductModel = selectedItems[position]
holder.pname.text = pos.name
holder.pprice.text = pos.unitPrice.toString()
holder.pqty.text = pos.quantity.toString()
// holder.pimage.= pos.image
holder.pclose.setOnClickListener {
val username = pos.name
var price = pos.unitPrice
totalPrice -= price
selectedItems.removeAt(position)
notifyDataSetChanged()
notifyItemRangeChanged(position, selectedItems.size)
// Toast.makeText(mUx, "User $username Deleted", Toast.LENGTH_SHORT).show()
}
holder.pminus.setOnClickListener {
if(pos.quantity == 1 ){
// Toast.makeText(mUx,"Can't go any lower", Toast.LENGTH_SHORT).show()
}else {
pos.quantity -= 1
notifyItemChanged(position)
}
}
holder.pplus.setOnClickListener {
pos.quantity += 1
notifyItemChanged(position)
}
}
fun grandTotal(items: ArrayList<ProductModel>): BigDecimal {
totalPrice = BigDecimal.ZERO
for (i in items.indices) {
totalPrice += items[i].unitPrice.multiply(items[i].quantity.toBigDecimal())
}
return totalPrice
}
fun clearData() {
selectedItems.clear()
notifyDataSetChanged()
}
override fun getItemCount() = selectedItems.size
}
and in the Activity:
package com.mycodlabs.pos.ui
import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.mycodlabs.pos.R
import com.mycodlabs.pos.db.AndroidDatabase
import com.mycodlabs.pos.db.DatabaseTables
import com.mycodlabs.pos.db.inventory.InventoryDbo
import com.mycodlabs.pos.db.sale.SalesLinesDao
import com.mycodlabs.pos.domain.inventory.ProductModel
import com.mycodlabs.pos.ui.sale.adapter.MyAdapter
import kotlinx.android.synthetic.main.activity_pos.*
import kotlinx.android.synthetic.main.adapter_available_promotions.*
import kotlinx.android.synthetic.main.dialog_paymentsuccession.view.*
import kotlinx.android.synthetic.main.dialog_saleedit.*
import kotlinx.android.synthetic.main.layout_addcategory.*
import kotlinx.android.synthetic.main.layout_sale.*
import kotlinx.android.synthetic.main.listview_stock.*
import kotlinx.android.synthetic.main.pos_bottom_sheet.*
import kotlinx.android.synthetic.main.pos_item_card.*
class PosActivity : AppCompatActivity() {
private lateinit var bottomSheetBehavior: BottomSheetBehavior<LinearLayout>
private lateinit var productNames: ArrayList<String>
private lateinit var recyclerView: RecyclerView
private lateinit var db: SalesLinesDao
//// Create an empty list to store the selected items
var selectedItems = ArrayList<ProductModel>()
//// Create an adapter for the RecyclerView
val adapter = MyAdapter(this,selectedItems)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pos)
// for Back button
back.setOnClickListener {
finish()
}
check_out_Pos.setOnClickListener{
// val pos = ProductModel()
//
// db = SalesLinesDbo(this)
//
// pos.name = pos_name.text.toString()
//
// db.addLineItem(pos.id, LineItemModel())
adapter.clearData()
}
// val autoCompleteTextView = findViewById<AutoCompleteTextView>(R.id.searchBoxPos)
// autoCompleteTextView.threshold = 0
// val suggestions =InventoryDbo.getInstance(applicationContext).allProduct
// var NameArray=suggestions.toList().filter { t -> t.name.contains(autoCompleteTextView)}.toList().map { m->m.name }
// val arrayAdapter = ArrayAdapter(this, android.R.layout.simple_expandable_list_item_2 ,NameArray)
// autoCompleteTextView.setAdapter(arrayAdapter)
// Auto Complete Textview filtering from Product table colm "productName"
val autoCompleteTextView = findViewById<AutoCompleteTextView>(R.id.searchBoxPos)
val productNames = ArrayList<String>()
val dbHelper = AndroidDatabase(this)
val db = dbHelper.readableDatabase
val cursor = db.rawQuery(
"SELECT DISTINCT ${InventoryDbo.colm_productName} FROM ${DatabaseTables.TABLE_PRODUCT} WHERE ${InventoryDbo.colm_productName} like '%%'",
null
)
if (cursor.moveToFirst()) {
do {
productNames.add(cursor.getString(cursor.getColumnIndex(InventoryDbo.colm_productName)))
} while (cursor.moveToNext())
}
cursor.close()
db.close()
val adapterr = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, productNames)
autoCompleteTextView.setAdapter(adapterr)
// //// Auto Complete suggestion item display in recyclerview on select
// // Create the RecyclerView
val recyclerView = findViewById<RecyclerView>(R.id.sale_List_Pos)
//// Create a layout manager for the RecyclerView
val layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
// Set an item click listener for the AutoCompleteTextView
searchBoxPos.setOnItemClickListener { _, _, position, _ ->
// Get the selected product name and product from the list
val selectedItem = autoCompleteTextView.adapter.getItem(position).toString()
// val selectedProductName = productNames[position]
val selectedProduct = InventoryDbo.getInstance(applicationContext).getPosProductByName(selectedItem).first()
//InventoryDbo.getProductByName(selectedProductName)
// Add the selected product to the selected items list
selectedItems.add(selectedProduct)
// Notify the adapter that the data has changed
adapter.notifyDataSetChanged()
// Clear the focus and text from the AutoCompleteTextView
searchBoxPos.clearFocus()
searchBoxPos.setText("")
total_items_Pos.text = adapter.grandTotal(selectedItems).toString()
}
//for the bottomsheet
bottomSheetBehavior = BottomSheetBehavior.from<LinearLayout>(std_btm_sht)
bottomSheetBehavior.setBottomSheetCallback(object :
BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, state: Int) {
print(state)
when (state) {
BottomSheetBehavior.STATE_HIDDEN -> {
}
BottomSheetBehavior.STATE_EXPANDED -> {
total_items_Pos.text = adapter.grandTotal(selectedItems).toString()
}
BottomSheetBehavior.STATE_COLLAPSED -> {
total_items_Pos.text = adapter.grandTotal(selectedItems).toString()
}
BottomSheetBehavior.STATE_DRAGGING -> {
total_items_Pos.text = adapter.grandTotal(selectedItems).toString()
}
BottomSheetBehavior.STATE_SETTLING -> {
total_items_Pos.text = adapter.grandTotal(selectedItems).toString()
}
BottomSheetBehavior.STATE_HALF_EXPANDED -> {
total_items_Pos.text = adapter.grandTotal(selectedItems).toString()
}
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
}
})
}
}
i hope you can help me solve this because it was bugging me all day and i couldn't find anything about it
I'll post the changes for the plus button, you can then repeat it for the others. Mind that this is just an example, as I don't know in what way you'd like to update the text or what's the actual name of your TextView.
class MyAdapter(
mUx: Context,
var selectedItems: ArrayList<ProductModel>,
val plusLambda: (String) -> Unit // <------
) : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
...
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val pos: ProductModel = selectedItems[position]
...
holder.pplus.setOnClickListener {
pos.quantity += 1
plusLambda("The new quantity is: ${ pos.quantity }") // <------
notifyItemChanged(position)
}
...
class PosActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pos)
...
val adapterr = ArrayAdapter(this,
android.R.layout.simple_dropdown_item_1line,
productNames) {
someTextView.text = it // <------
}
autoCompleteTextView.setAdapter(adapterr)
...
Of course, you can use just one lambda for all buttons if all you want to do is to change the text of the TextView.

How to set default card without any selection

I am developing e-commerce app in which users can set multiple options for address. Which by default they have to select every time. Each address have a separate address id which helps in identification of address selected. To reduce extra click of users i want to make first address i.e. address id present on position 0 as default selected address. Different address are present in different material card view so you can easily select one of them. My app screen shot is present as below.
enter image description here
My code is as following
import android.content.Context
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
import com.sbhs.stone.R
import com.sbhs.stone.data.UserData
import com.sbhs.stone.databinding.LayoutAddressCardBinding
import com.sbhs.stone.ui.getCompleteAddress
private const val TAG = "AddressAdapter"
class AddressAdapter(
private val context: Context,
addresses: List<UserData.Address>,
private val isSelect: Boolean,
) :
RecyclerView.Adapter<AddressAdapter.ViewHolder>() {
lateinit var onClickListener: OnClickListener
var data: List<UserData.Address> = addresses
var lastCheckedAddress: String? = null
private var lastCheckedCard: MaterialCardView? = null
var selectedAddressPos = -1
inner class ViewHolder(private var binding: LayoutAddressCardBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(address: UserData.Address, position: Int) {
binding.addressCard.isChecked = position == selectedAddressPos
binding.addressPersonNameTv.text =
context.getString(R.string.person_name, address.fName, address.lName)
binding.addressCompleteAddressTv.text = getCompleteAddress(address)
binding.addressMobileTv.text = address.phoneNumber
if (isSelect) {
binding.addressCard.setOnClickListener {
onCardClick(position, address.addressId, it as MaterialCardView)
}
}
//binding.addressCard.setOnFocusChangeListener { view: View, b: Boolean ->
// onCardClick(0, address.addressId, 0 as MaterialCardView)
//}
binding.addressEditBtn.setOnClickListener {
onClickListener.onEditClick(address.addressId)
}
binding.addressDeleteBtn.setOnClickListener {
onClickListener.onDeleteClick(address.addressId)
notifyDataSetChanged()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutAddressCardBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(data[position], position)
}
override fun getItemCount(): Int = data.size
interface OnClickListener {
fun onEditClick(addressId: String)
fun onDeleteClick(addressId: String)
}
private fun onCardClick(position: Int, addressTd: String, card: MaterialCardView) {
if (addressTd != lastCheckedAddress) {
card.apply {
strokeColor = context.getColor(R.color.blue_accent_300)
isChecked = true
strokeWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
2F,
resources.displayMetrics
).toInt()
}
lastCheckedCard?.apply {
strokeColor = context.getColor(R.color.light_gray)
isChecked = false
strokeWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
1F,
resources.displayMetrics
).toInt()
}
lastCheckedAddress = addressTd
lastCheckedCard = card
selectedAddressPos = position
Log.d(TAG, "onCardClick: selected address = $addressTd")
}
}
}
So in above code i want to make selectedAddressPos = 0, & last checked Address as addressTd of address present on position 0. Remember my address can be null so this condition should only applied when address is non-null. Please help me i am new in coding. The information about user address are stored in firebase. so i cant make any card as default from xml.
I tried to make selectedAddressPos as 0 at time of initiation but it will only show it in app and no address id is taken.

How to add the views on the second line if there is no space on the first line?

I am trying to split some words on two lines to create a sentence. When there is no more space on the first line, the words should automatically go to the second line, but no matter what I have tried so far, only the first line is used, while the second line remains empty all the time.
Here is a screen capture.
MainActivity:
class MainActivity : AppCompatActivity(), RemoveAnswerListener {
private var binding: ActivityMainBinding? = null
var listAnswers = mutableListOf<Answer>()
private lateinit var actualAnswer: List<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding?.root)
actualAnswer = listOf(
"What",
"did",
"you",
"want",
"to",
"ask",
"me",
"yesterday?"
)
val answerOption = listOf(
"me",
"yesterday?",
"did",
"to",
"want",
"you",
"ask",
"What"
)
answerOption.forEach {
val key = TextView(binding?.answerBox?.context)
with(key){
binding?.answerBox?.addView(this)
setBackgroundColor(Color.WHITE)
text = it
textSize = 18F
setPadding(40, 20, 40, 20)
val margin = key.layoutParams as FlexboxLayout.LayoutParams
margin.setMargins(30, 30, 30, 30)
layoutParams = margin
setOnClickListener {
moveToAnswer(it)
}
}
}
}
private fun moveToAnswer(view: View) {
if(listAnswers.size < actualAnswer.size){
view.setOnClickListener(null)
listAnswers.add(Answer(view, view.x, view.y, (view as TextView).text.toString(), this#MainActivity))
val topPosition = binding?.lineFirst?.y?.minus(120F)
// val topPosition1 = binding?.lineSecond?.y?.minus(120F)
var leftPosition = binding?.lineFirst?.x
// var leftPosition1 = binding?.lineSecond?.x
if (listAnswers.size > 0) {
var allWidth = 0F
listAnswers.forEach {
allWidth += (it.view?.width)?.toFloat()!! + 20F
}
allWidth -= (view.width).toFloat()
if (allWidth + (view.width).toFloat() + 20F < binding?.lineFirst!!.width) {
leftPosition = binding?.lineFirst?.x?.plus(allWidth)
}else{
// leftPosition1 = binding?.lineSecond?.x?.plus(allWidth)
}
}
val completeMove = object: Animator.AnimatorListener{
override fun onAnimationRepeat(animation: Animator) {}
override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
}
}
if (leftPosition != null) {
if (topPosition != null) {
view.animate()
.setListener(completeMove)
.x(leftPosition)
.y(topPosition)
}
}
}
}
}
And this is the data class "Answer":
data class Answer(var view: View? = null,
var actualPositionX: Float = 0F,
var actualPositionY: Float = 0F,
var text: String = "",
var removeListener: RemoveAnswerListener? = null
){
init {
view?.setOnClickListener {
it.animate()
.x(actualPositionX)
.y(actualPositionY)
removeListener?.onRemove(this)
}
}
}
interface RemoveAnswerListener{
fun onRemove(answer: Answer)
}
You don't seem to be offsetting the View to the second line anywhere? You just initialise its position to the start of the first line:
val topPosition = binding?.lineFirst?.y?.minus(120F)
var leftPosition = binding?.lineFirst?.x
And then you adjust the x position if there's room for it, otherwise it stays at the start
if (allWidth + (view.width).toFloat() + 20F < binding?.lineFirst!!.width) {
leftPosition = binding?.lineFirst?.x?.plus(allWidth)
}
(unless I'm misunderstanding things - I don't know what lineFirst is, a containing view for the first line I guess)
So you're not moving the view down, or to lineSecond or whatever - and you're not adjusting the x position based on the contents of the second line either.
Honestly if I were you, I'd look into the Flow helper for ConstraintLayout - it works like flexbox and basically moves elements below as they fill up the horizontal space, so it automatically does what you're doing here. Here's a guide on it that should give you the idea. Might save you a lot of hassle!

How do I display a new image in tornadofx imageview?

I want to display a WritableImage in imageview, but I want that image to change when the user loads in a new file from the file browser. I know that there is a bind() function for strings that change over time, but I could not find a similar option for images. I could solve the problem for images that are the same size as the default loaded one (with writing through the pixels), but that only works if they are the same size, since I cant modify the size of the image that I displayed.
My Kotlin code so far:
class PhotoView : View("Preview") {
val mainController: mainController by inject()
override val root = hbox {
imageview{
image = mainController.moddedImg
}
hboxConstraints {
prefWidth = 1000.0
prefHeight = 1000.0
}
}
class ControlView: View(){
val mainController: mainController by inject()
override val root = hbox{
label("Controls")
button("Make BW!"){
action{
mainController.makeBW()
}
}
button("Choose file"){
action{
mainController.setImage()
mainController.update()
}
}
}
}
class mainController: Controller() {
private val ef = arrayOf(FileChooser.ExtensionFilter("Image files (*.png, *.jpg)", "*.png", "*.jpg"))
private var sourceImg=Image("pic.png")
var moddedImg = WritableImage(sourceImg.pixelReader, sourceImg.width.toInt(), sourceImg.height.toInt())
fun setImage() {
val fn: List<File> =chooseFile("Choose a photo", ef, FileChooserMode.Single)
sourceImg = Image(fn.first().toURI().toString())
print(fn.first().toURI().toString())
}
fun makeBW() {
val pixelReader = sourceImg.pixelReader
val pixelWriter = moddedImg.pixelWriter
// Determine the color of each pixel in a specified row
for (i in 0 until sourceImg.width.toInt()) {
for (j in 0 until sourceImg.height.toInt()) {
val color = pixelReader.getColor(i, j)
pixelWriter.setColor(i, j, color.grayscale())
}
}
}
fun update() {
val pixelReader = sourceImg.pixelReader
val pixelWriter = moddedImg.pixelWriter
// Determine the color of each pixel in a specified row
for (i in 0 until sourceImg.width.toInt()) {
for (j in 0 until sourceImg.height.toInt()) {
val color = pixelReader.getColor(i, j)
pixelWriter.setColor(i, j, color)
}
}
}
}
ImageView has a property for the image that you can bind:
class PhotoView : View("Preview") {
val main: MainController by inject()
val root = hbox {
imageview { imageProperty().bind(main.currentImageProperty) }
...
}
...
}
class MainController : Controller() {
val currentImageProperty = SimpleObjectProperty<Image>(...)
var currentImage by currentImageProperty // Optional
...
}
From there, any time you set the currentImage in MainController, it will update in the PhotoView.

Why does my tornadoFX ObservableList not receive updates?

I have a simple tornadoFX program that generates some circles in random locations on the screen. However, none of the circles get drawn. I've added some debug code to print a line when a circle is drawn, and it only prints once.
I would expect circles to appear at 100ms intervals, as well as when I click the "Add actor" button.
private const val WINDOW_HEIGHT = 600
private const val WINDOW_WIDTH = 1024
fun main(args: Array<String>) {
Application.launch(MainApp::class.java, *args)
}
class MainApp : App(WorldView::class, Stylesheet::class)
data class Actor(val x: Double, val y: Double)
class WorldView: View("Actor Simulator") {
override val root = VBox()
private val actors = ArrayList<Actor>(0)
init {
tornadofx.runAsync {
(0..100).forEach {
val x = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_WIDTH.toDouble())
val y = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_HEIGHT.toDouble())
actors.add(Actor(x, y))
Thread.sleep(100)
}
}
}
init {
with(root) {
stackpane {
group {
bindChildren(actors.observable()) {
circle {
centerX = it.x
centerY = it.y
radius = 10.0
also {
println("drew circle")
}
}
}
}
button("Add actor") {
action {
actors.add(Actor(0.0, 0.0))
}
}
}
}
}
}
Oddly, if I put a breakpoint during the circle draw code, circles will draw and the debug line will print.
Some observations:
Calling someList.observable() will create an observable list backed by the underlying list, but mutations on the underlying list will not emit events. You should instead initialize actors as an observable list right away.
Access to an observable list must happen on the UI thread, so you need to wrap mutation calls in runLater.
For people trying to run your example - you didn't include a stylesheet, but references one in your App subclass, so the IDEA will most probably import the TornadoFX Stylesheet class. This will not end well :)
The also call has no effect, so I removed it.
I updated your code to best practices here and there, for example with regards to how to create the root node :)
Updated example taking these points into account looks like this:
private const val WINDOW_HEIGHT = 600.0
private const val WINDOW_WIDTH = 1024.0
class MainApp : App(WorldView::class)
data class Actor(val x: Double, val y: Double)
class WorldView : View("Actor Simulator") {
private val actors = FXCollections.observableArrayList<Actor>()
override fun onDock() {
runAsync {
(0..100).forEach {
val x = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_WIDTH.toDouble())
val y = ThreadLocalRandom.current().nextDouble(0.0, WINDOW_HEIGHT.toDouble())
runLater {
actors.add(Actor(x, y))
}
Thread.sleep(100)
}
}
}
override val root = stackpane {
setPrefSize(WINDOW_WIDTH, WINDOW_HEIGHT)
group {
bindChildren(actors) {
circle {
centerX = it.x
centerY = it.y
radius = 10.0
println("drew circle")
}
}
}
button("Add actor") {
action {
actors.add(Actor(0.0, 0.0))
}
}
}
}