Spinner dropdown items: how is width determined? - android

I have a Spinner in my app, with customized dropdown views. This is what the layout of the dropdown items look like:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageButton
android:id="#+id/leadingButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_weight="0" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_gravity="center_vertical"
android:layout_weight="1">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="#+id/dropdown_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.appcompat.widget.AppCompatTextView
android:id="#+id/dropdown_text_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/dropdown_text" />
</RelativeLayout>
</FrameLayout>
<androidx.appcompat.widget.AppCompatImageButton
android:id="#+id/trailingButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_weight="0" />
</LinearLayout>
Android Studio warns me that my FrameLayout is useless. But when I take out the FrameLayout the dropdown views become narrow, and don't align with the spinner itself anymore. I have had the same problem when I tried to rewrite the drop-down items with a ConstraintLayout: the dropdown list became narrow, about half of the Spinner's size, and could not display all text, even though the ConstraintLayout had android:layout_width="match_parent".
A sketch to illustrate what I mean:
Why does this happen? How can I predict what the width of the dropdown menu will be based on the layout?
I find this dropdown view sizing quite magical

Did you look at the source code of the Spinner class? I just did. Here's what I found (API 27 Sources):
The spinner uses a ListView internally (first LOL), backed by DropdownPopup (private class):
private class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
Before looking at it, look at ListPopupWindow because has a lot of info about the problems it has to deal with. It's a big class but among these things, you can see:
private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
private int mDropDownHorizontalOffset;
private int mDropDownVerticalOffset;
It appears the DropDown is -by default- WRAPPING the content based upon the base class, however, the DropDownPopup that drives (and contains the adapter with all the items in the spinner) also has a void computeContentWidth() { method.
This method is called from the show() method, so before showing the popup, this computation happens every time.
I think here's part of the answer you're looking for:
void computeContentWidth() {
final Drawable background = getBackground();
int hOffset = 0;
if (background != null) {
background.getPadding(mTempRect);
hOffset = isLayoutRtl() ? mTempRect.right : -mTempRect.left;
} else {
mTempRect.left = mTempRect.right = 0;
}
final int spinnerPaddingLeft = Spinner.this.getPaddingLeft();
final int spinnerPaddingRight = Spinner.this.getPaddingRight();
final int spinnerWidth = Spinner.this.getWidth();
if (mDropDownWidth == WRAP_CONTENT) {
int contentWidth = measureContentWidth(
(SpinnerAdapter) mAdapter, getBackground());
final int contentWidthLimit = mContext.getResources()
.getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right;
if (contentWidth > contentWidthLimit) {
contentWidth = contentWidthLimit;
}
setContentWidth(Math.max(
contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
} else if (mDropDownWidth == MATCH_PARENT) {
setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
} else {
setContentWidth(mDropDownWidth);
}
if (isLayoutRtl()) {
hOffset += spinnerWidth - spinnerPaddingRight - getWidth();
} else {
hOffset += spinnerPaddingLeft;
}
setHorizontalOffset(hOffset);
}
You may want to DEBUG and set breakpoints here to observe what these values are and what they mean.
The other piece there is the setContentWidth() method. This method is from the ListPopupWindow, and looks like:
/**
* Sets the width of the popup window by the size of its content. The final width may be
* larger to accommodate styled window dressing.
*
* #param width Desired width of content in pixels.
*/
public void setContentWidth(int width) {
Drawable popupBackground = mPopup.getBackground();
if (popupBackground != null) {
popupBackground.getPadding(mTempRect);
mDropDownWidth = mTempRect.left + mTempRect.right + width;
} else {
setWidth(width);
}
}
And setWidth (also in that class) all it does is:
/**
* Sets the width of the popup window in pixels. Can also be {#link #MATCH_PARENT}
* or {#link #WRAP_CONTENT}.
*
* #param width Width of the popup window.
*/
public void setWidth(int width) {
mDropDownWidth = width;
}
This mDropDownWidth seems used all over the place, but also made me found this other method in ListPopupWindow...
/**
* Sets the width of the popup window by the size of its content. The final width may be
* larger to accommodate styled window dressing.
*
* #param width Desired width of content in pixels.
*/
public void setContentWidth(int width) {
Drawable popupBackground = mPopup.getBackground();
if (popupBackground != null) {
popupBackground.getPadding(mTempRect);
mDropDownWidth = mTempRect.left + mTempRect.right + width;
} else {
setWidth(width);
}
}
So there you have it, more logic needed including the "window dressing" (?)
I agree the Spinner is a badly designed class (or rather, with outdated design) and even more so with the name (at this Google I/O in 2019, they actually explained in one of the sessions why the name "Spinner" hint: it comes from the 1st android prototypes). By looking at all this code, it would take a few hours to figure out what the spinner is trying to do and how it works, but the trip won't be pleasant.
Good luck.
I will reiterate my advice to use ConstraintLayout which you said you were familiar with; at the very least, discard weights.
By looking at how this works (ListView!!!) the weight calculation warrants a 2nd measure/layout pass, which is not only extremely inefficient and not needed, but also may be causing issues with the internal data adapter this DropDown thing manages so the "list" is displayed.
Ultimately, another class is also involved, this is all presented in a PopupView. PopupViews are what you see when you open a Menu item for example, and are very hard to customize sometimes, depending what you want to do.
Why Google chose this approach at the time, I don't know, but it certainly warrants an update and Material Design hasn't brought much to the table in this regard yet, as it will always be incomplete or in alpha state a year behind anything else.

It is telling you the FrameLayout is useless because it has a single child view ( the Relative Layout).
Your Framelayout has width defined as so:
<FrameLayout
android:layout_width="0dp"
android:layout_weight="1"
Your relative layout has its width defined as:
<RelativeLayout
android:layout_width="match_parent"
So just removing the FrameLayout means that a different "rule" is in place for the width.
To truly replace the FrameLayout with the RelativeLayout it should look like this:
<RelativeLayout
android:layout_width="0dp"
android:layout_weight="1"

Related

Set Android navigation drawer menu footer at bottom of screen

I'm trying to do something that from the beginning I already know it's quite difficult to achieve, and it's to place a footer for a navigation drawer menu at the bottom of the screen.
The fact is that I need the footer to be exactly at the bottom of the screen when the list view items of the drawer are all visible on the screen and the footer should be just below the last item when elements go off the screen and scrollbars appear (normal behaviour).
For that I'm using the addFooterView method in the next way
ViewGroup footer = (ViewGroup)inflater.inflate(R.layout.testme_drawer_footer, mDrawerList, false);
mDrawerList.addFooterView(footer, null, false);
Where testme_drawer_footer is the next layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/footer_menu_facebook"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="start"
android:orientation="horizontal">
<TextView
android:id="#+id/drawer_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#8d3169"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:textColor="#fff"
android:textSize="12sp"/>
</LinearLayout>
Without doing anything the addFooterView just behaves the normal way and if elements are all visible in screen and there is much blank space left at bottom the footer just places below the last element (bad for what I'm trying to achieve).
I've tried many suggestions in different StackOverflow posts with no avail and after struggling my head for a while I was able to get something very close to what I need, and it's the next:
I have given all the list view elements a fixed height and the same for header so in the end I calculate footer height with screenHeight - statusBarHeight - actionBarHeight - drawerHeaderHeight - listViewElementHeight * numberOfElements in the next way:
ViewGroup footer = (ViewGroup)inflater.inflate(R.layout.testme_drawer_footer, mDrawerList, false);
int screenHeight = GeneralUtils.getScreenHeight(oActivity);
int statusBarHeight = GeneralUtils.getStatusBarHeight(oActivity);
int actionBarHeight = GeneralUtils.getActionBarHeight(oActivity);
int drawerHeaderHeight = GeneralUtils.dp2px(60, oActivity);
int menuItemHeight = drawerHeaderHeight;
int totalItemsHeight = menuItemHeight*(endItem-startItem+1);
int footerHeight = screenHeight - statusBarHeight - actionBarHeight - drawerHeaderHeight - totalItemsHeight;
footer.setMinimumHeight(footerHeight);
mDrawerList.setFooterDividersEnabled(true);
mDrawerList.addFooterView(footer, null, false);
But it's most likely some of the height measurement methods are not being quite exact and there is a difference of some pixels that it's not equal in all tested devices.
I know this is not the best way to do it, in fact I don't like it, I don't like to set a fixed height for drawer header and elements insted of wrap_content and I don't like calculating overall height this way but cannot find any other working way to achieve this.
Any help?
Thanks in advance!
This is the way I set the ListView in all activities:
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/llMainMain"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="horizontal">
//MAIN CONTENT
<ListView
android:id="#+id/left_drawer"
android:layout_width="#dimen/navigation_drawer_width_phone"
android:layout_height="match_parent"
android:layout_gravity="left|start"
android:headerDividersEnabled="true"
android:background="#ffeeeeee"/>
</android.support.v4.widget.DrawerLayout>
After struggling my head for the whole day I've finally found a great and accurate solution and I'll answer my own question in order to help anyone who might be facing the same problem.
The key was to set fixed heights to ListView items (in my case I've set a fixed height of 60dp) and calculate ListView height before it gets drawn with a TreeObserver, then you just multiply fixed item height by number of items and substract this value to ListView height. In my case I had to add header height (also fixed) and item dividers size to the total ListView height, so in the end my piece of code looks like follows:
final int numberOfItems = endItem - startItem + 1;
final ViewTreeObserver treeObserver = mDrawerList.getViewTreeObserver();
treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
mDrawerList.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int mDrawerListHeight = mDrawerList.getHeight();
int dividerHeight = mDrawerList.getDividerHeight() * numberOfItems;
int footerHeight = calculateFooterHeight(oActivity, mDrawerListHeight, numberOfItems, dividerHeight);
ViewGroup footer = (ViewGroup)inflater.inflate(R.layout.testme_drawer_footer, mDrawerList, false);
footer.setMinimumHeight(footerHeight);
mDrawerList.setFooterDividersEnabled(true);
mDrawerList.addFooterView(footer, null, false);
UserSession us = new UserSession(oActivity);
String footerText = us.getUserSession().getAlias();
TextView tvDrawerFooter = (TextView) oActivity.findViewById(R.id.drawer_footer);
tvDrawerFooter.setText(footerText);
// Set the list's click listener
DrawerItemClickListener drawer = new DrawerItemClickListener();
drawer.oActivity = oActivity;
mDrawerList.setOnItemClickListener(drawer);
}
});
private static int calculateFooterHeight(Activity oActivity, int listViewHeight, int numberOfItems, int dividerHeight){
int drawerHeaderHeight = GeneralUtils.dp2px(60, oActivity);
int menuItemHeight = drawerHeaderHeight;
int totalItemsHeight = menuItemHeight * numberOfItems;
return listViewHeight - drawerHeaderHeight - totalItemsHeight - dividerHeight;
}
I truly hope I can help anyone else with the same problem.

Android: When to call methods that affect child layout upon parent view size change?

I have a very simple RelativeLayout subclass that adds an image view with a text view on top of it. I have a method, show(), which creates and adds the child views and sets the initial text.
At the point I call show() for the first time, the view does not know how big it is, so I can't set the textSize nor the padding for the textView.
I have a solution that mostly works, where I call setTextSize() and setPadding() for the textView within the overridden method, onSizeChanged(). The text does not show the first time it is displayed. However, it shows every time after that, perfectly sized and placed.
Here is the code for onSizeChanged():
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Log.e(TAG, "onSizeChanged() called");
if (_childTextView != null) {
float textSize = h / 2.0f;
int topPadding = (int)(h / 3.0f);
Log.e(TAG, "setting textSize = " + textSize);
Log.e(TAG, "topPadding = " + topPadding);
_childTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
_childTextView.setPadding(0, topPadding, 0, 0);
}
super.onSizeChanged(w, h, oldw, oldh);
Log.e(TAG, "end onSizeChanged()");
}
The code for show() is as follows:
public void show(int val) {
_val = val;
Log.e(TAG, "in show(), val = " + val);
// create and add background image if not already there
if (_backgroundImageView == null) {
_backgroundImageView = new ImageView(_context);
_backgroundImageView.setImageResource(R.drawable.background);
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
params.addRule(CENTER_IN_PARENT);
addView(_backgroundImageView, params);
}
// create and add text view if not already there
if (_childTextView == null) {
_childTextView = new TextView(_context);
_childTextView.setTextColor(Color.BLACK);
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
params.addRule(CENTER_HORIZONTAL);
addView(_childTextView, params);
}
Log.e(TAG, "setting text to: " + _val);
// update value and make visible
_childTextView.setText(String.valueOf(_val));
setVisibility(VISIBLE);
Log.e(TAG, "end show()");
}
The background image displays correctly every time. The textView only displays correctly the second time show() is called and afterwards. Logging in onSizeChanged() shows that the calculated numbers are correct the first time. As expected, onSizeChanged() only gets called the first time, a bit after we return from show(). Subsequent calls to show() just set the value and visibility, and the text is displayed correctly.
My question is: is there a better way to do this? Or a better callback method to override?
Trying to set these values in show() doesn't work because the main view doesn't yet know its own size (at least the first time). I have tried putting invalidate() at the end of onSizeChanged(). I have also tried putting the call to setText() there.
I need to be able to do this based on size, because this class is reused in different contexts where the image needs to be smaller or larger.
Thank you for any insight you can give. I'd really like to keep this simple if possible.
Edit: What I am trying to do is size some text to be about 1/2 the size of the child image (which is the same as the parent size), and to have top padding set to about 1/3 of the image size. This would be easy if I just wanted it to be one size. However, I want it to be size-adjustable based on the needs of the display.
Imagine a postage stamp, where you want to place the value somewhere precisely in the image. So far so good. But what if this postage stamp needs to be displayed at different sizes on the same phone? You'd want both the placement offset (the padding) and the text size to adjust accordingly. If I hardcode this into the xml, then the text size and placement will not be adjusted when I size the layout. The text will be too big on the small version, and will be placed too far from the top of the image.
i have no idea why you override onSizeChanged(), normaly android handles all this nicely if you use it the way it is intendet.
can you pls explain what you want to achive - maybe with example picture?
however i wondered that you don't override onMesure() when you override the rest and if a delayed call to show() helps it also might be because of onMesure is called in between.
edit:
in android you should never want to know a real size of some views. nearly every device has other sizes and there is portrait/landscape mode too. if you start coding vs real sizes you can give up at the start. instead you should use something more relative like dp and sp than you should never again worry about text sizes and similar.
you may also want and you should use LayoutInflater and xml files as much as possible in your application. in a Activity you can call setContentView(). in other cases there might be methods to overload like onCreateView. and if you have nothing else you can do it like this:
LayoutInflater inflater = LayoutInflater.from(contextEgActivity);
View rootView = inflater.inflate(R.layout.highscore_daily, parentCanBeNull);
edit II:
so this is what you want - right? (on the ImageView and the TextView it would be even better to use wrap_content for height and width)
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:id="#+id/imageView"
android:layout_gravity="center"
android:background="#ff0000" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:text="New Text"
android:id="#+id/textView"
android:layout_gravity="center"
android:background="#00ff00"
android:textSize="20sp" />
</FrameLayout>
if you only have ~3 different sizes i would write 3 different xml files to match what you want. otherwise i think this code will fit your needs.
//http://stackoverflow.com/questions/4605527/converting-pixels-to-dp
public static float convertDpToPixel(float dp, Context context){
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
float px = dp * (metrics.densityDpi / 160f);
return px;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);//loads the xml above
ImageView v = (ImageView) findViewById(R.id.imageView);
int dp = 200;
int px = (int) convertDpToPixel(dp, this);
v.setMaxHeight(px);//no need for it
v.setMinimumHeight(px);//should do more or less the same as next line
v.setLayoutParams(new LinearLayout.LayoutParams(px, px));//is like android:layout_width="200dp" android:layout_height="200dp"
v.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, px));//is like android:layout_width="wrap_content" android:layout_height="200dp"
//basically you can do the same with the TextView + the Text styling
TextView tv = (TextView) findViewById(R.id.textView);
tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 50);
tv.setPadding(30,30,30,30);//don't forget, this is also px so you may need dp to px conversion
}
this is the normal way, nice clean and easy. if you why ever still want to react on size changes of your parent you can try this but i don't suggest it. btw changing view stuff should only be executed from ui/main thread so if the method gets called from a other thread thry a Handler like new Handler(getMainLooper)

