Android Question "RESOLVED" change image of a button in execution

Jose Briceño

Member
Licensed User
I have this code, there are several buttons with images, the idea is to press and change the image, to the selected image and when I press again to return to the main image.

Sub Activity_Create(FirstTime As Boolean)
Dim i As Int
Dim pos As Int

scvMain.Initialize(400)
Activity.AddView(scvMain, 0, 0, 100%x,100%y)
Activity.LoadLayout("ScrollViewNLayouts")


Dim pnltest As Panel
Dim lblTest As Label
Dim btnTest As Button
Dim edtTest As EditText

Dim pnl As Panel
Dim sld As StateListDrawable
Dim bmpTeam As BitmapDrawable
Dim bmpSel As BitmapDrawable
Dim bmpBackground As Bitmap
Dim bmdBackground As BitmapDrawable
Dim etiqueta As Label
Dim boton2 As Button

'Dim TipoBus As Int
Dim Bus() As Int
Dim Posicion() As Int
Dim Buttons(64) As Button
TipoBus=25

PanelNb=6
' Asiento 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
Bus = Array As Int(1 , 95, 315,1 ,95, 315,1 , 95, 315,1 , 95, 315, 1 , 95, 315, 1 , 95, 315, 1 , 95, 315, 1 , 95, 315, 1)
Posicion = Array As Int(1,1,1 ,1, 100, 100,100,190,190,190, 280, 280, 280, 370, 370, 370, 460, 460, 460, 550, 550, 550, 640, 640,640,730,730,730,820,820,820,910,910)






bmpBackground.Initialize(File.DirAssets,"bus.png")
bmdBackground.Initialize(bmpBackground)
Activity.Background = bmdBackground


For i=1 To TipoBus

If pos <> Posicion(i) Then
'salto de linea
pnltest.Initialize("pnlTest")
scvMain.Panel.AddView(pnltest,0,Posicion(i),100%x,PanelHeight)

pnltest.Tag=i
pos=Posicion(i)

End If

' boton con imagen

btnTest.Initialize("btnTest")

bmpTeam.Initialize(LoadBitmap(File.DirAssets,"asiento.png"))
bmpSel.Initialize(LoadBitmap(File.DirAssets,"asiento_sel.png"))

sld.Initialize
sld.AddState(sld.State_Disabled, bmpTeam )
sld.AddState(sld.State_Pressed, bmpSel )

btnTest.Background = sld

btnTest.Text=i
btnTest.Tag=i
btnTest.TextColor=Colors.Black
btnTest.TextSize=18
pnltest.AddView(btnTest,Bus(i-1)+2%x, 1dip, 70dip, 70dip)
Buttons(i) = btnTest '2dip

Next
scvMain.Panel.Height=PanelNb*PanelHeight

'pnl.Height = lstChecks.Size * height
'Activity.AddMenuItem("Display checked", "mnuChecked")
End Sub


[/url][/IMG]
 

emexes

Well-Known Member
Licensed User
Jose,

four tips, and then I'll get to what (I think) is your main problem:

1/ You've posted in a forum "Android Questions" but not actually asked a question. On the bright side, at least you've described what you're trying to do, so you're halfway there - if you also told us what results you're getting, vs what results you're expecting, then generally people here are super-helpful. So far, you have no responses, which is probably because people are wary of putting wasted effort into answering guessed questions.

2/ Next time, put the code between tags like this:

upload_2019-3-17_8-27-21.png


so that it keeps its structure (mostly) and is more readable like this:

B4X:
For I = 1 To 100
    Log(I)
Next
Righto, that's the housekeeping done, now on to some coding observations.
 

emexes

Well-Known Member
Licensed User
3/ You have a loop "for i = 1 to TipoBus" where i is used to index an array. This always makes me nervous, because arrays in B4A are zero-based, and using an index starting at one is usually and closely followed by an array-out-of-bounds error at the other end of the array when you try to access the "missing" last element.

But I think you've already discovered this: I'm guessing that the "Dim Buttons(64) as Button" used to be "Dim Buttons(25) as Button". No worries - we've all been there and done that at some point ;-)

If I am doing a too-large Dimming because I don't yet know how many I'll need, I usually do something like:

B4X:
Dim MaxNumButtons As Int = 64

Dim Buttons(MaxNumButtons) As Button
Dim NumButtons As Int = 0

Do While more buttons to add
    'add another button
    If NumButtons < MaxNumButtons Then
        Buttons(NumButtons) = AnotherButton
        NumButtons = NumButtons + 1
    Else
        Log("Too many buttons")
    End If
Loop
Yes, array bounds are always checked by B4A/Java and so the error would be caught anyway, but my experience has been that explicit pessimism generally leads to fewer bugs. And yes, you can use a Try..Catch instead of doing the explicit array bounds check yourself, but... to me that feels a bit like driving through a pothole and then repairing the tyre afterwards instead of driving around or stopping before the pothole. Best not to tempt fate.

4/ The Bus() and Posicion() arrays look misaligned. They look like Left and Top coordinates for the buttons, but Posicion() has an extra element at the beginning (ie four 1s instead of three), probably to pad it out to look like a one-based array rather than zero-based? I can live with that, although it'd be less bug-prone if the Bus() and Posicion() indexing was consistent. Also, Posicion() has a heap of extra entries that don't have a corresponding Bus() entry. Better too many than too few, but... it's just going to confuse things when you later add more buttons.
 

emexes

Well-Known Member
Licensed User
Righto, now to your actual probable problem. I'm guessing you expect to see 25 buttons on the screen, but you only see one. If this is the case:

Variables and array elements in B4A are not the actual objects - they are pointers to the objects. It is entirely plausible to have multiple B4A variables pointing to the same object, and that is what you (unexpectedly) have - all of the Buttons() elements are pointing to the same object, aka btnTest, which has only been created (Dimmed) once.

So your code currently does something like this, where a single button is created:

B4X:
Dim NumButtons As Int = 25
Dim Buttons(NumButtons) As Button    'is an array of (empty) pointers to objects, NOT an array of objects

Dim TestButton as Button    'creates a new Button object and makes TestButton point to it
TestButton.Initialize("TestButton")

For I = 0 to NumButtons - 1
    'configure button per I
    TestButton.Tag ="Call me Button " & (I+1)
    TestButton.Left = (I Mod 3) * 100
    TestButton.Top = Floor(I / 3) * 100

    Buttons(I) = TestButton    'Button(I) points to same SINGLE object that TestButton variable always points to
Next

whereas what you were probably hoping for was 25 individual buttons, which is easily done by moving the make-a-new-Button-object code to inside the For loop eg:

B4X:
Dim NumButtons As Int = 25
Dim Buttons(NumButtons) As Button    'is an array of (empty) pointers to objects, NOT an array of objects

For I = 0 to NumButtons - 1
    Dim TestButton as Button    'creates a new Button object and makes TestButton point to it
    TestButton.Initialize("TestButton")

    'configure button per I
    TestButton.Tag = I
    TestButton.Left = (I Mod 3) * 100
    TestButton.Top = Floor(I / 3) * 100

    Buttons(I) = TestButton    'Button(I) points to same NEW object that TestButton variable CURRENTLY points to
Next
Note that I have reverted to zero-based array indexing, ie 0..24 rather than 1..25, and that the tags have thus changed also (which should be no problem, assuming you are using the tag to index back to the array).

Zero-based indexing isn't all bad, eg, it simplifies the Left and Top calculations above. If the array was one-based, you'd be adding and subtracting ones all over the place, which usually takes a few goes to get right (and sometimes not even then).
 
Last edited:

Jose Briceño

Member
Licensed User
Thanks for answering. but the code that shows you well shows the buttons the problem I have is when I click on the buttons do not change the image, only, it does when pressing but then I return to the previous image ... the idea is to keep the pressed image ...
 

emexes

Well-Known Member
Licensed User
Thanks for answering
No worries :)

but the code that shows you well shows the buttons
I don't quite understand this bit (but your English is 1000x better than my Italian)

I think it means that your 25 buttons are all displayed on the screen. I'm kinda intrigued as to how this is happening, given that the code looks like all 25 buttons in the array are pointing to the same object that is only Dimmed once, but hey: if it's working, it's working!

