Retain view bounds of dynamically added textview on fragment reload - android

There is a Fragment named FragmentA that has a RelativeLayout with an ImageView behind it. (Say 4)Textviews are dynamically added to the rlParentView This layout resides inside a Fragment layout.
The Textviews are draggable inside the parent layout.
Another Fragment is loaded in the same activity and when FragmentA is reloaded, now the dynamically added textviews are lost so how can I retain the dynamically added TextViews with their text and other bounds.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.85"
android:layout_margin="5dp"
android:id="#+id/rlParentView">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/background_image" />
</RelativeLayout>
Dragable TextView are added dynamically like this:
View inflateLayout=mInflater.inflate(R.layout.text_drag_layout,mParentContainer,false);
TextView draggableView= (TextView) inflateLayout.findViewById(R.id.draggableView);
rlParentView.addView(inflateLayout);
draggableView.setText(Some_Text_here);
OnDragTouchListener listener=new OnDragTouchListener(draggableView, rlParentView,
new OnDragTouchListener.OnDragActionListener() {
#Override
public void onDragStart(View view) {
}
#Override
public void onDragEnd(View view) {
}
}
);
draggableView.setOnTouchListener(listener);

I haven't tried it, but here's an idea:
In onSaveInstanceState() of Fragment A, save the positions (and whetever else) of the TextViews inside their parent. Something like:
class ViewPositionInfo implements Serializable {
String text;
int xPositionInParent;
int yPositionInParent;
// ... other variables you need to store
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
List<ViewPositionInfo> positionInfos = new ArrayList<>();
// iterate over the views and add them inside the list
outState.putExtra("positions", positionInfos);
}
Then, when the fragment is re-created, read this information in onCreateView(), create TextViews with those attributes and add them to the parent layout.
The solution above has a drawback. When rotating the screen, the width / height will be different than before, so saving x and y positions of the the text views inside their parent might not be a good idea. You might need to save a relative position, i.e. a percentage. But first things first - make the simple case work (as explained above) and then think about this one.

Related

Add some element in CustomView (which is in fragment)

This sound simple and probably answer is trivial.
I have SomeCustomView class which extending RelativeView. This SomeCustomView have simple addShapeInCustomView():
public class SomeCustomView extends RelativeLayout {
//constructor etc.
public void addShapeInCustomView(){
View testView = new View(ctx);
testView.setLayoutParams(new LayoutParams(50, viewHeight));
ViewHelper.setTranslationX(testView, 20);
testView.setBackgroundColor(Color.GREEN);
addView(testView);
invalidate();
}
}
Of course I added this in my xml layout:
<com.example.test.app.lib.SomeCustomView
android:id="#+id/someCustomView"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="5dp"
android:background="#color/darkblue">
Then in Fragment I calling addShapeInCustomView():
public View onCreateView(...){
//inflate etc.
someCustomView = (SomeCustomView) view.findViewById(R.id.someCustomView);
someCustomView.addShapeInCustomView();
}
SomeCustomView is creating correctly. No errors. But testView from addShapeInCustomView() isn't visible.
What am I doing wrong? What if I want add some elements after initialization (and after onDraw())? I should call addShapeInCustomView() in other moment of Fragment lifecycle (tested and nothing happens)?
If I call addShapeInCustomView() with 200ms of delay it's works perfectly. someView shows up. But obviously it's strange work around.
You should use requestLayout() in place of invalidate(). Invalidating just redraws the current layout, whereas requestLayout() will re-measure and layout the child views, then draw them.

Adding a View makes a ListView within a fragment to refresh

