Detect whether TextView in ListView is ellipsized - android

I have a custom Adapter that renders some items in a ListView. I need to show an icon on the ListView's items, if the item's text is ellipsized, and hide it if there's enough room for the text to finish. I have access to the button in getView method of my adapter (where I set the text) but the ellipses are not added immediately upon setting the text.
Is there any way I can do this?
Here's my TextView markup:
<TextView android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:singleLine="true"
android:id="#+id/list_item_description"/>

public int getEllipsisCount (int line):
Returns the number of characters to be ellipsized away, or 0 if no ellipsis is to take place.
So, simply call :
if(textview1.getLayout().getEllipsisCount() > 0) {
// Do anything here..
}
Since the getLayout() cant be called before the layout is set, use ViewTreeObserver to find when the textview is loaded:
ViewTreeObserver vto = textview.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
Layout l = textview.getLayout();
if ( l != null){
int lines = l.getLineCount();
if ( lines > 0)
if ( l.getEllipsisCount(lines-1) > 0)
Log.d(TAG, "Text is ellipsized");
}
}
});
And finally do not forget to remove removeOnGlobalLayoutListener when you need it nomore.

The tricky part is that the view you're working with in getView will sometimes have been laid out, and sometimes not, so you have to handle both cases.
When it hasn't been laid out, you set a view tree observer to check on the ellipsis once it has been. In the case of recycled views, the layout will already be there and you can check for the ellipsis immediately after setting the text.
public View getView(int position, View convertView, ViewGroup parent) {
final ViewHolder vh;
if (convertView == null) {
vh = new ViewHolder();
... // create/inflate view and populate the ViewHolder
}
vh = (ViewHolder) convertView.getTag();
// Set the actual content of the TextView
vh.textView.setText(...);
// Hide the (potentially recycled) expand button until ellipsizing checked
vh.expandBtn.setVisibility(GONE);
Layout layout = vh.textView.getLayout();
if (layout != null) {
// The TextView has already been laid out
// We can check whether it's ellipsized immediately
if (layout.getEllipsisCount(layout.getLineCount()-1) > 0) {
// Text is ellipsized in re-used view, show 'Expand' button
vh.expandBtn.setVisibility(VISIBLE);
}
} else {
// The TextView hasn't been laid out, so we need to set an observer
// The observer fires once layout's done, when we can check the ellipsizing
ViewTreeObserver vto = vh.textView.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
Layout layout = vh.textView.getLayout();
if (layout.getEllipsisCount(layout.getLineCount()-1) > 0) {
// Text is ellipsized in newly created view, show 'Expand' button
vh.expandBtn.setVisibility(VISIBLE);
}
// Remove the now unnecessary observer
// It wouldn't fire again for reused views anyways
ViewTreeObserver obs = vh.textView.getViewTreeObserver();
obs.removeGlobalOnLayoutListener(this);
}
});
return convertView;
}

I hope I understand your question correctly--if you're looking to end a TextView that's too wide for a screen with ellipses, you can add these attributes to your TextView:
android:ellipsize="end"
android:maxLines="1"
android:scrollHorizontally="true"
If, however, you want to determine whether a TextView is ended with an ellipsis or is displayed fully, I'm not so sure that's possible--it doesn't look like it is. Still, you might want to try the getEllipsize() method of TextView. I'm not sure whether that returns the point at where the TextView is ellipsized by Android, or where you have set the TextView to be ellipsized.

You can either set your text to marque.
add this in your code might help.....
android:ellipsize="marquee"
android:focusable="true"
android:focusableInTouchMode="true"
android:scrollHorizontally="true"
android:freezesText="true"
android:marqueeRepeatLimit="marquee_forever"
You can also put a horizontal scrollview for you code....
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="#+id/list_item_description"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
</HorizontalScrollView>

Related

Parse RelativeLayout, Set Content, And Add It To A LinearLayout

