How to switch to another XML layout after click on button? - kotlin

I would like to have one kotlin file with the logic and I would like to allow users to switch between two different XLM layouts (logic of program is still the same, but layout of buttons shall be changed when clicking on button).
I simply add setContentView function to setOnClickListener for this button in order to load activity_main_second_layout.xml layout.
PS. activity_main_second_layout.xml is almost the same like activity_main.xml, I only changed the position of elements (not the names of elements)
button_switch_to_the_second_design.setOnClickListener {
setContentView(R.layout.activity_main_second_layout);
}
When clicking on the button, voala, the layout really changes to the second one.
BUT the functionality of the program is not working any more, the logic disappear. It seems that I need to resume running of the program somehow to make the code working again without interuption including loss of variables.

There is a lot of ways to do that.
In my opinion you should not try to change layout in runtime - it's possible, but you have to override setContentView and rebind all views and all listeners (or do it in other method, which will be called after changing the layout).
So... Sth like this:
fun sth() {
setContentView(R.layout.activity_main_second_layout)
rebindLayout(R.layout.activity_main_second_layout)
}
fun rebindLayout(#LayoutRes layoutId: Int) {
when (layoutId) {
R.layout.activity_main_first_layout -> { /* rebind views here */ }
R.layout.activity_main_second_layout -> { /* rebind views here */ }
}
}
The other's, better I think is to create independent fragments and change fragment via fragmentManager.
Others approches - ViewAnimator, ViewSwitcher.

Related

Jetpack Compose: scrolling to the bottom of list on event

I have a composable representing list of results:
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState()
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
coroutineScope.launch {
listState.animateScrollToItem(results.size)
}
}
}
}
Expected behaviour: The list smoothly scrolls to the last item whenever a new item is added
Actual behaviour: All is good, but whenever I manually scroll fast through the list, it is also automatically put on the bottom. Also, the scrolling is not smooth.
Your code gives the following error:
Calls to launch should happen inside a LaunchedEffect and not composition
You should not ignore it by calling the side effect directly from the Composable function. See side effects documentation for more details.
You can use LaunchedEffect instead (as this error suggests). By providing results.size as a key, you guarantee that this will be called only once when your list size changes:
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
val listState = rememberLazyListState()
LaunchedEffect(results.size) {
listState.animateScrollToItem(results.size)
}
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
}
}
}
Philip's solution will work for you. However, I'm posting this to ensure that you understand why
A.) The scroll was not smooth
B.) The list gets scrolled to the bottom when you scroll through it fast enough.
Explanation for A.)
It is because you are using animateScollTo. I've experienced issues with this method if called too often,
Explanation for this lies in how Lazy scrollers handle their children internally. You see, Lazy scrollers, as you might know, are meant to display only a small window of a large dataset to the user. To achieve this, it uses internal caching. So, the items on the screen, AND a couple of items to the top and bottom of the current window are cached.
Now, since in your code, you are making a call to animateScrollTo(size) inside the Composable's body (the items scope), the code will essentially be executed upon every composition.
Hence, on every recomposition, there is an active clash between the animateScrollTo method, and the users touch input. When the user scrolls past in a not-so-fast manner, this is what happens - user presses down, gently scrolls, then lifts up the finger. Now, remember this, for as long as the finger is actually pressed down, they animateScrollTo will seem to have no effect (because the user is actively holding a position on the scroller, so it won't be scrolled past it by the system). Hence, while the user is scrolling, some items ahead of the list are cached, but the animateScrollTo does not work. Then, because the motion is slow enough, the distance the scroller travels because of inertia is not a problem, since the list already has enough cached items to show for the distance. That also explains the second problem.
B.)
When you are scrolling through the list FAST enough, the exact same thing as the above case (the slow-scroll) happens. Only, this time the inertia carries the list too forward for the scroller to be handled based on the internal cache, and hence there is active recomposition. However, now since there is no active user input (they have lifted their finger off the screen), it actually does animate to the bottom, since their is no clash here for the animateScrollTo method.
For as long as your finger is pressed, no matter how fast you scroll, it won't scroll to the bottom (test that!)
Now to the solution of the actual problem. Philip your answer is brilliant. The only thing is that it might not work if the developer has an item remove implementation as well. Since only the size of the list is monitored, it will scroll to end when an item is added OR deleted. To counteract that, we would actually need some sort of reference value. So, either you can implement something of your own to provide you with a Boolean variable that actually confirms whether an item has been ADDED, or you could just use something like this.
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
//Right here, create a variable to remember the current size
val currentSize by rememberSaveable { mutableStateOf (results.size) }
//Now, extract a Boolean to be used as a key for LaunchedEffect
var isItemAdded by mutableStateO(results.size > currentSize)
LaunchedEffect (isItemAdded){ //Won't be called upon item deletion
if(isItemAdded){
listState.animateScrollToItem(results.size)
currentSize = results.size
}
}
val listState = rememberLazyListState()
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
}
}
}
This should ensure the proper behaviour. Of course, let me know if there is anything else, happy to help.
Pretty obvious. Why are you calling:
listState.animateScrollToItem(results.size) inside your LazyList? Of course you're going to get extremely bad performance. You shouldn't be messing around with scrolling when items are being rendered. Get rid of this line of code.