This might be a little bit hard to explain, so the best way I can think of, is providing you a Video showing up the issue.
In the Video I show myself scrolling listview, and after 5 seconds, a View is created and added inside that holder in the bottom. In that moment, listview is refreshed.
http://tinypic.com/player.php?v=vpz0k8%3E&s=8#.U0VrIvl_t8E
The issue is the following:
I've an Activity with a layout that consists of a:
Fragment (above RelativeLayout), match parent, match parent.
RelativeLayout, as wrap content.
The fragment displays a ListView with animations for every row.
If I add a View on the "RelativeLayout", it makes the fragment to readjust to the new size, as it's set above this RelativeLayout, so every Row is rebuilt again.
Do you guys think in any way to avoid this?
EDIT: Sourcecode:
https://bitbucket.org/sergicast/listview-animated-buggy
Don't start the animation if the layout process for the added footer view is running. The end of the layout process can be determined using the ViewTreeObserver (the start obviously starts with adding the footer view):
hand.postDelayed(new Runnable() {
#Override
public void run() {
ViewTreeObserver viewTreeObserver = holder.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#SuppressWarnings("deprecation")
#Override
public void onGlobalLayout() {
holder.getViewTreeObserver().removeGlobalOnLayoutListener(this);
mIgnoreAnimation = false;
}
});
mIgnoreAnimation = true;
holder.addView(viewToAdd);
}
}, 5000);
Add this method to your Activity:
public boolean ignoreAnimation() {
return mIgnoreAnimation;
}
And check it in your Fragment:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
Context context = FragmentTest.this.getActivity();
TextView tv = new TextView(context);
tv.setText("Pos: " + position);
tv.setTextSize(35f);
if (runAnimation()) {
Animation anim = AnimationUtils.loadAnimation(context, R.anim.animation);
tv.startAnimation(anim);
}
return tv;
}
private boolean runAnimation() {
Activity activity = getActivity();
if (activity != null && activity instanceof MainActivity) {
return ! ((MainActivity)activity).ignoreAnimation();
}
return true;
}
Of course the whole Activity - Fragment communication can be improved considerably but the example gives you the idea how to solve the problem in general.
While it prevents the animation from being started, it doesn't prevent the ListView from being refreshed although the user won't notice. If you are concerned about performance you can improve the Adapter code by re-using the views:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
Context context = FragmentTest.this.getActivity();
TextView tv = null;
if (convertView != null && convertView instanceof TextView) {
tv = (TextView) convertView;
}
else {
tv = new TextView(context);
}
Yes, I can think of a possible way to solve this.
Your problem is:
You have set layout params of your holder to wrap_content. By default, when it has no content, it is "zero-sized" somewhere in the bottom and invisible to you (not invisible in terms of Android, though, sic!)
When you add a View to this holder, the framework understands, that the size of your holder container is different now. But this container is a child of another container - your root RelativeLayout, which, in turn, contains another child - your <fragment>.
Thus, framework decides, the root container alongside with its children should get laid out again. That's why your list gets invalidated and redrawn.
To fix the issue with list getting invalidated and redrawn, simply specify some fixed layout parameters to your holder. For example:
<RelativeLayout
android:id="#+id/holder"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true" >
</RelativeLayout>
That will prevent the list from being redrawn. But in that case you'll get your holder displayed from the very beginning.
Yes. This is the expected behavior of RelativeLayout
You are adding the ListView Fragment and TextView into a RelativeLayout, So whenever there is a change in the child view dimension, will affect the other child in the RelativeLayout.
So here when you add a new TexView , the other child Fragment is affected even though its height is match_parent.
You can fix this only by changing the parent layout to LinearLayout.

Why EditText in a custom compound view is re-using the text entered in another compound view instance?