Ok... here's my situation.
I have a carousel of images in a HorizontalScrollView - which contains a LinearLayout - in my Activity, like so:
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/slider"
android:scrollbars="none" >
<LinearLayout
android:id="#+id/carousel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
/>
</HorizontalScrollView>
I have a TypedArray, loop through it, and on each run, set these images programatically, add a ClickListener and a Tag, and add this ImageView to the LinearLayout (set in my Activity Layout), like so:
// Get the array
final TypedArray carouselArray = getResources().obtainTypedArray(R.array.carousel_array);
// Populate the Carousel with item
for (int i = 0 ; i < carouselArray.length() ; ++i) {
// Image Item
ImageView outerImage;
// Set the image view resource
if(i == 0) {
outerImage.setImageResource(R.drawable.toy_filter_clear);
}
else {
outerImage.setImageResource(carouselArray.getResourceId(i, -1));
}
// Set Touch Listener
outerImage.setOnTouchListener(this);
final String prepend = "CAROUSEL_";
final String index = String.valueOf(i);
final String tag = prepend.concat(index);
outerImage.setTag(tag);
/// Add image view to the Carousel container
mCarouselContainer.addView(outerImage);
}
But now, I just found out that I have to programatically add a second image to sit inside/on top of the first image at particular coordinates (damn you UI ppl!). I need these to be considered the same image/view essentially, so need to pack them together inside of a layout, I am assuming. So I have made a layout file, like so:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/carousel_item"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:id="#+id/carousel_outer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:src="#drawable/toy_filter_normal"
/>
<ImageView
android:id="#+id/carousel_inner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="#+id/carousel_outer"
android:layout_alignRight="#+id/carousel_outer"
android:layout_marginBottom="10dp"
android:layout_marginRight="5dp"
android:src="#drawable/thumb_nofilter"
/>
</RelativeLayout>
This has the proper positioning, and the default images set on it. So what I want to be able to do is to reach into the Layout file, grab the ImageViews by their ID, overwrite the image if necessary, and then add that RelativeLayout to my LinearLayout at the end of my loop... sounds easy enough, right ?
My first attempt was to do it like this :
RelativeLayout item = (RelativeLayout) findViewById(R.id.carousel_item);
ImageView outerImage = (ImageView) item.findViewById(R.id.carousel_outer);
ImageView innerImage = (ImageView) item.findViewById(R.id.carousel_inner);
... but that gives me a NullPointer on the ImageView...So then I tried to inflate the RelativeLayout first, like this:
LayoutInflater inflater = (LayoutInflater) this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.carousel_item_layout, null);
ImageView outerImage = (ImageView) view.findViewById(R.id.carousel_outer);
ImageView innerImage = (ImageView) view.findViewById(R.id.carousel_inner);
This gets rid of the NPE's, and (apparently) let's the images be set properly like so:
if(i == 0) {
outerImage.setImageResource(R.drawable.toy_filter_clear);
innerImage.setImageResource(0);
}
else {
outerImage.setImageResource(R.drawable.toy_filter_normal);
innerImage.setImageResource(carouselArray.getResourceId(i, -1));
}
but when I try to add the outerImage ImageView back to the LinearLayout, I get an NPE there:
mCarouselContainer.addView(outerImage);
More to the point, I don't want to add ONLY the one ImageView to the LinearLayout/HorizontalScrollView - I want to somehow pack the resulting images back into the RelativeLayout and add the whole thing back into my LinearLayout... but, it is worth mentioning, that this also gives me an NPE.
What is a guy to do ? Any thoughts appreciated...
Ok... Wow, thanks SO Code Monkey!
I managed to fix this with a one line fix, by adding the inflated View to the LinearLayout instead of the ImageView or the RelativeLayout (which wasn't doing anything), like so:
mCarouselContainer.addView(view);
Don't know why I hadn't tried that before, but I was unclear on whether as it's children were being updated if it would reflect the parent, so to speak... now I know it was.
I'm gonna keep the question up, as I think it's helpful... ?

Android Layout make all children's not clickable

I am using Relative Layout and many buttons in it with TextViews etc.I want to make all of them not clickable unless some event happens.
I tried setting RelativeLayout.setClickable(false); but still all the elements inside the layout are clickable.
I know one way of doing it that setting each child element not clickable but it is not an appropriate way because i have lot of child elements like buttons text views etc inside a layout i cannot make each child not clickable.
Here my question is How to set all to setClickable(false); in layout ??
I found an alternative way to achieve this. You may create a blocking LinearLayout on top all its sibling views in the RelativeLayout like below:
<RelativeLayout
android:id="#+id/rl_parent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- the children views begins -->
...
<!-- the children views ends -->
<LinearLayout
android:id="#+id/ll_mask"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/transparent"
android:clickable="true"
android:orientation="horizontal"
android:visibility="visible"/>
</RelativeLayout>
Later you may toggle the LinearLayout's visibility to GONE to allow the children views to be clickable again:
mMask.setVisibility(View.VISIBLE); // blocks children
mMask.setVisibility(View.GONE); // children clickable
When you say click do you actually mean touch? Touch events are done element by element. They're first sent to the top level, which says if it handled it and if it didn't it goes onto each children of the view.
When a touch event is created, the onTouch method of view in the chain is called, if any of these return true (true meaning "I handled this!") it stops going down to the children.
To make your relativelayout block touches and clicks for all of its children you simply need to set the onTouchListener, like this:
YOUR_RELATIVE_LAYOUT.setOnTouchListener(new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
// ignore all touch events
return true;
}
});
This will ignore all touch events happening on the relative layout (and all of its children) which includes simple touch down then release events (called clicks).
You can use following function to find all the child view and cancel click.
public void setClickable(View view) {
if (view != null) {
view.setClickable(false);
if (view instanceof ViewGroup) {
ViewGroup vg = ((ViewGroup) view);
for (int i = 0; i < vg.getChildCount(); i++) {
setClickable(vg.getChildAt(i));
}
}
}
}
A very simple and full-proof way to do it is to create a sub class and override onInterceptTouchEvent:
public class MyRelativeLayout extends RelativeLayout {
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// true if you do not want the children to be clickable.
return mShouldInterceptAllTouch;
}
}
No need to call any of the children's methods.
You can still call setOnClickListener on your myRelativeLayout object. Also, you can use the class in XMLs as if you were using a RelativeLayout
An easy Kotlin extension solution to disable/enable a view and all of it's children:
fun View.isUserInteractionEnabled(enabled: Boolean) {
isEnabled = enabled
if (this is ViewGroup && this.childCount > 0) {
this.children.forEach {
it.isUserInteractionEnabled(enabled)
}
}
}
and call it with:
view.isUserInteractionEnabled(false)
If you want the children of a ViewGroup to be unresponsive to touch events, but you want the ViewGroup itself to respond to clicks, for example, you can create your own ViewGroup subclass, override onInterceptTouchEvent(), and always return true. This will intercept all touch events before children see them, while allowing your custom ViewGroup to remain responsive to touch events.
So, instead of RelativeLayout, you could use your own subclass:
public class ControlFreakRelativeLayout extends RelativeLayout {
private boolean mWithholdTouchEventsFromChildren;
// Constructors omitted for sake of brevity
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mWithholdTouchEventsFromChildren || super.onInterceptTouchEvent(ev);
}
public void setWithholdTouchEventsFromChildren(boolean withholdTouchEventsFromChildren) {
mWithholdTouchEventsFromChildren = withholdTouchEventsFromChildren;
}
}
Problem : same as original question, but different use case. i want to progressBar widget which in container with transparent background colors. i want to disable all clicks on other items which comes under my transparent background color .
solution : just set your container clickable in my case Relative layout, it can any other layout too, keep your layout clickable true, until you want according to condition in my case till api call completed. after it simply set your parent clickable false.
android xml
<RelativeLayout
android:id="#+id/rl_parent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- the children views begins -->
...
<!-- the children views ends -->
</RelativeLayout>
Java:
rl_parent.setClickable(true); // when to disable all clicks of children view
rl_parent.setClickable(false); // when to enable all clicks of children view
Since you are going to perform the click for some items in the layout when that particular function is executed,
You can keep a static flag like a boolean
Create a static boolean globally and declare it as false, once the particular function is performed change it to true.
so in all the onclick functions that u r performing check this flag if it is true perform the necessory function.
You can make a common method in which you can write the code for disabling all your views inside your relative layout and call this method in onCreate() and then you can enable your particular view inside the the layout on your event.
If Butterknife library is used, the views can be grouped and the functionality can be done on the group.
Refer http://jakewharton.github.io/butterknife/,
#BindViews({ R.id.first_name, R.id.middle_name, R.id.last_name })
List<EditText> nameViews;
The apply method allows you to act on all the views in a list at once.
ButterKnife.apply(nameViews, DISABLE);
ButterKnife.apply(nameViews, ENABLED, false);
Action and Setter interfaces allow specifying simple behavior.
static final ButterKnife.Action<View> DISABLE = new ButterKnife.Action<View>() {
#Override public void apply(View view, int index) {
view.setEnabled(false);
}
};
static final ButterKnife.Setter<View, Boolean> ENABLED = new ButterKnife.Setter<View, Boolean>() {
#Override public void set(View view, Boolean value, int index) {
view.setEnabled(value);
}
};
For eg., if the textviews are grouped, you could do
static final ButterKnife.Setter<TextView, Boolean> ENABLED = new ButterKnife.Setter<TextView, Boolean>() {
#Override public void set(TextView view, Boolean value, int index) {
view.setClickable(value);
view.setLongClickable(value);
if(value){
view.setTextColor(color);
} else {
view.setTextColor(color);
}
}
};
As an alternative way, you can make clickable ViewGroup with children views via FrameLayout instead of RelativeLayout. Just position your child views in FrameLayout using paddings and gravity, make FrameLayout clickable, and all children views non-clickable:
<FrameLayout
android:layout_width="51dp"
android:layout_height="59dp"
android:layout_gravity="center"
android:clickable="true"
android:focusable="true"
android:onClick="#{() -> activity.onClick()}"
android:padding="5dp">
<android.support.v7.widget.AppCompatImageButton
android:layout_width="wrap_content"
android:layout_height="29dp"
android:layout_gravity="center"
android:clickable="false"
app:srcCompat="#drawable/ic_back" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|left"
android:layout_marginBottom="5dp"
android:layout_marginLeft="5dp"
android:clickable="false"
android:text="#string/back" />
</FrameLayout>
I would recommend creating an extension function something like extensions.kt
import android.view.View
fun View.disable() {
isEnabled = false
isClickable = false
alpha = 0.5F
}
fun View.enable() {
isEnabled = true
isClickable = true
alpha = 1F
}
obviously, you can customize the value as you need
and now you can use it on any View like
somethingView.enable()
imageView.disable()
myButton.enable()
textView.diable()
loop on your buttons and use setClickable function and pass false value for it.
then when your even happen loop again on ot and setClickable to true