Moving a textview next to another textview off-screen

I have a relative layout containing three textviews, each having a width of half-width of the screen. I want the user to be able to use a scroll gesture and move these textviews together, and if the textview located far left goes off-screen, it is moved to the far right next to the third textview. So I want to create a sort of a endless scroller-system.
However, using the code below results in gaps between the views when scrolling, and I think the gap widths are dependable on the scrolling speed.
Here is a link to a screenshot of the problem: http://postimg.org/image/bnl0dqsgd/
Currently I have implemented scrolling only for one direction.
XML:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/rel_layout"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false">
<com.app.healthview.BorderedTextView
android:id="#+id/btvYear1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:background="#color/YearColor1"
android:maxLines="1"
android:text="2012" />
<com.app.healthview.BorderedTextView
android:id="#+id/btvYear2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_alignParentTop="true"
android:layout_toRightOf="#+id/btvYear1"
android:background="#color/YearColor2"
android:maxLines="1"
android:text="2013" />
<com.app.healthview.BorderedTextView
android:id="#+id/btvYear3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#color/YearColor1"
android:layout_alignParentTop="true"
android:layout_toRightOf="#+id/btvYear2"
android:gravity="center"
android:maxLines="1"
android:text="2014" />
</RelativeLayout>
Then I initialize the views in a function, which is called after setting the content view:
public void InitTimeView() {
year_views = new BorderedTextView[3];
year_views[0] = (BorderedTextView) findViewById(R.id.btvYear1);
year_views[1] = (BorderedTextView) findViewById(R.id.btvYear2);
year_views[2] = (BorderedTextView) findViewById(R.id.btvYear3);
// Acquire display size
display = getWindowManager().getDefaultDisplay();
size = new Point();
display.getSize(size);
int year_width = size.x / 2;
year_views[0].setWidth(year_width);
year_views[1].setWidth(year_width);
year_views[2].setWidth(year_width);
// This is done, because when scrolling, the third view which in the beginning is off-screen, could not be seen
RelativeLayout relLayout = (RelativeLayout) findViewById(R.id.rel_layout);
relLayout.getLayoutParams().width = year_width * 4;
relLayout.invalidate();
}
Then the onScroll-method:
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// intCurrYearMember is public, it stores the view that is next to be moved
// intRightYearMember; intCurrYearMember is located right to this view.
switch(intCurrYearMember) {
case 0:
intRightYearMember = 2;
case 1:
intRightYearMember = 0;
case 2:
intRightYearMember = 1;
}
// Move the views
for (TextView textview : year_views) {
textview.setX(textview.getX() - (distanceX / 2));
}
// Check if the view most left is now too far on left, and move it if needed to far right
if ((year_views[intCurrYearMember].getX() + year_views[intCurrYearMember].getWidth()) <= 0) {
// Is the problem here perhaps?
year_views[intCurrYearMember].setX(year_views[intRightYearMember].getRight());
intPreviousMember = intCurrYearMember;
if (intCurrYearMember < 2)
intCurrYearMember++;
else
intCurrYearMember = 0;
}
return true;
}
As it shows in the code, my idea is to build a year scroller. If someone happends to have a better, more efficient idea for how to do it, I am happy to hear your advices!
So my question is: why are there gaps between the textviews?
I would not suggest doing this at all. Utilizing Horizontal Swipes for these things is not a standard use of the platform and why waste your time developing this when there are already tested and pretty Android Components such as the Pickers that you can use easily.
A Horizontal Swiping gesture is normally saved for a Menu Drawer, View Pager, or other piece of functionality.