the problem I have is when I click on the buttons do not change the image, only, it does when pressing but then I return to the previous image ... the idea is to keep the pressed image ...
I think what might be happening here is that you are changing the not-being-pressed and the being-pressed images of a momentary button.

Sorry, just got distracted for a long time. Banged up quick sample in B4J. Assume you are implementing some kind of control panel and want to have buttons that act like physical press-on press-off toggle battons, with an indicator of whether they are currently on or off, eg a LED inside the button.

I've used the button text to indicate the current state of each button. Changing the button imagery rather than the text should be straightforward, but I don't have any suitable images, and I do have other tasks piling up, so I'll leave that project for you ;-) If you get stuck for more than an hour, post back here and I'll put in the extra effort.

:)

B4X:
#Region Project Attributes 
    #MainFormWidth: 600
    #MainFormHeight: 600 
#End Region

Sub Process_Globals
    Private fx As JFX
    Private MainForm As Form
   
    Dim NumButtons As Int = 32
    Dim Button(NumButtons) As Button    'initially this is an array of empty pointers
   
    Dim ButtonState(NumButtons) As Int    'using momentary button as toggle button = need to save current state
   
End Sub

Sub AppStart (Form1 As Form, Args() As String)
    MainForm = Form1
    'MainForm.RootPane.LoadLayout("Layout1") 'Load the layout file.

    Dim NumCols As Int = 3
    Dim NumRows As Int = Ceil(NumButtons / NumCols)
   
    Dim NewPadding As Double = MainForm.Width * 0.02    'boldly assuming square pixels
    Dim NewWidth As Double = (MainForm.Width - NewPadding * (NumCols + 1)) / NumCols
    Dim NewHeight As Double = (MainForm.Height - NewPadding * (NumRows + 1)) / NumRows
   
    Dim NewLeft, NewTop As Double
   
    Dim ValveLocations() As String = Array As String("Upper", "Lower", "Middle", "Front", "Back", "Side")
   
    For I = 0 To NumButtons - 1
        Dim NewButton As Button    'create another button
        Button(I) = NewButton    'and save (pointer to) it in array so that we can access it later
       
        'at this point, NewButton and Button(I) variables both point to the one/same object,
        'thus it doesn't matter which we use to alter the button,
       
        NewButton.Initialize("AnyButton")
       
        NewLeft = NewPadding + (I Mod 3) * (NewPadding + NewWidth)
        NewTop = NewPadding + Floor(I / 3) * (NewPadding + NewHeight)
        Log(I & " " & NewLeft & " " & NewTop & " " & NewWidth & " " & NewHeight)
       
        MainForm.RootPane.AddNode(NewButton, NewLeft, NewTop, NewWidth, NewHeight)    'and show on screen
       
        'and at this point, now there are three pointers to the one/same object,
        'so even though we think we've handed it over to RootPane,
        'we still have access to it via NewButton and Button(I)
       
        NewButton.Text = ValveLocations(I Mod 6) & " Valve " & (I + 1)    'number using human one-based rather than computer zero-based
        NewButton.Tag = I    'index into Button array so that we don't have to search for it
    Next
   
    For I = 0 To NumButtons - 1
        ButtonState(I) = Min((I / 2) Mod 3, 1)    'demo = random on/off state
        RefreshButtonAppearance(Button(I))
    Next
   
    MainForm.Show
   
End Sub

'Return true to allow the default exceptions handler to handle the uncaught exception.
Sub Application_Error (Error As Exception, StackTrace As String) As Boolean
    Return True
End Sub

Sub AnyButton_Click
   
    Dim B As Button = Sender
   
    Log("righto, who's the joker that pressed Button(" & B.Tag & ") aka " & B.Text & " ???")
   
    Dim I As Int = B.Tag    'index into Button array
   
    ButtonState(I) = (ButtonState(I) + 1) Mod 4    'advance through states 0, 1, 2, 3, 0...
   
    RefreshButtonAppearance(B)

End Sub

