On a specific view I am adding a global layout listener:
myView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
// code
LinearLayout linearLayout = (LinearLayout)findViewById(R.id.someView);
if(linearLayout != null && linearLayout.getVisibility() == View.VISIBLE) {
linearLayout.setVisibility(View.GONE);
}
LinearLayout otherLayout = (LinearLayout) findViewById(R.id.someOtherView);
otherLayout.setVisibility(View.GONE);
//other code
}
});
In some cases but I don't know exactly how, during rotation some times it happens that there is NPE for the line otherLayout.setVisibility(GONE)
To be honest I am not sure why the code checks for null in the lines above for the linearLayout and not for the otherLayout but both are defined in the same resource file and are not e.g. removed programmatically anywhere.
The only difference is that the otherLayout is not visible.
So my question is: Are there any things I should look out for on rotation with global layout listeners? Why am I getting NPE in some random cases?
Update:
Both views are part of the same xml file. And actually one defined is after the other. The only difference is that someView is defined as visible and otherView as not visible. Having said that though, there can be such a case where someView is already visible/rendered while otherView has not been yet rendered/made visible when the rotation is happening depending on the current width
I suspect there is a difference between the layouts of findViewById(R.id.someView) and findViewById(R.id.someOtherView). The difference is the timing, for sure, and possibly the layout xml file that it is inflating. With R.id.someOtherView, it is done immediately while R.id.someView, it is executed when the layout is drawn OR at any other time as in screen orientation change since the width/height of the screen changed.
NEW:
final LinearLayout linearLayout = (LinearLayout)findViewById(R.id.someView);
myView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
//LinearLayout linearLayout = (LinearLayout)findViewById(R.id.someView);
if(linearLayout != null && linearLayout.getVisibility() == View.VISIBLE) {
linearLayout.setVisibility(View.GONE);
}
...
}
});
Notes:
I commented out the findViewById() inside onGlobalLayout(), basically removing it.
I think it's not safe to call findViewById() inside the listener since layouts cannot be cached, similar to views and anything related to UI objects. This is what I meant above on my last sentence. I know it's not obvious. I think this explains that the issue is not consistent or strange, as you said essentially.
Related
This is probably an easy question to answer, but searches were just turning up things like inflating and xml layouts, which isn't what I want.
I have a custom TextView that I want to draw a cursor on. (I can't use an EditText.) In order to draw the line, I need to calculate various coordinates from the character offset that the cursor should be at. I tried the following as a method of the custom TextView:
public void setCursorLocation(int characterOffset) {
Layout layout = this.getLayout();
int line = layout.getLineForOffset(characterOffset);
mCursorX = layout.getPrimaryHorizontal(characterOffset);
mCursorBaseY = layout.getLineBaseline(line);
mCursorBottomY = layout.getLineBottom(line);
mCursorAscentY = layout.getLineAscent(line);
}
However, the layout is null. I even tried passing in the TextView as a parameter but the layout was still null. What am I doing wrong?
public void setCursorLocation(View view, int characterOffset) {
Layout layout = ((TextView) view).getLayout();
...
}
Update
I changed it to the following
public void setCursorLocation(Layout layout, int characterOffset) {
but even that layout was null. Then I looked at where I was calling it from in the main activity:
inputWindow.setText(displayText);
inputWindow.setCursorLocation(inputWindow.getLayout(), glyphCurosrPosition);
Watching the expression inputWindow.getLayout() was fine until setText was called and then all of a sudden it became null. I guess that is because it invalidated itself. How/when do I draw the cursor then? My guess is to post a runnable maybe. Update 2 Yes, see below.
This solution finally worked for me.
From the main activity:
inputWindow.setText(displayText);
inputWindow.post(new Runnable() {
#Override
public void run() {
inputWindow.setCursorLocation(glyphCursorPosition);
}
});
In the custom TextView:
public void setCursorLocation(int characterOffset) {
Layout layout = this.getLayout();
int line = layout.getLineForOffset(characterOffset);
mCursorX = layout.getPrimaryHorizontal(characterOffset);
mCursorBaseY = layout.getLineBaseline(line);
mCursorBottomY = layout.getLineBottom(line);
mCursorAscentY = layout.getLineAscent(line);
this.invalidate();
}
The downside is that there is a slight time lag before the cursor updates itself.
I'm trying to grab the dimensions of a view in my activity. The view is a simple custom view which extends an ImageView:
<com.example.dragdropshapes.CustomStrechView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#drawable/border"
android:src="#drawable/missingpuz"
android:clickable="true"
android:onClick="pickShapes"
/>
I need to know what the specific "fill_parent" ends up being. I attempted to get this information during the onCreate method of the Activity using the layout containing my custom views:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_puzzle_picker);
// Show the Up button in the action bar.
setupActionBar();
int a = findViewById(R.id.pickshapes).getMeasuredHeight();
int b = findViewById(R.id.pickshapes).getHeight();
In this case, both a and b return a value of 0. Later, the custom view will be used as a button (it has an onClick handler) so I thought to try again to get the size in the handler:
public void pickShapes(View view){
Intent intent = new Intent(this, ShapesActivity.class);
int a = findViewById(R.id.pickshapes).getMeasuredHeight();
int b = findViewById(R.id.pickshapes).getHeight();
startActivity(intent);
}
Here a and b both give valid dimensions... I don't want to wait for a "onClick" event however, I want to get the dimensions as soon as possible. I've tried Overriding both onStart() and onResume() to check the dimensions as well, but in both cases I still get 0.
So my question is, where in the Android Activity start up flow, is the first place I can get the actual size of a View? I want to be able to get the height/width as soon as I can, and I want to do it before the user has a chance to interact with the environment.
There's a fairly useful thing in Android called the ViewTreeObserver. I've done precisely what you need to do many times this way. As you've discovered, you need to wait until at least the measure cycle completes. Try something like the following:
...
setContextView(R.layout.activity_puzzle_picker);
final View view = findViewById(R.id.pickshapes);
view.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int height = view.getMeasuredHeight();
if(height > 0) {
// do whatever you want with the measured height.
setMyViewHeight(height);
// ... and ALWAYS remove the listener when you're done.
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});
...
(Note that you haven't set the id of your view in your XML... I'm using R.id.pickshapes because that's what you chose.)
I have multiple HorizontalScrollViews inside a ScrollView. Horizontal scroll isn't smooth at all. I have to scroll almost perfectly horizontally for scrolling to work. Is there a simple fix to tweak this ??? Thanks!
You can use Recycler view with Staggered layout manager
StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(4, StaggeredGridLayoutManager.HORIZONTAL);
RecyclerViewAdapter recyclerViewAdapter = newRecyclerViewAdapter(this);
recyclerView.setAdapter(recyclerViewAdapter); //Don't miss to initialize your adapter
This class creates a ScrollView containing a HorizontalScrollView combined into one class. You can put stuff inside it using the AddChild() method. The dispatchTouchEvent overide keeps the scrolling smooth so you can pan around with a single slide of the finger.
(I recently used this to wrap a programmatically created TextView)
class MultiScrollView extends ScrollView
{
public HorizontalScrollView hscroll;
public MultiScrollView ( Context context )
{
super( context );
}
public void AddChild( View child )
{
hscroll.addView( child );
}
#Override
public boolean dispatchTouchEvent( MotionEvent event )
{
hscroll.dispatchTouchEvent(event);
onTouchEvent(event);
return true;
}
}
If you are using the horizontal scroll view solution from (http://www.dev-smart.com/archives/34) the solution for the cross focus problem between the scroll view and the list view is blocking the focus to the scroll view once you have focus on the list view.
From a technical point of view you should add the following line to the onScroll function inside the HorizontalListView class.
getParent().requestDisallowInterceptTouchEvent(true);
Hope this helps.
I've found the solution and still can't believe that this is what you have to do to make this work normal! Just added blank onClickListener to the each item in the HorizontalScrollView:
item.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
}
});
After this slide is really smooth, both upwards and downwards.
In general, you shouldn't be using nested ScrollViews in Android at all, the behaviour of scrolling in this way is unnatural too.
You may want to rethink your layout design, is it anything that couldn't be achieved with an expandable list?
While David's answer works, it has a downside. It passes ScrollView's MotionEvent object directly to HorizontalScrollView.onTouchEvent(), so if HorizontalScrollView or its children try to get the event coordinates, they will get the wrong coordinates which based on ScrollView.
My solution:
public class CustomScrollView extends ScrollView{
/*************skip initialization*************/
#Override
public boolean onInterceptTouchEvent(MotionEvent e){
//returning false means ScrollView is not interested at any events,
//so ScrollView's onTouchEvent() won't be called,
//and all of the events will be passed to ScrollView's child
return false;
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//manually call ScrollView's onTouchEvent(),
//the vertical scrolling happens there.
onTouchEvent(ev);
//dispatch the event,
//ScrollView's child will have every event.
return super.dispatchTouchEvent(ev);
}
}
Just wrap this CustomScrollView around the HorizontalScrollView in your layout file.
I have an android app that let's the user modify some layout parameters. One of my functions let's the user decide if a TextView will be aligned against the top or the bottom of a picture.
This is the function:
private void setAlign(String align) {
/* Get Preferences */
SharedPreferences prefs = getSharedPreferences("prefs", Context.MODE_WORLD_READABLE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("align", align);
editor.commit();
Log.d("ALIGN", align);
paramAlign = align;
FrameLayout.LayoutParams floLP = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
(align == "TOP") ? Gravity.TOP : Gravity.BOTTOM);
txtGoal.setLayoutParams(floLP);
int res = paramAlign == "TOP" ? R.drawable.btn_toolbar_align_top_up : R.drawable.btn_toolbar_align_bottom_up ;
btnAlign.setImageResource(res);
}
Now once the activity is started, this function works fine. However, when I initialize the activity, I call the setAlign() function in the onGlobalLayout method after retrieving the alignment preference.
This is the relevant code:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.personalize);
/* Get Preferences */
SharedPreferences prefs = getSharedPreferences("prefs", Context.MODE_WORLD_READABLE);
paramAlign = prefs.getString("align", "BOTTOM");
Log.d("ALIGN", paramAlign);
// Get screen dimensions and initialize preview
ViewTreeObserver vto = rootView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
setAlign(paramAlign);
ViewTreeObserver obs = rootView.getViewTreeObserver();
obs.removeGlobalOnLayoutListener(this);
}
});
}
Now if you notice the logging functions, they both return "TOP" when I start the activity. And the setAlign() function is obviously getting called. Yet, the TextView is aligned at the bottom. This is the XML for the TextView:
<TextView
android:id="#+id/txtGoal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dip"
android:textColor="#color/color_white"
android:textSize="8sp"
android:shadowColor="#color/color_black"
android:shadowDx="1.2"
android:shadowDy="1.2"
android:shadowRadius="1.2"
/>
Any idea why the setLayoutParams is not happening when the activity is created? The function is getting fired when the layout is done being drawn so it shouldn't be the issue here. And the XML has no gravity specified to start with.
What am I missing here?
First of all, I see that the variable txtGoal is not initialized (nor even declared) so I am assuming you did that somewhere else (that is not posted in the question).
The behavior you are encountering is pretty much normal : the function is working only at start-up, and that's because once you change the layout of your text view, you must indicate that so it will be redrawn, by adding the following :
txtGoal.invalidate ();
after this :
txtGoal.setLayoutParams(floLP);
EDIT:
You can also try changing the gravity in a different way:
txtGoal.setGravity ( Gravity.TOP );
EDIT:
My apologies, what I suggest (the second solution) is wrong, because it changes the gravity of the text inside the text view (and not the gravity of the text view inside the root view).
Please try the following:
Do not try to modify the gravity of your text view using a listener, you can directly apply the gravity you want after setting the content view of your activity (because the text view is thus created). I advise the following:
Apply the new layout to your text view directly (not via the listener) after retrieving the shared preferences.
Your onCreate method should look something similar to this:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.personalize);
/* Get Preferences */
SharedPreferences prefs = getSharedPreferences("prefs", Context.MODE_WORLD_READABLE);
paramAlign = prefs.getString("align", "BOTTOM");
setAlign(paramAlign);
}
You should compare two string using the equals method:
Replace that :
(align == "TOP")
By that
align.equals ( "TOP" )
And this :
paramAlign == "TOP"
By this :
paramAlign.equals ( "TOP" )
So, here is what I found out and what I did to fix my problem.
Apparently the condition
align == "TOP"
Was not testing true when the activity was started altho the Log dump would tell me that it was in fact true at the time. Now why it did that I have no clue. This seems like a weird bug. That condition tested true once the activity was running.
Since this parameter could only have 2 values, I switched it to a Boolean variable where false is now the equivalent of "BOTTOM" and true the equivalent of "TOP" and it is working perfectly.
This is something that might actually need to be looked into as the condition should of tested true at startup.
** EDIT **
You cannot compare 2 strings in java using the "==" operator. You have to use .equals() instead.
I have a LinearLayout, and this LinearLayout will hold dynamically placed views. I need to find out what the width of the children of LinearLayout, however this has to be done in onCreate method. From researching I've found out that you can't use getWidth from this method. So instead I'm using onWindowFocusChanged, which works for the parent LinearLayout (returning me the actual size), but it doesn't work with its children.
Another thing I noticed is that when the screen is fading away and the screen is locked, I can see at the logs the actual width of the children being returned (I think the activity is being paused).
I'm really stuck and this is needed because I need to dynamically place those views depending on the children width.
You might be able to get with the below. But as others pointed out, this probably isn't a great idea.
LinearLayout.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
LinearLayout.getMeasuredWidth();
inside the onCreate , views still can't know the state of the nearby views and the children ,etc... so only after all is prepared and the layout process is done , you can get the size of views .
here's a quick code for getting the size of the view just before it's being drawn:
private static void runJustBeforeBeingDrawn(final View view, final Runnable runnable)
{
final ViewTreeObserver vto = view.getViewTreeObserver();
final OnPreDrawListener preDrawListener = new OnPreDrawListener()
{
#Override
public boolean onPreDraw()
{
Log.d(App.APPLICATION_TAG, CLASS_TAG + "onpredraw");
runnable.run();
final ViewTreeObserver vto = view.getViewTreeObserver();
vto.removeOnPreDrawListener(this);
return true;
}
};
vto.addOnPreDrawListener(preDrawListener);
}
alternatively , you can use addOnGlobalLayoutListener instead of addOnPreDrawListener if you wish.
example of usage :
runJustBeforeBeingDrawn(view,new Runnable()
{
#Override
public void run()
{
int width=view.getWidth();
int height=view.getHeight();
}
});
another approach is to use onWindowFocusChanged (and check that hasFocus==true) , but that's not always the best way ( only use for simple views-creation, not for dynamic creations)
EDIT: Alternative to runJustBeforeBeingDrawn: https://stackoverflow.com/a/28136027/878126
https://stackoverflow.com/a/3594216/1397218
So you should somehow change your logic.