Android GridView Custom Adapter only ever displays a single row

I am working on a ListView with items that can expand to show a GridView. The ListView and adapter work fine, and the GridView expands fine when added, but I cannot get it to display more than a single row in the grid. There is only ever a single column displayed, no matter the length I set the column to, and it's clear that the issue isn't that the view is just too small (I have tried making it have an absurdly large height). I've browsed other questions here but this doesn't seem to be a common problem. What am I doing wrong?
xml:
<?xml version="1.0" encoding="utf-8"?>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/scorecardGridView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:columnWidth="25dp"
android:numColumns="18"
android:isScrollContainer="false"
android:verticalSpacing="5dp"
android:horizontalSpacing="5dp"
android:stretchMode="columnWidth"
android:gravity="center" />
GridAdapter getView() code:
public View getView(int position, View convertView, ViewGroup parent) {
Log.d("position",String.valueOf(position));
TextView textView;
if (convertView == null) { // if it's not recycled, initialize some
// attributes
textView = new TextView(context);
textView.setLayoutParams(new GridView.LayoutParams(25, 25));
} else {
textView = (TextView) convertView;
}
if (position < 18)
textView.setText(String.valueOf(position+1));
else {
position -= 18;
textView.setTypeface(null, Typeface.BOLD);
if (scores[position] > 0) {
textView.setTextColor(context.getResources().getColor(
R.color.red));
textView.setText("+" + String.valueOf(scores[position]));
} else {
if (scores[position] < 0)
textView.setTextColor(context.getResources().getColor(
R.color.blue));
textView.setText(String.valueOf(scores[position]));
}
}
return textView;
}
I am using this to show a two row, 18 column golf scorecard. No matter what I do, only the first row shows. If I change the numColumns attribute, that new number of columns shows, but always just one row. You can see I tried Logging the positions for which getView() is being called, and it is revealing that it is only being called for the first row.
Let me know if you want to see any other code, thanks.