How to change the fonts of all items on recyclerview runtime

I wanted to change font family of items on a recycler view every time I click a button.
So I coded like below.
rbAritaBuri = view.findViewById(R.id.rb_aritaBuri)
rbCafe24 = view.findViewById(R.id.rb_cafe24SurroundAir)
rbAritaBuri.setOnClickListener {
rv_work_preview.tv_work_content.typeface = Typeface.createFromAsset(requireActivity().assets, "fonts/arita_buri.otf")
}
rbCafe24.setOnClickListener {
rv_work_preview.tv_work_content.typeface = Typeface.createFromAsset(requireActivity().assets, "fonts/cafe24_surround_air.ttf")
}
But it changes only the font family of the first item of the recycler view.
Is there a way to change fonts of them all together runtime? And please tell me why the code I wrote doesn't work right.
Thank you.
If I were in your position, I would:
Put your font changing calls inside of onBindViewHolder(). If you have to, you could put a bool in there like buttonClicked and link its value to your buttons.
Come up with a good way to force a call to onBindViewHolder(). Sometimes notifyDataSetChanged() is enough. But in some cases, you might have to remove the adapter by setting it to null and then reset the adapter to its original value.
Place that logic from step 2 inside of your buttons' onClick()s.
Edit:
What I mean is, create a var inside the class with the most exterior scope, so outside of oncreate().
var textChoice=""
Now use your buttons to change that var.
rbAritaBuri.setOnClickListener {
textChoice="fonts/arita_buri.otf"
}
Now inside your onBindViewHolder(), make the font switch.
when (fontChoice){
"fonts/arita_buri.otf"->{ rv_work_preview.tv_work_content.typeface = Typeface.createFromAsset(requireActivity().assets, "fonts/arita_buri.otf")}
//and so on and so forth for all of your fonts
Now when you want to show the change, call notifyDatasetChanged(). I think maybe the best place to do that would be inside of your buttons. So maybe you'd actually have:
rbAritaBuri.setOnClickListener {
textChoice="fonts/arita_buri.otf"
<The name of your recyclerview adapter>.notifyDatasetChanged()
}
Here is how I solved it, thanks to D. Kupra:
class SampleWorkAdapter(private val context: Context) :
RecyclerView.Adapter<SampleWorkAdapter.ViewHolder>() {
var selectedFont = EditTextActivity.HAMBAK_SNOW
First, I assigned the default font Hambak_snow to selectedFont, type String.
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
...
fun changeFont(font: String) {
CustomFontHelper.setCustomFont(content, font, itemView.context)
} ...
}
Then I wrote a function to be called on onBindViewHolder to change font-family of textview, using custom typeface. https://stackoverflow.com/a/16648457/15096801 This post helped a lot.
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
...
viewHolder.changeFont(selectedFont)
...
}
Now, replaceFont will be called when the variable selectedFont get changed and adapter.notifyDatasetChanged() is called on an activity, like this:
rbMapoFlowerIsland.setOnClickListener {
sampleWorkAdapter.selectedFont = EditTextActivity.MAPO_FLOWER
sampleWorkAdapter.notifyDataSetChanged()
}

NavigationLink button focusable override issue

I face an issue which stucks for days. I am createing a tvos application which reqiures a custome navigationlink(button), when I move the focus to the navigation item, it should scale a little bit, and also I need to change the parent's view backgound. It is pretty simple, but it seems that the focusabe override the my custome button Style. The test shows that the background image was changed but without any scale effect when the navigationbutton get focused. Any suggestion?
NavigationLink(destination: Text("myview"))
{Text("Test")
}
.buttonStyle(myButtonStyle())
.Focusable(true){(focus) in
//the code to change the background image
//myButtonStyle definition
struct MyButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
return AppButton(configuration: configuration)
}
}
struct AppButton: View {
#Environment(\.isFocused) var focused: Bool
let configuration: ButtonStyle.Configuration
var body: some View {
configuration.label
.scaleEffect(focused ? 1.1 : 1.0)
.focusable(true)
}
}
The line to change the background image is always called when the item get focused as my expected, but the scale effect is gone. If I remove the following line of codes, the scale effect is back:
// .Focusable(true){(focus) in
//the code to change the background image
// }
It looks like that this line of code override my custome style of navigation button, any ideas? Appreciate any help!
Ah, finally I found the tricky, though there is very little document about this. When Focusable was introduced, it should not be in your code to change focus engine, which will cause the navigationlink tap message uncaptured, then your navigationlink for another view will not work.
Use .onChange() function to deal with any focus change event, not use Focusable.