Sub RefreshButtonAppearance(B As Button)    'or could pass it an Index into Button()

    Dim I As Int = B.Tag    'index into Button array
   
    'this is where you paint the button with whatever imagery you like
    'for each of the button's various states (normal toggle button = 2 states,
    'but you can have more if you wish, eg 4 here:
   
    Select Case ButtonState(I)
        Case 0
            B.Text = "Valve " & (I + 1) & " is OFF"
           
        Case 1
            B.Text = "Valve " & (I + 1) & " is ON"
           
        Case 2
            B.Text = "Valve " & (I + 1) & " is AUTO"
           
        Case 3
            B.Text = "[LOCKED OUT]"    '
           
        Case Else
            B.Text = "[ERROR " & ButtonState(I) & "]"    'just in case...
           
    End Select
   
End Sub
upload_2019-3-17_15-18-32.png
 

emexes

Well-Known Member
Licensed User
Then I thought, ah, stuff it, I'll just do it. And I found out that B4J Buttons don't have the background image thing. Well, they probably do, I've just never used it in B4J.

No worries, though, it looked like you were on the right track anyway, but derailed by the button not being a toggle-on-toggle-off button.

What I'd try is (merging the best bits of your code and mine ;-) :

B4X:
'in globals:

Dim NumButtonStates As Int = 4
Dim ButtonStateSld(NumButtonStates) As StateListDrawable


'in startup code:

Dim ButtonStateImageFiles() As String = Array As String("asiento.png", "asiento_sel.png", "state2.png", "state3.png")

For I = 0 To NumButtonStates - 1
    Dim ButtonStateBitmap As BitmapDrawable
    ButtonStateBitmap.Initialize(LoadBitmap(File.DirAssets, ButtonStateImageFiles(I))

    Dim sld As StateListDrawable
    sld.Initialize

    sld.AddState(sld.State_Disabled, ButtonStateBitmap)
    sld.AddState(sld.State_Pressed, ButtonStateBitmap)

    ButtonStateSld(I) = sld    'save for use in refresh routine
Next


'in RefreshButtonAppearance():

Select Case ButtonState(I)    'or just: If ButtonState(I) < NumButtonStates Then
    Case 0, 1, 2, 3:
        Button(I).Background = ButtonStateSld(ButtonState(I))

    Case Else:
        'whatever you want to do if ButtonState is invalid (shouldn't happen, but...)
        Button(I).Background = ButtonStateSld(sld_to_use_if_ButtonState_is_invalid)

End Select
 

Jose Briceño

Member
Licensed User
this code loads all the seats well, and when you press a button it changes it to yellow ... now you only need to put an image when pressing.

B4X:
For i=1 To TipoBus 
        Dim TestButton As Button
        TestButton.Initialize("TestButton")
       
    If pos <> Posicion(i) Then       
       'salto de linea
       pnltest.Initialize("pnlTest") 
       scvMain.Panel.AddView(pnltest,0,Posicion(i),100%x,PanelHeight)
     
       pnltest.Tag=i
       pos=Posicion(i)

    End If
   
    ' boton con imagen
    bmpTeam.Initialize(LoadBitmap(File.DirAssets,"asiento.png"))   
    bmpSel.Initialize(LoadBitmap(File.DirAssets,"asiento_sel.png"))
   
    sld.Initialize
    sld.AddState(sld.State_Disabled, bmpTeam ) 
    sld.AddState(sld.State_Pressed, bmpSel )
   
    TestButton.Background = sld
   
       
    TestButton.Text=i
    TestButton.Tag=i
    TestButton.TextColor=Colors.Black
    TestButton.TextSize=18
    pnltest.AddView(TestButton,Bus(i-1)+2%x, 1dip, 70dip, 70dip)

    Buttons(i) =  TestButton       
   
Next
    scvMain.Panel.Height=PanelNb*PanelHeight
   
    'pnl.Height = lstChecks.Size * height
    'Activity.AddMenuItem("Display checked", "mnuChecked")
End Sub

Sub Activity_Resume

End Sub


Sub TestButton_Click
    Dim Send As Button
    Dim n As Int
   
    Send=Sender
    ToastMessageShow("Botón pulsadoooo nº: " &Send.tag , False)
    Buttons(Send.tag).Color=Colors.Yellow
   
End Sub


I still have this code but the idea is to put an image by clicking on the buttons Thank you
 
Last edited:

emexes

Well-Known Member
Licensed User
Jose,

That looks like an amazing home cinema setup you've got going there!!! Once you get your management app going, perhaps you should get in touch with this guy:

https://www.theage.com.au/national/...rage-into-picture-palace-20180205-p4yzfx.html

or is there already some worldwide organisation for home cinema buffs???

Nextly, partly out of curiousity and partly because it'd be useful to know:

Are you writing in Spanish or in English? I just noticed today that your post is in Spanish when I view it in my email, and in English when I view it through the web interface. It looks like the forum software is automatically translating for us. I am impressed, but also mildly embarrassed that it took me so long to notice.

I am writing in English - does this automatically come up as Spanish on your screen?
 

Attachments

emexes

Well-Known Member
Licensed User
Righto, on to programming matters :)

