I've seen many questions (and answers) regarding ViewTreeObserver but none have completely answer the question/problem i'm facing. So here's what i have:
I have some views being added programmatically inside a scrollview. After all the layout has been set I want to scroll to a specific view that is inside the scrollview. For this I'm using View Tree Observer and the code is as follows (Some pseudo of what I'm doing)
viewInsideScrollView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public synchronized void onGlobalLayout() {
firstTime++; // Counts how many times I enter the listener
if (firstTime == 1) {
// Here the fully measure of the scroll view has not happened
} else if (firstTime == 2) {
// I still don't have the full measure of the scroll view (Let's say that, for example, paddings are missing/weren't calculated)
} else if (firstTime == 3) {
// Here is when I can safely get the locationOfTheView variable and the scroll down will happen perfectly!
viewInsideScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
scrollView.scrollTo(0, locationOfTheView);
}
}
});
Note: The reason I add firstTime == 3 is because, in this specific case, I noticed that it only calls the global layout 3 times.
So my question is as follows: How can I detect, cleanly, when the Global Layout has finished all the necessary measurements?
Yes I have some work arounds that works like adding a small delay which will give time for the Global Layout to have it's measures done but I was looking for another (cleaner) way to do that.
Any ideas?
Thanks!
Related
My outer RecyclerView crashes either with
IllegalArgumentException: Scrapped or attached views may not be recycled. isScrap:false isAttached:true...
or
IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
Like the title suggests I have an RecyclerView in the list item layout of the first RecyclerView. This layout is used to display messages and the
inner RecyclerView to display attachments that come with the message. The inner RecyclerViews visibility is set to either GONE or VISIBLE depending whether the message has any attachments or not. The simplified outer list item layout looks like this
ConstraintLayout
TextView
TextView
TextView
RecyclerView
And the part of the adapter that handles the inner RecyclerView looks like this
private fun bindFiles(message: Message?) = with(itemView) {
if (message != null && message.attachments.isNotEmpty())
{
sent_message_attachments.setAsVisible()
sent_message_attachments.layoutManager = GridLayoutManager(this.context,Math.min(message.attachments.size,3))
sent_message_attachments.adapter = AttachmentAdapter(message.attachments)
sent_message_attachments.itemAnimator = null
sent_message_attachments.setHasFixedSize(true)
}
else{
sent_message_attachments.setAsGone()
sent_message_attachments.adapter = null
sent_message_attachments.layoutManager = null
}
}
The bug has something to do with the way I fetch the attachments in the inner adapter since once I disable the part that start the download process, everything is fine. There's no problem when loading images from the device, but once I start the download process, everything goes to hell. This is the part that handles images and kicks off the download process in the inner adapter. I have functions for videos and for other file types that are pretty much the same exact thing but use slightly different layout.
private fun bindImage(item: HFile?) = with(itemView) {
if (item != null)
{
if (item.isOnDevice && !item.path.isNullOrEmpty())
{
if (item.isGif)
{
attachment_image.displayGif(File(item.path))
}
else
{
attachment_image.displayImage(File(item.path))
}
}
else
{
//TODO: Add option to load images manually
FileHandler(item.id).downloadFileAsObservable(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ progress ->
//TODO: Show download process
},
{ error ->
error.printStackTrace()
//TODO: Enable manual retry
},
{ notifyItemChanged(adapterPosition)} //onComplete
)
}
}
}
I use the same structure as above in my DiscussionListAdapter to load discussion portraits (profile pictures etc.) and it does not have the same issue.
These are the extensions functions used to inflate the viewHolders and to display the images
fun ViewGroup.inflate(layoutRes: Int): View
{
return LayoutInflater.from(context).inflate(layoutRes, this, false)
}
fun ImageView.displayGif(file:File){
GlideApp.with(context).asGif().load(file).transforms(CenterCrop(), RoundedCorners(30)).into(this)
}
fun ImageView.displayImage(file:File){
GlideApp.with(context).load(file).transforms(CenterCrop(), RoundedCorners(30)).into(this)
}
I've been on this for the past couple of days and just can't get my head around it. Any help in any direction is greatly appreciated. I know my explanations can be a bit all over the place so just ask for clarification when needed :)
UPDATE
I have now been able to produce this with a GridLayout as well as with RecyclerView. It's safe to assume that the nested RecyclerViews were not the culprit here. I even tried to ditch the Rx-piece that handled loading the images and created an IntentService for the process, but the same crashes still occur.
With GridLayout I mean that instead of having another adapter to populate the nested RecyclerView I use only one adapter to populate the message and to inflate and populate views for the attachments as well and to attach those views to the nested GridLayout.
The crash happens when I start to download a file and then scroll the view, that is supposed to show the downloaded file, out of the screen. That view should get recycled but for some reason the download process (which in my test cases only takes around 100ms-400ms) causes the app to throw one of the two errors mentioned in the original question. It might be worth noting that I'm using Realm and the adapter takes in a RealmResults<Message> list as it's dataset. My presenter looks for changes in the list and then notifies the adapter when needed (changed due to the implementation of IntentService).
This is how I'm capable to reproduce this time and time again:
Open a discussion that has messages with attachments
Start to scroll upwards for more messages
Pass a message with an attachment and scroll it off screen while it's still loading
Crash
There is no crash if I stop and wait for the download to complete and everything works as intended. The image/video/file gets updated with a proper thumbnail and the app wont crash if I scroll that out of view.
UPDATE 2
I tried swapping the nested ViewGroup for a single ImageView just to see is the problem within the nestedness. Lo and behold! It still crashes. Now I'm really confused, since the DiscussionListAdapter I mentioned before has the same exact thing in it and that one works like a charm... My search continues. I hope someone, some day will benefit from my agony.
UPDATE 3
I started to log the parent of every ViewHolder in the onBindViewHolder() function. Like expected I got nulls after nulls after nulls, before the app crashed and spew this out.
04-26 21:54:50.718 27075-27075/com.hailer.hailer.dev D/MsgAdapter: Parent of ViewHolder: android.view.ViewOverlay$OverlayViewGroup{82a9fbc V.E...... .......D 0,0-1440,2168}
There's a method to my madness after all! But this just poses more questions. Why is ViewOverlay used here? As a part of RecyclerView or as a part of the dark magicians plans to deprive me of my sanity?
Sidenote
I went digging into RecyclerViews code to check if I could find a reason for the ViewOverlaymystery. I found out that RecyclerView calls the adapters onCreateViewHolder() function only twice. Both times providing itself as the parent argument for the function. So no luck there... What the hell can cause the item view to have the ViewOverlay as it's parent? The parent is an immutable value, so the only way for the ViewOverlay to be set as the parent, is for something to construct a new ViewHolder and supply the ViewOverlay as the parent object.
UPDATE 4
Sometimes I amaze myself with my own stupidity. The ViewOverlay is used because the items are being animated. I didn't even consider this to be an option since I've set the itemAnimator for the RecyclerView as null, but for some odd reason that does not work. The items are still being animated and that is causing this whole charade. So what could be the cause of this? (How I chose to ignore the moving items, I do not know, but the animations became very clear when I forced the app to download same picture over and over again and the whole list went haywire.)
My DiscussionInstanceFragment contains the RecyclerView in question and a nested ConstraintLayout that in turn contains an EditText for user input and a send button.
val v = inflater.inflate(R.layout.fragment_discussion_instance, container, false)
val lm = LinearLayoutManager(context)
lm.reverseLayout = true
v.disc_instance_messages_list.layoutManager = lm
v.disc_instance_messages_list.itemAnimator = null
v.disc_instance_messages_list.adapter = mPresenter.messageAdapter
This is the piece that handles the initialization of the RecyclerView. I'm most definitely setting the itemAnimator as null, but the animations just wont stop! I've tried setting the animateLayoutChanges xml attribute on the root ConstraintLayout and on the RecyclerView but neither of them worked. It's worth mentioning that I also checked whether the RecyclerView had an itemAnimator in different states of the program, and every time I check the animator, it is null. So what is animating my RecyclerView?!
I have faced the same issue
Try this in your child RecyclerView it works for me
RecyclerView childRC = itemView.findViewById(R.id.cmol_childRC);
layoutManager = new LinearLayoutManager(context);
childRC.setItemAnimator(null);
childRC.setLayoutManager(layoutManager);
childRC.setNestedScrollingEnabled(false);
childRC.setHasFixedSize(true);
now set your Adapter like this
ArrayList<Model> childArryList = new ArrayList<>();
childArryList.addAll(arrayList.get(position).getArrayList());
ChildOrderAdapter adapter = new ChildOrderAdapter(context, childArryList);
holder.childRC.swapAdapter(adapter, true);
hope this helps
I finally figured out what was causing this. In my DiscussionInstanceView I have a small view that is animated into and out of view with ConstraintLayout keyframe animations. This view only shows the download progress of the chat history and is used only once, when the discussion is first opened. BUT since I had a call to hiding that view every time my dataset got updated, I was forcing the ConstraintLayout to fire of an animation sequence thus making everything animate during the dataset update. I just added a simple check whether I was downloading the history or not and this problem got fixed.
I am facing following problem:
I got 4 Buttons over the screen, which can be dragged and move back to their original position when dropping the button.
To do so I want to read out the specific position of the button (on the specific device) by using the getX() and getY() function and writing the results into an array.
buttons[0][0] = button.getX();
buttons[0][1] = button.getY();
As I cannot use these functions in the onCreate or in the onStart (the position is always (0,0) cause the view is not instantiated yet I am thinking about how to read out the original position without any extra coding.
Thanks a lot
In your onCreate—though onResume may make more sense—get a reference to a view that is an ancestor of all the buttons (or just the root of the activity, which may be easiest) and then do this:
rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
// Read values here.
buttons[0][0] = button.getX();
buttons[0][1] = button.getY();
}
});
not sure what you mean by "extra coding" but you could use an OnTouchListener (or modify the one you're already using, if that's the case) to capture the position as soon as they touch the button, but before they drag it anywhere (I think that would be ACTION_DOWN).
This has the advantage of only being set when you need it to, skipping it when they don't actually drag the buttons, and not calling it for every layout pass, etc.
I know there are a lot of similar questions, but suggested solutions don't work in my case.
What is my problem.
I have fragment nested inside activity.
Fragment layout includes ViewPager.
What is my idea ? As far as my application won't support landscape (this can cause some additional changes), my ViewPager is loading images from the network.Images can be quietly large and heavy, so I have written server side API which helps to convert image to the desired size on the server and return resized image url.
This will help to get rid of OutOfMemory errors.
But the problem is that I need to send ViewPager's height and width in my request.
I am finding it by id in onViewCreated
#Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mBannerSlider = (AutoScrollViewPager) view.findViewById(R.id.banner_slider);
mBannerSlider.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener(){
#Override
public void onGlobalLayout() {
mSliderHeight = mBannerSlider.getHeight();
mSliderWidth = mBannerSlider.getWidth();
Toast.makeText(getActivity(),"View " + mSliderHeight + " " + mSliderWidth,Toast.LENGTH_SHORT).show();
}
requestBannerImages(mSliderHeight........);
});
The problem here is that this method is called on every page change.
Of course I can use some helper variable to determine if it is first time or not.
Also there is way to send callback from activity to the fragment using this activitiy's method.
#Override
public void onWindowFocusChanged(boolean hasFocus) {
// TODO Auto-generated method stub
super.onWindowFocusChanged(hasFocus);
//Here you can get the size!
}
What is the best solution in this way, maybe I can do this better in another way.
Thanks in advance.
EDIT
I have a plenty of view where I need to get dimensions, so I need to add listener to every view and that after first measurement remove it ?
I would do it with the tree observer. To avoid repeated calls I would remove the listeners once i get the value. This assures you that you will get the dimensions and it wont get call anymore.
if (Build.VERSION.SDK_INT < 16) {
v.getViewTreeObserver().removeGlobalOnLayoutListener(listener);
} else {
v.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
}
I took the code from: https://stackoverflow.com/a/16190337/2051397
I understand your dilemma, mainly because I have implemented a multimedia app. However setting dimensions in every view or many UI elements related to icons/images is time consuming. If not so, then you end up with lots of code doing the same stuff.
In my opinion, you don't have to set dimensions for every view, for the sake of looks. You may set max height of a view/UI or/and set setAdjustViewBound to true, let the UI framework handle the images. There is a cost of performance though if the image is large, in your case probably not.
You may refer to Google notes on setMaxHeight of ImageView. Let me know of your view on this, interested to know.
I'm trying to inspect a code for a very big Android (Amazon Fire TV) activity but i keep loosing the focus in the running app and i don't know what element is being focused.
I'm looking for a way (Wether it's an App, a developer setting - Show Layout Limits gets near - or something i can code inside the activity) to see what view is being focused, without having to change the layout (Selectors) of every single view.
What do you suggest?
Activity has a method called getCurrentFocus().
Maybe you could call hasFocus() on all the Views if the above doesn't work. I imagine the method would look something like this:
public View getFocusedView(View layout)
{
View focusedView = null;
// Note: I'm not sure if FOCUS_DOWN is the right one to use here
// so you may want to see the other constants offered
ArrayList<View> views = layout.getFocusables(View.FOCUS_DOWN)
for(View v: views)
{
if(v.hasFocus())
{
focusedView = v;
}
}
return focusedView;
}
I have a fragment and I need to measure location/width/height of its views on screen and pass to some other class.
So what I have is a function which does it, something like this :
private void measureTest(){
v = ourView.findViewById(R.id.someTextField);
v.getLocationOnScreen(loc);
int w = v.getWidth();
...
SomeClass.passLocation(loc,w);
...
The problem is that the location/width/height of views is not ready within fragment lifecycle.
So if I run that function within these lifecycle methods :
onCreateView
onViewCreated
onStart
onResume
I either get wrong location and width/height measurments or 0 values.
The only solution I found is to add a GlobalLayoutListener like this to mainView
mainView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
if(alreadyMeasured)
mainView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
else
measureTest();
}
});
this gets the job done.. but its just Yack! IMO.
Is there a better way of doing this? seems like such a basic thing to do
inside onActivityCreated of your fragment retrieve the currentView (with getView()) and post a runnable to its queue. Inside the runnable invoke measureTest()
There is no better way. That code isn't that bad! It's fired as soon as the view is layed out (my terminology might be a bit weird there) which happens right after measuring. That is how it is done in the BitmapFun sample (see ImageGridFragment, line 120) in Google's Android docs. There is a comment on that particular piece of code stating:
// This listener is used to get the final width of the GridView and then calculate the
// number of columns and the width of each column. The width of each column is variable
// as the GridView has stretchMode=columnWidth. The column width is used to set the height
// of each view so we get nice square thumbnails.