Return imageView rotation position and stop if at a particular position

hoping someone can help. I am creating an app whereby the user will touch a series of images to rotate them. What I am trying to do. Is highlight the image once the user has rotated to a particular position.
Is this possible? If, so any tips greatly appreciated.
edit - ok here's an example instead!
First, the simplest way, based off the code example you just posted:
r1c1.setOnClickListener {
r1c1.animate().apply{ duration = 100 rotationBy(270f) }.start()
}
So the issue here is that you want to highlight the view when it's rotated to, say 90 degrees, right? But it has an animation to complete first. You have three options really
do something like if (r1c1.rotation + 270f == 90) and highlight now, as the animation starts, which might look weird
do that check now, but use withEndAction to run the highlighting code if necessary
use withEndAction to do the checking and highlighting, after the anim has finished
the latter probably makes the most sense - after the animation finishes, check if its display state needs to change. That would be something like this:
r1c1.animate().setDuration(100).rotationBy(270f).withEndAction {
// need to do modulo so 720 == 360 == 0 etc
if (r1c1.rotation % 360 == TARGET_ROTATION) highlight(r1c1)
}.start()
I'm assuming you have some way of highlighting the ImageViews and you weren't asking for ways to do that!
Unfortunately, the problem here is that if the user taps the view in the middle of animating, it will cancel that animation and start a new one, including the rotationBy(270) from whatever rotation the view currently happens to be at. Double tap and you'll end up with a view at an angle, and it will almost never match a 90-degree value now! That's why it's easier to just hold the state, change it by fixed, valid amounts, and just tell the view what it should look like.
So instead, you'd have a value for the current rotation, update that, and use that for your highlighting checks:
# var stored outside the click listener - this is your source of truth
viewRotation += 270f
# using rotation instead of rotationBy - we're setting a specific value, not an offset
r1c1.animate().setDuration(100).rotation(viewRotation).withEndAction {
// checking our internal rotation state, not the view!
if (viewRotation % 360 == TARGET_ROTATION) highlight(r1c1)
}.start()
I'm not saying have a single rotation var hanging around like that - you could, but see the next bit - it's gonna get messy real quick if you have a lot of ImageViews to wrangle. But this is just to demonstrate the basic idea - you hold your own state value, you're in control of what it can be set to, and the View just reflects that state, not the other way around.
Ok, so organisation - I'm guessing from r1c1 that you have a grid of cells, all with the same general behaviour. That means a lot of repeat code, unless you try and generalise it and stick it in one place - like one click listener, that does the same thing, just on whichever view it was clicked on
(I know you said youre a beginner, and I don't like loading too many concepts on someone at once, but from what it sounds like you're doing this could get incredibly bloated and hard to work with real fast, so this is important!)
Basically, View.onClickListener's onClick function passes in the view that was clicked, as a parameter - basically so you can do what I've been saying, reuse the same click listener and just do different things depending on what was passed in. Instead of a lambda (the code in { }, basically a quick and dirty function you're using in one place) you could make a general click listener function that you set on all your ImageViews
fun spin(view: View) {
// we need to store and look up a rotation for each view, like in a Map
rotations[view] = rotations[view] + 270f
// no explicit references like r1c1 now, it's "whatever view was passed in"
view.animate().setDuration(100).rotation(rotations[view]).withEndAction {
// Probably need a different target rotation for each view too?
if (rotations[view] % 360 == targetRotations[view]) highlight(view)
}.start()
}
then your click listener setup would be like
r1c1.setOnClickListener { spin(it) }
or you can pass it as a function reference (this is already too long to explain, but this works in this situation, so you can use it if you want)
r1c1.setOnClickListener(::spin)
I'd recommend generating a list of all your ImageView cells when you look them up (there are a few ways to handle this kind of thing) but having a collection lets you do things like
allCells.forEach { it.setOnClickListener(::spin) }
and now that's all your click listeners set to the same function, and that function will handle whichever view was clicked and the state associated with it. Get the idea?
So your basic structure is something like
// maybe not vals depending on how you initialise things!
val rotations: MutableMap<View, Float>
val targetRotations: Map<View, Float>
val allCells: List<ImageView>
// or onCreateView or whatever
fun onCreate() {
...
allCells.forEach { it.setOnClickListener(::spin) }
}
fun spin(view: View) {
rotations[view] = rotations[view] + 270f
view.animate().setDuration(100).rotation(rotations[view]).withEndAction {
val highlightActive = rotations[view] % 360 == targetRotations[view]
highlight(view, highlightActive)
}.start()
}
fun highlight(view: View, enable: Boolean) {
// do highlighting on view if enable is true, otherwise turn it off
}
I didn't get into the whole "wrapper class for an ImageView holding all its state" thing, which would probably be a better way to go, but I didn't want to go too far and complicate things. This is already a silly length. I might do a quick answer on it just as a demonstration or whatever
The other answer is long enough as it is, but here's what I meant about encapsulating things
class RotatableImageView(val view: ImageView, startRotation: Rotation, val targetRotation: Rotation) {
private var rotation = startRotation.degrees
init {
view.rotation = rotation
view.setOnClickListener { spin() }
updateHighlight()
}
private fun spin() {
rotation += ROTATION_AMOUNT
view.animate().setDuration(100).rotation(rotation)
.withEndAction(::updateHighlight).start()
}
private fun updateHighlight() {
val highlightEnabled = (rotation % 360f) == targetRotation.degrees
// TODO: highlighting!
}
companion object {
const val ROTATION_AMOUNT = 90f
}
}
enum class Rotation(var degrees: Float) {
ROT_0(0f), ROT_90(90f), ROT_180(180f), ROT_270(270f);
companion object {
// just avoids creating a new array each time we call random()
private val rotations = values()
fun random() = rotations.random()
}
}
Basically instead of having a map of Views to current rotation values, a map of Views to target values etc, all that state for each View is just bundled up into an object instead. Everything's handled internally, all you need to do from the outside is find your ImageViews in the layout, and pass them into the RotatableImageView constructor. That sets up a click listener and handles highlighting its ImageView if necessary, you don't need to do anything else!
The enum is just an example of creating a type to represent valid values - when you create a RotatableImageView, you have to pass one of these in, and the only possible values are valid rotation amounts. You could give them default values too (which could be Rotation.random() if you wanted) so the constructor call can just be RotatableImageView(imageView)
(you could make more use of this kind of thing, like using it for the internal rotation amounts too, but in this case it's awkward because 0 is not the same as 360 when animating the view, and it might spin the wrong way - so you pretty much have to keep track of the actual rotation value you're setting on the view)
Just as a quick FYI (and this is why I was saying what you're doing could get unwieldy enough that it's worth learning some tricks), instead of doing findViewById on a ton of IDs, it can be easier to just find all the ImageViews - wrapping them in a layout with an ID (like maybe a GridLayout?) can make it easier to find the things you want
val cells = findViewById<ViewGroup>(R.id.grid).children.filterIsInstance<ImageView>()
then you can do things like
rotatables = cells.map { RotatableImageView(it) }
depends what you need to do, but that's one possible way. Basically if you find yourself repeating the same thing with minor changes, like the infomercials say, There Has To Be A Better Way!

talkback not focusing by default on any view on start of inner fragment

I am using accessibility talkback functionality and I am facing one problem I have one bottom navigation in the parent activity and from the setting tab I am opening another fragment(inner fragment) using .add but the inner fragment view not getting focus by default
I also tried with . replace but it's not focusing by default on fragment creation.
open fragment code
val details = DetailsFragment.newInstance();
getSupportFragmentManager().setupForAccessibility()
getSupportFragmentManager().beginTransaction().add(android.R.id.content, details).commit()
and I used this extension function to not get focus on the previous fragment from this source
fun FragmentManager.setupForAccessibility() {
addOnBackStackChangedListener {
val lastFragmentWithView = fragments.lastOrNull { it.view != null }
for (fragment in fragments) {
if (fragment == lastFragmentWithView) {
fragment.view?.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_YES
} else {
fragment.view?.importantForAccessibility =
View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
}
}
}
}
in normal I show that at the start of the first fragment it's focusing top first Textview and speaking automatic but in the inner fragment it's not focusing by default so what should I do to get focus by default on the first view by default
I already try
android:focusable="true"
android:focusableInTouchMode="true"
and request focus but it's not working
Please suggest me any help would be highly appriciated
I've had the best luck using
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
on the view you want to focus, or maybe
Handler().postDelayed({
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
}, someDelayMillis)
to let the system do whatever it does, and then jump in after a moment and force a focus change.
But this might be considered bad accessibility, if it's interfering with the usual navigation, and that might be why it's so hard to get focus working consistently! It might be better to just announce the new fragment (with something like view.announceForAccessibility("new fragment")) and let the user start navigating from the top. It depends on your app, it's your call
Also you probably want to use replace for your fragment instead of add, if you add a fragment on top of an old one, TalkBack can get stuck looking at the "invisible" views on the old fragment
this is my improved code that extends from #cactustictacs
//target to specific a view
binding.getRoot().postDelayed(new Runnable() {
#Override
public void run() {
binding.textView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
}
}, 300);
full: https://github.com/dotrinh-PM/AndroidTalkback