View not displaying with visibility change

I have a property animation set for the alpha channels of several views which is not working as I think it should. Essentially I have 3 TextView derivatives, a TextView and two Views which I want to initially be hidden, set their data when it comes in and then animate them in. I do this with several other views in the layout and occasionally they have the same problem although it is very infrequent. Most of the time the TextView and one of the Views simple does not appear even though the data for the TextView is valid according to its Logcat output. It is always the same two views and they do rarely show up but not an any predictable manner I have been able to find. The section of the layout file and all the animation code follows. Please note that as per this question, I have set the visibility to invisible in the layout and set it to visible right before starting the animation.
EDIT
Calling `View.bringToFront() on the views which are having trouble has no visible effect. Also, not executing the animation but instead just changing the visibility of the views from invisible to visible does not show them. Removing the visibility change code and android:visibility="invisible" tag from the layout also does not make the view display.
THE LAYOUT
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:qwerjk="http://schemas.android.com/apk/res/tenkiv.app"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp" >
<tenkiv.view.widget.MagicTextView
android:id="#+id/hourMinuteValueTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toLeftOf="#+id/secondValueTv"
android:lines="1"
android:textSize="77sp"
android:visibility="invisible"
qwerjk:strokeColor="#color/light_text_outline"
qwerjk:strokeJoinStyle="miter"
qwerjk:strokeWidth="1.8" />
<tenkiv.view.widget.MagicTextView
android:id="#+id/secondValueTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="#+id/hourMinuteValueTv"
android:layout_toLeftOf="#+id/amPmValueTv"
android:textSize="39sp"
android:visibility="invisible"
qwerjk:strokeColor="#color/light_text_outline"
qwerjk:strokeJoinStyle="round"
qwerjk:strokeWidth="1.3" />
<tenkiv.view.widget.MagicTextView
android:id="#+id/amPmValueTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="#+id/hourMinuteValueTv"
android:layout_alignParentRight="true"
android:textSize="31sp"
android:visibility="invisible"
qwerjk:strokeColor="#color/light_text_outline"
qwerjk:strokeJoinStyle="round"
qwerjk:strokeWidth="1.3" />
<tenkiv.view.widget.MagicTextView
android:id="#+id/dateValueTv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_below="#+id/hourMinuteValueTv"
android:textSize="19sp"
android:visibility="invisible"
qwerjk:strokeColor="#color/light_text_outline"
qwerjk:strokeJoinStyle="round"
qwerjk:strokeWidth="0.9" />
<View
android:id="#+id/lineTimeConditionSepView"
android:layout_width="290dp"
android:layout_height="3dp"
android:layout_alignParentRight="true"
android:layout_below="#id/dateValueTv"
android:layout_marginTop="4dp"
android:background="#color/holo_text"
android:visibility="invisible" />
<View
android:id="#+id/lineTimeConditionSepViewOutline2"
android:layout_width="290dp"
android:layout_height="1dp"
android:layout_alignBottom="#id/lineTimeConditionSepView"
android:layout_alignLeft="#id/lineTimeConditionSepView"
android:layout_marginTop="0.5dp"
android:background="#color/location_area_dark_green"
android:visibility="invisible" />
---THE REST OF THE LAYOUT---
THE METHODS IN MY FRAGMENT TO ANIMATE IT
public void onUpdateClock(final String time, final String seconds, final String ampm, final String date) {
Activity activity = getActivity(); //Used to make sure we dont update the clock with the activity dead
if (activity != null) {
if (mHourMinuteValue == null) { //These are all used together so this should be safe
mHourMinuteValue = (MagicTextView) activity.findViewById(R.id.hourMinuteValueTv);
mSecondValue = (MagicTextView) activity.findViewById(R.id.secondValueTv);
mAmPmValue = (MagicTextView) activity.findViewById(R.id.amPmValueTv);
mDateValue = (TextView) activity.findViewById(R.id.dateValueTv);
mTimeConditionsLine = activity.findViewById(R.id.lineTimeConditionSepView);
mTimeConditionsOutline2 = activity.findViewById(R.id.lineTimeConditionSepViewOutline2);
mTimeInAnim = createAnimation(CLOCK_IN);
}
}
if (mHourMinuteValue == null) {
Log.e(TAG, "Clock text views were null!");
return;
}
uiHandler.post(new Runnable() {
public void run() {
mHourMinuteValue.setText(time);
mSecondValue.setText(seconds);
if (ampm != null) {
mAmPmValue.setText(ampm);
} else {
mAmPmValue.setText(ClockUICallbacks.EMPTY_AMPM);
}
mDateValue.setText(date);
mDateValue.bringToFront();
if (mFadeInClock) {
setClockVisibility(true);
mTimeInAnim.start();
mFadeInClock = false;
}
}
});
private void setClockVisibility(boolean visible) {
int flag;
if (visible) {
flag = View.VISIBLE;
} else {
flag = View.INVISIBLE;
}
mHourMinuteValue.setVisibility(flag);
mSecondValue.setVisibility(flag);
mAmPmValue.setVisibility(flag);
mDateValue.setVisibility(flag);
mTimeConditionsLine.setVisibility(flag);
mTimeConditionsOutline2.setVisibility(flag);
}
private AnimatorSet createAnimation(int type) {
AnimatorSet set = new AnimatorSet();
switch (type) {
case CLOCK_IN:
ValueAnimator hourMinInAnim = ObjectAnimator.ofFloat(mHourMinuteValue, "alpha", INVISIBLE, OPAQUE);
hourMinInAnim.setDuration(FADE_IN_DURATION);
ValueAnimator secondInAnim = ObjectAnimator.ofFloat(mSecondValue, "alpha", INVISIBLE, OPAQUE);
secondInAnim.setDuration(FADE_IN_DURATION);
ValueAnimator amPmInAnim = ObjectAnimator.ofFloat(mAmPmValue, "alpha", INVISIBLE, OPAQUE);
amPmInAnim.setDuration(FADE_IN_DURATION);
ValueAnimator dateInAnim = ObjectAnimator.ofFloat(mDateValue, "alpha", INVISIBLE, OPAQUE);
dateInAnim.setDuration(FADE_IN_DURATION);
ValueAnimator timeLine1InAnim = ObjectAnimator.ofFloat(mTimeConditionsLine, "alpha", INVISIBLE, OPAQUE);
timeLine1InAnim.setDuration(FADE_IN_DURATION);
ValueAnimator timeLine2InAnim = ObjectAnimator.ofFloat(mTimeConditionsOutline2, "alpha", INVISIBLE, OPAQUE);
timeLine2InAnim.setDuration(FADE_IN_DURATION);
set.playTogether(hourMinInAnim, secondInAnim, amPmInAnim, dateInAnim, timeLine1InAnim, timeLine2InAnim);
return set;
/*SOME OTHER ANIMATIONS*/
}
return null;
}
Any other information which might be needed please let me know.
Thanks, Jared
It turns out that this was caused by a mistake in creating the layouts for two different fragments in the activity. If you look in the layout there is a MagicTextView with id dateValueTv. Well this same id existed in another layout within the activity and since both were using the activity findViewById() method to get the references sometimes they found the right view and sometimes not. I discovered this because my text view in the other fragment had the data that this problem text view should have had. All I had to do was change their id's. I am aware that I could do it based on getView().findViewById(). If this is the best practice I would love to hear comments on it but by checking if the activity is null from getActivity() and getting the views from there I have a convenient way of making sure my background thread doesn't post messages to the ui handler if the activity doesn't exist (such as during rotation).

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