Small EditText have a setError with a lot of lines

I have a small EditText and I want to display errors (using editText.setError()) in it. In Android API 10 the message is displayed in a lot of lines and it's unreadable. In Android 15 works relatively fine. I attach screenshots to illustrate the problem at the end of the question.
How I can display the error messages in a appropriate mode?
I wrote a little example to reproduce the problem:
The Activity:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
((EditText) findViewById(R.id.b)).setError("A error description and bla bla bla bla bla.");
}
The layout:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >
<EditText
android:id="#+id/a"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1" />
<EditText
android:id="#+id/b"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1" />
<EditText
android:id="#+id/c"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1" />
<EditText
android:id="#+id/d"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1" />
<EditText
android:id="#+id/e"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1" />
<EditText
android:id="#+id/f"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
Device with Android API 10:
Tablet with Android API 15:
Related question. But the answer doesn't work for me.
UPDATE
I executed the same code on two equals simulators except the API level. The results can be seen on the screens. The API 15 still does not fix the error completely. The text is legible but the popup is not in the correct position.
So looking at the source for 2.3.3 the width of the error text is set to slightly less than the width of the TextView it is related to.
They've jimmied around with that for 4.0.3 so that, in your example, the width of the pop-up is correct - but the nature of your layout is such that the pointer is in the wrong place.
I think you've a reasonable example for a bug report against 4.0.3 as I don't think you've got that unusual a use-case.
In order to sort this out though I'd recommend using a TextView that you hide and reveal as necessary. You can set an error image on the edit text as below.
Drawable errorImage = getContext().getResources().getDrawable( R.drawable.your_error_image);
theEditTextInQuestion.setCompoundDrawableWithIntrinsicBounds(errorImage, null, null, null);”
on api 14, TextView use this class;
private static class ErrorPopup extends PopupWindow {
private boolean mAbove = false;
private final TextView mView;
private int mPopupInlineErrorBackgroundId = 0;
private int mPopupInlineErrorAboveBackgroundId = 0;
ErrorPopup(TextView v, int width, int height) {
super(v, width, height);
mView = v;
// Make sure the TextView has a background set as it will be used the first time it is
// shown and positionned. Initialized with below background, which should have
// dimensions identical to the above version for this to work (and is more likely).
mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
com.android.internal.R.styleable.Theme_errorMessageBackground);
mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
}
void fixDirection(boolean above) {
mAbove = above;
if (above) {
mPopupInlineErrorAboveBackgroundId =
getResourceId(mPopupInlineErrorAboveBackgroundId,
com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
} else {
mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
com.android.internal.R.styleable.Theme_errorMessageBackground);
}
mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
mPopupInlineErrorBackgroundId);
}
private int getResourceId(int currentId, int index) {
if (currentId == 0) {
TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
R.styleable.Theme);
currentId = styledAttributes.getResourceId(index, 0);
styledAttributes.recycle();
}
return currentId;
}
#Override
public void update(int x, int y, int w, int h, boolean force) {
super.update(x, y, w, h, force);
boolean above = isAboveAnchor();
if (above != mAbove) {
fixDirection(above);
}
}
}
And call like this;
final float scale = getResources().getDisplayMetrics().density;
mPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
mPopup.setFocusable(false);
So Android TextView use your phone density. I tried for change density but i couldn't work because it need root. If you can this, maybe its work. But i guess it is not possible.
I think you should set the width of the edit text as fill parents so that it will take the proper place, otherwise give the width like 200 or 250 dp so that in that particular width your error message will be shown to you.
does those two emulator have the same screen sizes?.. if so, i think it would be appropriate to set the layout_widths and the heights to wrap content for the popup and if the error still persist then define specifically the width and height of the popup
I think a problem might be that the LinearLayout in your XML has no weightSum attribute. I do not know if you can use layout_weight in child views if LinearLayout does not have a weightSum declared. Also, try changing your layout_widths to "wrap_content" and get rid of the layout_weights if that doesnt work.

