Android: viewTreeObserver listener is never called - android

I would like to use the viewTreeObserver so I can listen to when the layout is finished loading and then get coordinates of some views in that layout. I followed the advice here:
How to get the absolute coordinates of a view
I translated the code to Kotlin, however the function in the listener is never being called.
My code is very simple:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val layoutInflater: LayoutInflater = LayoutInflater.from(applicationContext)
val view: View = layoutInflater.inflate(R.layout.activity_main, null)
view.viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
Log.d("TAG", "Called!!!")
}
})
setContentView(R.layout.activity_main)

The view you inflate and get the viewTreeObserver from is never added to the hierchy. So this observer never calls any events.
When you call setContentView(R.layout.activity_main) a new view is inflated which never had the listener added.
You could use setContentView(view) instead.

Related

Should I inflate the layout in onCreateView or onViewCreated?

I am using the following fragment to show an onboarding screen on the first launch of the application. Should I inflate my layout in onCreateView or in onViewCreated? I don't quite understand how to decide on this. Also, do I need to create a ViewModel for my code?
class OnBoardingFragment : Fragment() {
private lateinit var viewPager: ViewPager
private lateinit var dotsLayout: LinearLayout
private lateinit var sliderAdapter: SliderAdapter
private lateinit var dots: Array<TextView?>
private lateinit var letsGetStarted: Button
private lateinit var next: Button
private lateinit var animation: Animation
private var currentPos: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val navOptions = NavOptions.Builder().setPopUpTo(R.id.onBoardingFragment, true).build()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_onboarding, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewPager = view.findViewById(R.id.slider);
dotsLayout = view.findViewById(R.id.dots);
letsGetStarted = view.findViewById(R.id.get_started_btn);
next = view.findViewById(R.id.next_btn)
sliderAdapter = SliderAdapter(requireContext())
viewPager.adapter = sliderAdapter;
addDots(0);
viewPager.addOnPageChangeListener(changeListener);
next.setOnClickListener {
viewPager.currentItem = currentPos + 1
}
letsGetStarted.setOnClickListener {
findNavController().navigate(R.id.action_onBoardingFragment_to_loginFragment)
}
}
private fun addDots(position: Int) {
dots = arrayOfNulls(2)
dotsLayout.removeAllViews();
for (i in dots.indices) {
dots[i] = TextView(requireContext())
dots[i]!!.text = HtmlCompat.fromHtml("•", HtmlCompat.FROM_HTML_MODE_LEGACY)
dots[i]!!.setTextColor(
ContextCompat.getColor(
requireContext(),
android.R.color.darker_gray
)
)
dots[i]!!.textSize = 35F
dotsLayout.addView(dots[i])
}
if (dots.isNotEmpty()) {
dots[position]!!.setTextColor(
ContextCompat.getColor(
requireContext(),
R.color.wine_red
)
)
}
}
private var changeListener: ViewPager.OnPageChangeListener =
object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
addDots(position)
currentPos = position
animation =
AnimationUtils.loadAnimation(requireContext(), android.R.anim.fade_in)
if (position == 0) {
letsGetStarted.visibility = View.INVISIBLE
next.animation = animation
next.visibility = View.VISIBLE
} else {
letsGetStarted.animation = animation
letsGetStarted.visibility = View.VISIBLE
next.visibility = View.INVISIBLE
}
}
override fun onPageScrollStateChanged(state: Int) {}
}
}`
The Android framework calls Fragment's onCreateView to create the view object hierarchy. Therefore, it's correct to inflate the layout here as you did.
onViewCreated is called afterwards, usually you find views and setup them. So, your code is ok.
Regarding the ViewModel, in your sample code you're just configuring the UI so you won't need it. If instead, you need to obtain some data from an API service, transform it, show the states of "loading data", "data retrieved" and "there was an error retrieving data", then you would like not to do those things in the fragment and you could consider using an MVVM approach.
Some references:
https://developer.android.com/guide/fragments/lifecycle#fragment_created_and_view_initialized
https://guides.codepath.com/android/Creating-and-Using-Fragments
https://developer.android.com/topic/architecture
onCreateView is where you inflate the view hierarchy, and return it (so the Fragment can display it). If you're handling that inflation yourself, you need to override onCreateView so you can take care of it when the system makes that request. That's why it's named that way - when the view (displayed layout) is being created, this function is called, and it provides a View.
onViewCreated is called after the Fragment's view has already been created and provided to it for display. You get a reference to that view passed in, so you can do setup stuff like assigning click listeners, observing View Models that update UI elements, etc. You don't inflate your layout here because it won't be displayed (unless you're explicitly inflating other stuff and adding it to the existing view for some reason, which is more advanced and probably not what you're talking about).
So onCreateView is really concerned with creating a view hierarchy for display, and onViewCreated is for taking that displayed hierarchy and initialising your stuff. You might not need to implement onCreateView at all (e.g. if you use the Fragment constructor that takes a layout ID, so it sets it up for you) in which case you'd just implement onViewCreated instead. Or if you are handling it yourself in onCreateView, and you don't have much setup code, you might run that on the View you've inflated before you return it, and not bother with onViewCreated at all.
It's worth getting familiar with the Fragment lifecycle if you haven't already, just so you know the basic way the system moves between states and the callbacks it calls as it does so (and have a look at the documentation for the callback methods too!)

Android can't change height of constraint layout in post

I am trying to change the height of the ContraintLayout inside the post block.
like so:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
changeHeight()
}
private fun changeHeight() {
val layoutView = findViewById<ConstraintLayout>(R.id.block1)
layoutView.post {
Log.d("222", "1===========")
layoutView.run {
Log.d("222", "2===========")
layoutParams.height = 1000
setBackgroundColor(Color.BLUE)
}
Log.d("222", "3===========")
}
}
}
It doesn't work at all.
And the logs are printed out and the color was changed too.
If it removed the layoutView.post then it works.
I want to figure out why this happened, anyone knows? thanks!
I tried to move the changeHeight() into a button listener to see if the view isn't attached or something relative view. But still the same.

make toolbar disappear when image view is clicked

I want to implement a feature that is sorta like a focus mode, essentially when someone clicks on a image view when looking at an image the tool bar would disappear unless it is reclicked. I believe this allows users to focus more on the image instead of toolbar unless they want information on the image. I have tinkered with this idea and tried to set an onclick feature on the image view, so that once clicked it would turn the visibility on the toolbar to invisible and when its clicked again it would make it visible. The problem is that I can only access the image view in the adapter I set for it and even when I got the alogrithm right (as in I put print statements to see what if statement I enter, and that works successfully) but what happens is that the toolbar doesn't react to it as if I cannot communicate with it.
from adapter
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val page_layout: View = inflater.inflate(R.layout.activity_viewer, container, false)
val presenter: Toolbar = page_layout.findViewById<View>(R.id.presenter) as Toolbar
val image_layout: View = inflater.inflate(R.layout.view_pager_item, container, false)
val page_image: PhotoView = image_layout.findViewById<View>(R.id.page_image) as PhotoView
Picasso.get().load(PageList[position].link).into(page_image)
page_image.setOnClickListener(View.OnClickListener {
println("clicked")
println(presenter.visibility)
if (presenter.visibility == View.INVISIBLE) {
println("outside")
presenter.visibility = View.VISIBLE
} else {
println("inside")
presenter.visibility = View.INVISIBLE
}
})
container.addView(image_layout)
return image_layout
}
from onCreate method from activity
class Page_Activity : AppCompatActivity() {
lateinit var lstPages: MutableList<Page>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_viewer)
lstPages = ArrayList()
mangaPages()
val myrv = findViewById<View>(R.id.view_page) as ViewPager
myViewPager = PageViewAdapter(this, lstPages)
myrv.adapter = myViewPager
the foucus on the activity is to make the call so that there is an array full of image link which gets throws into the adapter so that I can display it back in the activity. Nevertheless, I tested with this concept with a framelayout where I attack a onClickListener, but for someone reason when I do it to an image view in an adapter it act different. Anyways any help with this would be much appreciated!!! Thank you!
Your toolbar doesn't behave as you want because you are inflating a new toolbar in your adapter.
In this code
val page_layout: View = inflater.inflate(R.layout.activity_viewer, container, false)
val presenter: Toolbar = page_layout.findViewById<View>(R.id.presenter) as Toolbar
So, in order to access the toolbar that you have inflated in your Page_Activity, you can implement a callback from your adapter to your Page_Activity, like this
First, create a callback
interface PageImageCallback {
fun onClick()
}
Then, create a variable, setter function and call onClick function in your adapter class
class Adapter() {
private lateinit var pageImageCallback: PageImageCallback
fun setPageImageCallback(pageImageCallback: PageImageCallback) {
this.pageImageCallback = pageImageCallback
}
...
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val image_layout: View = inflater.inflate(R.layout.view_pager_item, container, false)
val page_image: PhotoView = image_layout.findViewById<View>(R.id.page_image) as PhotoView
Picasso.get().load(PageList[position].link).into(page_image)
page_image.setOnClickListener(View.OnClickListener {
println("clicked")
pageImageCallback.onClick()
})
container.addView(image_layout)
return image_layout
}
}
Lastly, implement the callback in Page_Activity
class Page_Activity : AppCompatActivity(), PageImageCallback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_viewer)
...
myViewPager.setPageImageCallback(this)
}
...
override fun onClick() {
if (presenter.visibility == View.INVISIBLE) {
println("outside")
presenter.visibility = View.VISIBLE
} else {
println("inside")
presenter.visibility = View.INVISIBLE
}
}
}

Fragment loses listener at orientation change

I have an activity using fragments. To communicate from the fragment to the activity, I use interfaces. Here is the simplified code:
Activity:
class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {
override fun onAddEntryClicked() {
//DO something
}
override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
//Do something
}
private val diaryFragment: DiaryFragment = DiaryFragment()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
diaryFragment.setOnEntryClickedListener(this)
diaryFragment.setOnDeleteClickedListener(this)
supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
}
}
The fragment:
class DiaryFragment: Fragment() {
private var onEntryClickedListener: IAddEntryClickedListener? = null
private var onDeleteClickedListener: IDeleteClickedListener? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View = inflater.inflate(R.layout.fragment_diary, container, false)
//Some user interaction
onDeleteClickedListener!!.onEntryDeleteClicked()
onDeleteClickedListener!!.onEntryDeleteClicked()
return view
}
interface IAddEntryClickedListener {
fun onAddEntryClicked()
}
interface IDeleteClickedListener {
fun onEntryDeleteClicked()
}
fun setOnEntryClickedListener(listener: IAddEntryClickedListener) {
onEntryClickedListener = listener
}
fun setOnDeleteClickedListener(listener: IDeleteClickedListener) {
onDeleteClickedListener = listener
}
}
This works, but when the fragment is active and the orientation changes from portrait to landscape or otherwise, the listeners are null. I can't put them to the savedInstanceState, or can I somehow? Or is there another way to solve that problem?
Your Problem:
When you switch orientation, the system saves and restores the state of fragments for you. However, you are not accounting for this in your code and you are actually ending up with two (!!) instances of the fragment - one that the system restores (WITHOUT the listeners) and the one you create yourself. When you observe that the fragment's listeners are null, it's because the instance that has been restored for you has not has its listeners reset.
The Solution
First, read the docs on how you should structure your code.
Then update your code to something like this:
class HomeActivity : AppCompatActivity(), DiaryFragment.IAddEntryClickedListener, DiaryFragment.IDeleteClickedListener {
override fun onAddEntryClicked() {
//DO something
}
override fun onEntryDeleteClicked(isDeleteSet: Boolean) {
//Do something
}
// DO NOT create new instance - only if starting from scratch
private lateinit val diaryFragment: DiaryFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
// Null state bundle means fresh activity - create the fragment
if (savedInstanceState == null) {
diaryFragment = DiaryFragment()
supportFragmentManager.beginTransaction().replace(R.id.content_frame, diaryFragment)
}
else { // We are being restarted from state - the system will have
// restored the fragment for us, just find the reference
diaryFragment = supportFragmentManager().findFragment(R.id.content_frame)
}
// Now you can access the ONE fragment and set the listener on it
diaryFragment.setOnEntryClickedListener(this)
diaryFragment.setOnDeleteClickedListener(this)
}
}
Hope that helps!
the short answer without you rewriting your code is you have to restore listeners on activiy resume, and you "should" remove them when you detect activity losing focus. The activity view is completely destroyed and redrawn on rotate so naturally there will be no events on brand new objects.
When you rotate, "onDestroy" is called before anything else happens. When it's being rebuilt, "onCreate" is called. (see https://developer.android.com/guide/topics/resources/runtime-changes)
One of the reasons it's done this way is there is nothing forcing you to even use the same layout after rotating. There could be different controls.
All you really need to do is make sure that your event hooks are assigned in OnCreate.
See this question's answers for an example of event assigning in oncreate.
onSaveInstanceState not working

Kotlin synthetic view in activity gets null when a custom dialog is shown and dismissed

I am updating a view in activity's onCreate method which is working fine using kotlin extension as stated below.
Activity's onCreate
import kotlinx.android.synthetic.main.activity_otpverification.*
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_otpverification)
tvContactNumber.text = getString(R.string.dual_string_value_placeholder)
}
Then at a click of a button I am showing a custom dialog for performing some action. When the dialog is dismissed I update the same textView in activity with the data sent from dialog, but the view tvContact is throwing null exception.
Activity's onClick
override fun onClick(p0: View?) {
when (p0?.id) {
R.id.ivEdit -> {
object : ChangeNumberDialog(this) {
override fun onSubmitClicked(number: String) {
tvContactNumber.text =number
}
}.show()
}
}
}
onSubmitClicked is an abstract method in the dialog which is triggered when the dialog is dismissed.
Error from logcat :
java.lang.IllegalStateException: tvContactNumber must not be null
at com.beat.em.ui.activities.OTPVerificationActivity$onClick$1.onSubmitClicked
(OTPVerificationActivity.kt:211)
onCreate and onClick methods from the ChangeNumberDialog:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = layoutInflater.inflate(R.layout.dialog_change_number, null, false)
setContentView(view)
setCanceledOnTouchOutside(false)
setCancelable(true)
tvSubmit.setOnClickListener(this)
}
override fun onClick(view: View) {
when (view.id) {
R.id.tvSubmit -> {
onSubmitClicked(etNumber.text.toString().trim())
dismiss()
}
}
}
I have just started using kotlin extension and not able to understand the cause. Help appreciated.
The variable you are trying to access is in another scope, try to add explicit scope to the view i.e.
this#YourActivity.tvContactNumber.text = number

Categories

Resources