I'm trying to write a custom compound view composed by a TextView and an EditText, _compound_view.xml_:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/compoundText"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:id="#+id/textLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Label" />
<EditText
android:id="#+id/textEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="enter text here" >
</EditText>
and this is the class extending LinearLayout:
public class CompoundView extends LinearLayout {
public CompoundView(Context context, AttributeSet attrs) {
super(context, attrs);
readAttributes(context, attrs);
init(context);
}
public CompoundView(Context context) {
super(context);
init(context);
}
private void init(Context c) {
final LayoutInflater inflater = (LayoutInflater) c
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.compound_view, this);
}
}
Now, if I use 2 of these View in my _activity_main.xml_:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<it.moondroid.compoundview.example.CompoundView
android:id="#+id/compoundview1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true" />
<it.moondroid.compoundview.example.CompoundView
android:id="#+id/compoundview2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/compoundview1" />
</RelativeLayout>
and in the Activity code I only inflate the RelativeLayout, without managing onSaveInstanceState:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
then when I write something in the 2nd EditText and I rotate my device, the same text appears in the EditText of the first custom View.
Why is happening this behaviour?
EDIT:
I solved the issue by removing android:id and using android:tag for the EditText in compound_view.xml, then managing the saving of the EditText state in CompoundView class:
#Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("instanceState", super.onSaveInstanceState());
bundle.putString("currentEdit", mEditText.getText().toString());
bundle.putBoolean("isFocused", mEditText.hasFocus());
return bundle;
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
mEditText.setText(bundle.getString("currentEdit"));
if (bundle.getBoolean("isFocused")) {
mEditText.requestFocus();
}
super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
return;
}
super.onRestoreInstanceState(state);
}
You need to disable SaveEnabled property of EditText using android:saveEnabled="false"
In your custom view, you are inflating layout from XML which has ID defined. Android OS has default functionality to save and restore the state of the view if the view has ID defined.
It means that it will save the text of the EditText when Activity gets paused and restore automatically when Activity gets restored. In your case, you are using this custom view multiple times and that is inflating the same layout so your all EditText have the same ID. Now when Activity will get pause Android will retrieve the value of the EditText and will save against their ID but as you have the same ID for each EditText, values will get override and so it will restore same value in all your EditText.
I'll start off by saying that I haven't confirmed this... but I experienced the same issues when using a compound view, similar to what you were doing.
I think the root of the problem is how Android automatically saves the state of EditText controls, and I think it saves it by "id". So if you have multiple controls in your xml with same "id", then when it saves state, and then restores state, all controls with the same id get the same value. You can try this by adding 2 EditText contols to you normal xml and give them the same android:id value and see if they end up getting the same value.
In your case, you can try to NOT use ids in the compound view and rather find the elements another way, either by tag (View.findViewWithTag), or by name, and see if that makes a difference.
In my case, I solved the problem by doing the latter.
I had the same issue, This is how I made it to work. First need to set false for saveEnabled for editText. We can keep android:id in our layout.
<EditText
android:id="#+id/editText"
android:saveEnabled="false"
Then override below methods in your compound view and manage state by your own. Feel free to ask working example if needed.
#Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("state", super.onSaveInstanceState());
String text = editText.getText().toString();
bundle.putString("text", text);
return bundle;
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
String text = bundle.getString("text");
state = bundle.getParcelable("state");
editText.setText(text);
}
super.onRestoreInstanceState(state);
}
Take a look at my comment in your question and also make sure that you're getting correctly the references to your views.
I'm using your code like this:
CompoundView cv1 = (CompoundView) findViewById(R.id.compoundview1);
TextView tv1 = (TextView) cv1.findViewById(R.id.textEdit);
CompoundView cv2 = (CompoundView) findViewById(R.id.compoundview2);
TextView tv2 = (TextView) cv2.findViewById(R.id.textEdit);
In case you have more complex compound view with many child view, you can consider overriding the dispatchSaveInstanceState of your most outer ViewGroup class and don't call the super implementation.
Like this:
#Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
//If you don't call super.dispatchRestoreInstanceState(container) here,
//no child view gets its state saved
}
I use this, because in my case I have hierarchy of many different compound views that have common super class in which I did this override. (I kept forgetting to set the SaveEnabled attribute to false for new layouts.) However there are many caveats, for example you need to manually save focused view and then request its focus, so your app doesn't behave oddly when screen is rotated.
EDIT:
If you actually need to save state of your compound view, overriding dispatchSaveInstanceState with an empty body will cause onSave/RestoreInstanceState not being called. If you want to use them and still not save state of any of the child views, you need to call super.dispatchFreezeSelfOnly(container) in your override.
More on this here: http://charlesharley.com/2012/programming/views-saving-instance-state-in-android
The issue is happening because of the id field on compound_view.xml
From your code, I just noticed that you inflated compound_view layout file in CompoundView class.
As soon as you create compound_view.xml and put android:id="#+id/textLabel" and android:id="#+id/textEdit" id in your layout xml file, android studio automatically create those ids into int values in R class for single time.
So, when you put CompoundView twice time in your activity_main.xml layout file, you just creating two instance of CompoundView but, both instances textEdit and textLabel have only 1 address location for each one. So, they are pointing to same address locations which are declared in R class.
That's why, whenever you change textEdit or textLabel text programatically, they also change other textEdit or textLabel which are presented in both of your CompoundView
I would like to emphasize a great article, which opened my eyes. It is based on reimplementing onSaveInstanceState() and onRestoreInstanceState(state: Parcelable?).
The advantage of this is that you can use the same compound view multiple times in the same layout (no duplicate ids problem).
In case someone has troubles with incorrect focus being restored after screen rotation, which occurs due to the shared ids of inner views, you can control which id is saved as focused view by overriding findFocus method like this:
override fun findFocus(): View {
if (focusedChild != null) {
return this
}
return super.findFocus()
}
Then the focus gets restored to your compound view, however you should handle the requestFocus call, so the proper child view gets focus upon restoration.
I got the same annoying problem and solved it with another solution than the ones suggested. When the custom view is inflated, I don't find them with IDs or tags, I get them using the getChildAt method. I did not have to save the instance state.
Here is an example:
Custom XML to inflate (custom_view.xml):
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:textAppearance="?android:attr/textAppearanceMedium"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"/>
</merge>
You then just get the views this way during the view initialization:
private void initView(Context context) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.custom_view, this, true);
setOrientation(LinearLayout.HORIZONTAL);
setGravity(Gravity.CENTER);
TextView mTitleTV = (TextView) getChildAt(0);
EditText mContentET = (EditText) getChildAt(1);
}
IMPORTANT: you must remove all IDs from the XML