Adding a simple ScrollView to Gallery causes a memory leak

I've run into what I can only categorize as a memory leak for ScrollView elements when using the Gallery component.
A short background. I've got an existing app that is a photo slideshow app.
It uses the Gallery component, but each element in the adapter is displayed in full-screen.
(full source is available at this link)
The adapter View element consist of an ImageView, and two TextViews for title and description.
As the photos are of a quite high-resolution, the app uses quite a lot of memory but the Gallery has in general manage to recycle them well.
However, when I am now implementing a ScrollView for the description TextView, I almost immediately run into memory problems. This the only change I made
<ScrollView
android:id="#+id/description_scroller"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:fillViewport="true">
<TextView
android:id="#+id/slideshow_description"
android:textSize="#dimen/description_font_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#color/white"
android:layout_below="#id/slideshow_title"
android:singleLine="false"
android:maxLines="4"/>
</ScrollView>
I did a heap dump and could clearly see that it was the Scrollview which was the root of the memory problems.
Here are two screenshots from the heap dump analysis. Note that the ScrollView retains a reference to mParent which includes the large photo I use
PS same problem occurs if I use the TextView's scrolling (android:scrollbars = "vertical" and .setMovementMethod(new ScrollingMovementMethod());
PSS Tried switching off persistent drawing cache, but no different dreaandroid:persistentDrawingCache="none"
Have you tried removing the scroll view whenever it's container view scrolls off the screen? I'm not sure if that works for you but its worth a shot? Alternatively, try calling setScrollContainer(false) on the scroll view when it leaves the screen. That seems to remove the view from the mScrollContainers set.
Also, this question, answered by Dianne Hackborn (android engineer), explicitly states not to use scrollable views inside of a Gallery. Maybe this issue is why?
Just add this -> android:isScrollContainer="false"
<ScrollView
android:id="#+id/description_scroller"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:fillViewport="true"
android:isScrollContainer="false">
There is some source why this is appear:
http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.1_r1/android/view/View.java
the problem is:
setScrollContainer(boolean isScrollContainer)
by default:
boolean setScrollContainer = false;
but in some cases like this
if (!setScrollContainer && (viewFlagValues&SCROLLBARS_VERTICAL) != 0) {
setScrollContainer(true);
}
it can be true, and when it happends
/**
* Change whether this view is one of the set of scrollable containers in
* its window. This will be used to determine whether the window can
* resize or must pan when a soft input area is open -- scrollable
* containers allow the window to use resize mode since the container
* will appropriately shrink.
*/
public void setScrollContainer(boolean isScrollContainer) {
if (isScrollContainer) {
if (mAttachInfo != null && (mPrivateFlags&SCROLL_CONTAINER_ADDED) == 0) {
mAttachInfo.mScrollContainers.add(this);
mPrivateFlags |= SCROLL_CONTAINER_ADDED;
}
mPrivateFlags |= SCROLL_CONTAINER;
} else {
if ((mPrivateFlags&SCROLL_CONTAINER_ADDED) != 0) {
mAttachInfo.mScrollContainers.remove(this);
}
mPrivateFlags &= ~(SCROLL_CONTAINER|SCROLL_CONTAINER_ADDED);
}
}
mAttachInfo.mScrollContainers.add(this) - all view put into ArrayList this lead to leak of memory sometimes
Yes i noticed the problem, sorry for my previous comment, i've tried to empty the Drawables
by setting previous Drawable.setCallBack(null); but didnt work, btw i have nearly the same project, i use ViewFlipper instead of Gallery, so i can control every thing, and i just use 2 Views in it, and switch between them, and no memory leak, and why not you resize the Image before displaying it, so it will reduce memory usage (search SO for resizing Image before reading it)
Try moving "android:layout_below="#id/slideshow_title" in TextView to ScrollView.
Ended up with implementing a workaround that uses a TextSwitcher that is automatically changed to the remaining substring every x seconds.
Here is the relevant xml definition from the layout
<TextSwitcher
android:id="#+id/slideshow_description"
android:textSize="#dimen/description_font_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="#+id/slideshow_description_anim1"
android:textSize="#dimen/description_font_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:textColor="#color/white"
android:singleLine="false"/>
<TextView
android:id="#+id/slideshow_description_anim2"
android:textSize="#dimen/description_font_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:textColor="#color/white"
android:singleLine="false"/>
</TextSwitcher>
Here I add the transition animation to the TextSwitcher (in the adapter's getView method)
final TextSwitcher slideshowDescription = (TextSwitcher)slideshowView.findViewById(R.id.slideshow_description);
Animation outAnim = AnimationUtils.loadAnimation(context,
R.anim.slide_out_down);
Animation inAnim = AnimationUtils.loadAnimation(context,
R.anim.slide_in_up);
slideshowDescription.setInAnimation(inAnim);
slideshowDescription.setOutAnimation(outAnim);
Here is how I swap to the part of the description
private void updateScrollingDescription(SlideshowPhoto currentSlideshowPhoto, TextSwitcher switcherDescription){
String description = currentSlideshowPhoto.getDescription();
TextView descriptionView = ((TextView)switcherDescription.getCurrentView());
//note currentDescription may contain more text that is shown (but is always a substring
String currentDescription = descriptionView.getText().toString();
if(currentDescription == null || description==null){
return;
}
int indexEndCurrentDescription= descriptionView.getLayout().getLineEnd(1);
//if we are not displaying all characters, let swap to the not displayed substring
if(indexEndCurrentDescription>0 && indexEndCurrentDescription<currentDescription.length()){
String newDescription = currentDescription.substring(indexEndCurrentDescription);
switcherDescription.setText(newDescription);
}else if(indexEndCurrentDescription>=currentDescription.length() && indexEndCurrentDescription<description.length()){
//if we are displaying the last of the text, but the text has multiple sections. Display the first one again
switcherDescription.setText(description);
}else {
//do nothing (ie. leave the text)
}
}
And finally, here is where I setup the Timer which causes it to update every 3.5 seconds
public void setUpScrollingOfDescription(){
final CustomGallery gallery = (CustomGallery) findViewById(R.id.gallery);
//use the same timer. Cancel if running
if(timerDescriptionScrolling!=null){
timerDescriptionScrolling.cancel();
}
timerDescriptionScrolling = new Timer("TextScrolling");
final Activity activity = this;
long msBetweenSwaps=3500;
//schedule this to
timerDescriptionScrolling.scheduleAtFixedRate(
new TimerTask() {
int i=0;
public void run() {
activity.runOnUiThread(new Runnable() {
public void run() {
SlideshowPhoto currentSlideshowPhoto = (SlideshowPhoto)imageAdapter.getItem(gallery.getSelectedItemPosition());
View currentRootView = gallery.getSelectedView();
TextSwitcher switcherDescription = (TextSwitcher)currentRootView.findViewById(R.id.slideshow_description);
updateScrollingDescription(currentSlideshowPhoto,switcherDescription);
//this is the max times we will swap (to make sure we don't create an infinite timer by mistake
if(i>30){
timerDescriptionScrolling.cancel();
}
i++;
}
});
}
}, msBetweenSwaps, msBetweenSwaps);
}
Finally I can put this problem to a rest :)

Categories

Resources