1/ Is your app now working the way that you want?

2/ If it is not, here are some thoughts I had:

2a/ Does it *have* to be a button? An ImageView and a Label also have Click (and Long Click) events. These might be better for your purpose.

2b/ Speaking of Long Click - you could use normal click to do one thing to the seat (allocate/deallocate? clear request?) and a long click to do something else, like bring up a menu of other less-common options.

2c/ Could you put a label over a seat image, so that you can see the seat image under the text? The Material Icons and Awesome Fonts have characters that could represent: seat occupied, reserved, service request, patron seated.

2d/ Presumably each installation of your app will be used with a different seating layout, so your use of an internal minidatabase (arrays) to store the location and numbering of the seats is a good plan, rather than the automatic grid layout that I had going.

2e/ A cheap way of getting call buttons going might be those Bluetooth iTag keyfinder things - they're like $3 on Ebay, and have a button that can be read. The downsides that I can immediately see are: (i) they beep when the button is pressed (open up and cut wire to buzzer might fix that), and the button seems to only be readable when the Android device is connected to the iTag, which might limit the number of iTags that can be used in one setting.

Chew those over, give it a burl, let me know how it goes. I'm itching to code some of it, but I've got plenty of other stuff to keep me occupied elsewhere, so no worries if you want to keep all the fun to yourself :)
 

emexes

Well-Known Member
Licensed User
Ignore the cinema references: I just noticed the wheels.

Or is it a mobile cinema?!?!

:)
 

klaus

Expert
Licensed User
Instead of Buttons use ToggleButtons for this, they are made for what you want.
Use the StateListDrawable and BitmapDrawables with two bitmaps for each button and use the CheckedChange event instead of the Click event.
Erel had written a StateListDrawable example for ToggleButtons, it uses colors, but you have to replace the the ColorDrawables by BitmapDrawables.
Tips:
1. As already suggested use For i = 0 To TipoBus - 1 instaed of For i = 1 To TipoBus, arrays are 0 indexed.
2. Define the two bitmaps outsides the For / Next loop, loading them only once.
 

emexes

Well-Known Member
Licensed User
Instead of Buttons use ToggleButtons for this, they are made for what you want.
Use the StateListDrawable and BitmapDrawables with two bitmaps for each button and use the CheckedChange event instead of the Click event.
Erel had written a StateListDrawable example for ToggleButtons, it uses colors, but you have to replace the the ColorDrawables by BitmapDrawables.
Good point. I didn't suggest this because it looked like the use case might grow to more than 2 states, in which case the standard button with the Click event seemed better.

1. As already suggested use For i = 0 To TipoBus - 1 instaed of For i = 1 To TipoBus, arrays are 0 indexed.
+1 generally, but what I also noted was that the array in question was psuedo-1-based, because there was some index calculation referring to the previous element, and B4A/Java/C doesn't allow for negative-based arrays either.

BASIC FTW here: a nice feature of most BASICs is that you can DIM (A to B) where A and B can be any integers, including negative (well, as long as A < B, I guess: having said that, I've never actually tried reversing the bounds - how boring am I?!?!) I do miss this flexibility, eg DIM Year(1980 to 2030) or DIM Temperature(-30 to 60) or DIM Direction$(-1 to 1)

2. Define the two bitmaps outsides the For / Next loop, loading them only once.
+1 (assuming they are constant)
 
Top