Programmatically add view into a LinearLayout

I am trying to simulate a ListView in a LinearLayout in order to show three rows with identical layout. This single View is composed by a ratingbar and a content. It is very strange, but all ratingBars receive the last assigned value. First of all this is my custom Component that extends LinearLayout just adding these two methods:
public void setElements(List<Item> elements) {
removeAllViews();
for (int i = 0; i < elements.size() && i < 3; i++) {
View vi = buildElementView(elements.get(i));
vi.setId(i);
addView(vi);
}
}
private View buildElementView(Item itemElement) {
View view = inflater.inflate(R.layout.element_list_item, null, true);
// set values
View header = view.findViewById(R.id.header);
RatingBar ratingInItem = (RatingBar) header.findViewById(R.id.ratingBarInItem);
TextView content = (TextView) view.findViewById(R.id.content);
content.setText(itemElement.getContent());
ratingInItem.setRating(itemElement.getRating());
return view;
}
and this is the layout I am inflating:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.weorder.client"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#color/transparent"
android:minHeight="60.0dp"
android:orientation="vertical"
android:paddingBottom="8.0dp"
android:paddingLeft="5.0dp"
android:paddingRight="5.0dp"
android:paddingTop="8.0dp" >
<RelativeLayout
android:id="#+id/header"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<RatingBar
android:id="#+id/ratingBarInItem"
style="#style/RatingBarSm"
android:isIndicator="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:numStars="5"
android:stepSize="0.1" />
</RelativeLayout>
<TextView
android:id="#+id/content"
style="#style/Content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:layout_marginRight="3dp"
android:layout_marginTop="3dp"
android:textColor="#color/dark_brown"
android:textSize="#dimen/text_size_small" />
</LinearLayout>
I call the setElements method inside onActivityCreated() in the fragment. It works well when the fragment start, but when I try to rotate the phone, the content of items changes properly but the ratingbar gets the last value (if there are 3 elements with rating 1,2 and 3, all ratingbars have 3 stars). Is it a bug?
EDIT: this is my onSaveInstanceMethod:
#Override
public void onSaveInstanceState(Bundle outState) {
if (elements != null) {
outState.putSerializable("elements", elements);
}
super.onSaveInstanceState(outState);
}
Thanks
I think you are using the findViewById() with the wrong view which is causing issues.
Here is a sample based on what you are doing that works for me.
LinearLayout ll = (LinearLayout)findViewById(R.id.ll);
ViewGroup parentGroup = parentGroup = (ViewGroup)ll;
for (int i = 0; i < 3; i++)
{
LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflater.inflate(R.layout.element_list_item, null, true);
RatingBar rBar = (RatingBar)view.findViewById(R.id.ratingBar1);
rBar.setProgress(i);
parentGroup.addView(view);
}
The results should show three rating bars with 0, 1, and 2 stars depending on how you set up the rating bar.
I attach the new view to the view parent three times. The parent is my linearlayout and the new view is the rating bar (or whatever you choose).
For saving in fragments use
#Override
public void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
getFragmentManager().putFragment(outState, Content.class.getName(), this);
}
then you can get your arguments in the onCreateView and restore your data (if necessary)
Bundle extras = this.getArguments();
if(extras != null)
{
//set arguments here
}
It works well when the fragment start, but when I try to rotate the phone, the content of items changes properly but the ratingbar gets the last value (if there are 3 elements with rating 1,2 and 3, all ratingbars have 3 stars). Is it a bug?
This is not a bug - this is how Android works.
Why is this happening?
By default, the system will save and restore the state of views that have IDs and it uses those IDs to maintain the internal map of state. Because you are using the same layout in the LinearLayout, each child has a RatingBar with the ID ratingBarInItem. When the system saves your view's state, it saves the state of the first bar, then overwrites it with the second bar, then overwrites that with the third bar. Then, when you restore state, it restores every view with the ID ratingBarInItem to what it has in the state map - which will be the last value saved, the value from the 3rd item.
How do you fix this?
You have two options.
Manually save and restore the state of the views yourself.
You show that you are already saving the "elements" that are in the view. Well, you could update onRestoreInstanceState to read those elements back then use them to re-update the child views (i.e., just call setElements again).
Give each rating bar a unique ID.
After you inflate a child view to add to the layout, use generateViewID to create a new, unique ID you can set on each RatingBar.
RatingBar ratingInItem = (RatingBar) header.findViewById(R.id.ratingBarInItem);
ratingInItem.setID(View.generateViewID());
Then you don't have to implement saving or restoring state because, now that each view has a unique ID, the default system behavior will work.
Hope that helps!

Android: pass on a "Clicked" status?

I am using a Linear Layout inside of a dialog, and have some TextViews inside that layout that I would also like to change color based on the "pressed" state of the Layout that is their parent. They have a state-list for what color they should be, but it seems that when the layout is clicked, the Views beneath it are not given that "clicked" state.
How could I make the TextViews change color when their parent layout is clicked?
The easiest way is just to pass the event down into whatever the child views are. You can extend TextView and add a method that you can call from the Layout's onclick handler.
class MyTV extends TextView{
public MyTV(Context c){
//constructor gets context in case you want to make instances from code rather than XML
}
public doSomethingToMe(){
//do stuff to this View from outside
}
}
then in your Activity...
public void layoutClicked(View v){ //call this from your layout click
((MyTV)findViewById(R.id.myTV1)).doSomethingToMe();
}

Categories

Resources