Why dynamically create framelayout? - android

I am trying to understand how the Tinder like/dislike card system works by looking through this example on GitHub: https://github.com/kikoso/Swipeable-Cards/blob/master/AndTinder/src/main/java/com/andtinder/view/CardStackAdapter.java. I understand the importance of BaseAdapters and populating the view/card with the necessary info. This part of the code that is confusing the hell out of me is this:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
FrameLayout wrapper = (FrameLayout) convertView;
FrameLayout innerWrapper;
View cardView;
View convertedCardView;
if (wrapper == null) {
wrapper = new FrameLayout(mContext);
wrapper.setBackgroundResource(R.drawable.card_bg);
if (shouldFillCardBackground()) {
innerWrapper = new FrameLayout(mContext);
innerWrapper.setBackgroundColor(mContext.getResources().getColor(R.color.card_bg));
wrapper.addView(innerWrapper);
} else {
innerWrapper = wrapper;
}
cardView = getCardView(position, getCardModel(position), null, parent);
innerWrapper.addView(cardView);
} else {
if (shouldFillCardBackground()) {
innerWrapper = (FrameLayout) wrapper.getChildAt(0);
} else {
innerWrapper = wrapper;
}
cardView = innerWrapper.getChildAt(0);
convertedCardView = getCardView(position, getCardModel(position), cardView, parent);
if (convertedCardView != cardView) {
wrapper.removeView(cardView);
wrapper.addView(convertedCardView);
}
}
return wrapper;
}
Why are FrameLayouts being created dynamically? It seems like multiple FrameLayouts are being made with wrapper and inner wrapper? What does wrapper even mean? Why are classes like shouldFillCardBackground() used but not even defined anywhere in the repository?

First of all, it is important to notice that the CardStackAdapter you link to is abstract, so it will never be instantiated directly. Rather, the class SimpleCardStackAdapter will be used for instance.
That said, lets get the purpose of the method clear; The getView() method's purpose is to create and populate a view for the given position.
Creating layouts can be done either by inflating them from xml, or otherwise, by creating them in code. The latter is happening here. The main reason that they create the layout dynamically, is to keep things dynamic. The exact views in the layout depend on some configurations which are checked at runtime (in the method shouldFillCardBackground()). Note that this could also be achieved by creating the layout in xml, then inflate it, and dynamically hide/show (or remove/add) views and layouts. The auther of the code simply chose to do it in code.
A wrapper is an object (in this case a layout) which does not much more that holding other objects (other layouts). In the context of layouts, they are usually used to add some kind of background or padding. That is also what is happening here.
shouldFillCardBackground() is a method, not a class, and it is definitely defined: in line 71.
It is important to realize that it seems that the main puropose of this code is to be an example, a demo. It is possible it is not fully functional and that not everything is implemented the best way (for instance, shouldFillCardBackground() returns true by default).

Related

How to get a ConstraintSet working inside a programatically generated ConstraintLayout descendant?

I have a view that inherits from ConstraintLayout. Inside this layout I place the children by use of a ConstraintSet.
This works as long as use a given AttributeSet form outside in the constructor:
public AnswerView(Context context, AttributeSet attrs) {
super(context, attrs);
It does not work, if I try to assign the layout otherwise, when I don't have attrs available.
public AnswerView(Context context) {
super(context);
setLayoutParams(new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
In the first case the children get a pretty distribution like defined by the ConstraintSet, in the second case the all line up at the left border.
In both cases the layout stretches full width, as I can prove by setting a background color.
What is missing in my code?
This is not a direct answer to the question. A direct answer is still missing. This answer is of the type, "take a better approach!".
When there are few or no answers to a question, I am usually on a track, that few people go. Then there are reasons why they don't.
My approach to programatically nest views within other views is cool, but it turns out to be difficult to do this without the use of layouts. It's too expensive to set up all the benefits of the configurations programatically, that can easily done within a layout. The API of Android is not well prepared for this.
So I turned back to the approach, to create the view classes based on layouts. This means, I create the view with the two parameter constructor for layouts.
In the enclosing view class I can't create the nested view directly any more, as there is no constructor for this. Instead I read the configured view from a layout.
I created a small class to assist extracting configured subparts of a layout:
public class Cloner {
LayoutInflater inflater;
int layoutId;
public Cloner of(Context context) {
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
return this;
}
public Cloner from(#LayoutRes Integer layoutId) {
this.layoutId = layoutId;
return this;
}
public View clone(#IdRes int id) {
assert inflater != null;
View layout = inflater.inflate(layoutId, null);
View view = layout.findViewById(id);
((ViewManager) view.getParent()).removeView(view);
return view;
}
}
It is used like this:
MultipleChoiceAnswerView mcav =
(MultipleChoiceAnswerView) new Cloner().of(getContext())
.from(layoutId).clone(R.id.multipleChoiceAnswerView);
mcav.plugModel(challenge.getAnswer());
This already shows, how I connect the model in a second step, because I can't feed it into by the constructor any more.
In the constructor I first evaluate the given attributes. Then I set up the view by inflating an accompanying second layout file, that I don't show here. So there are two layouts involved, one to configure the input to the constructor, one for the internal layout.
When the call to plugModel happens, the inflated internal layout is used and extended by objects matching the given model. Again I don't create this objects programatically, but read them from a third (or the second) layout file as templates. Again done with the assistance of the Cloner given above.
private Button getButton(final Integer index, String choice) {
Button button = (Button) new Cloner().of(getContext()).from(layoutId).clone(R.id.button);
button.setId(generateViewId());
button.setText(choice);
button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
answer.choiceByIndex(index);
}
});
return button;
}
In practice I put this object templates (like a button) as children into the second layout file. So I can style it as a whole in Android Studio Designer.
Programatically I remove the templates before I actually fill the layout with the cloned objects. By this approach I only need to maintain one layout file per view class. (The configuration of the constructor happens in the layout file of the enclosing view.)

Can i have two xml layouts use the same viewholder using Kotlin synthetic extensions?

I have a list view that has expandable rows. When i click on the row, i inflate a different xml layout which is very similar (item_view(left) and item_view_expanded(right)) and has almost all the same views, but some sub views appear in different sizes (for example the image view is enlarged) and there is one additional button in the expanded view (Save Photo).
Upon converting this file to kotlin and trying to access the views using the kotlin synthetic extensions, i receive the error Overload Resolution Ambiguity. This makes sense because there are indeed two xml files which have the same ids and are both imported in this file. Most stackoverflow posts i see (such as this one) are resolved by removing one of the import statements or changing the ids. But i want the ids to match because i want to have one shared ViewHolder that can bind both of these different views. After all, the text views and images are all the same content, just in a different visual layout.
I could do this in Java with butterknife because i simply annotate the button that doesn't exist in the collapsed layout (download) as nullable and do some null checks in the binding. I'm wondering how i can have two xml layouts use the same binding since almost everything is the same. Can this be done in kotlin using the synthetic extensions?
Here is the java code for my ViewHolder
public static class ItemViewHolder extends RecyclerView.ViewHolder {
#BindView(R.id.tvName) TextView mNameTextView;
#BindView(R.id.tvRelationship) TextView mRelationshipTextView;
#BindView(R.id.tvLifespan) TextView mLifespanTextView;
#BindView(R.id.topDivider) View mTopDividerView;
#BindView(R.id.bottomDivider) View mBottomDividerView;
#BindView(R.id.imageView) ImageView mImageView;
#Nullable
#BindView(R.id.savePhoto) TextView mSavePhoto;
public ItemViewHolder(final View view) {
super(view);
ButterKnife.bind(this, view);
}
private void bindItem(final int position, final PathPerson pathPerson, final PathDetailAdapter pathDetailAdapter) {
// item
mNameTextView.setTag(position);
PathUtilities.loadRoundedLeftCornersImageForGender(pathPerson, mImageView);
mNameTextView.setText(pathPerson.getFullName());
itemView.setOnClickListener(v -> {
pathPerson.toggleExpanded();
pathDetailAdapter.notifyItemChanged(position);
});
if (mSavePhoto != null) {
mSavePhoto.setOnClickListener(v -> {
// TODO: 9/7/17 download original
Toast.makeText(itemView.getContext(), R.string.downloading, Toast.LENGTH_SHORT).show();
});
}
PathUtilities.setLifespan(pathPerson, mLifespanTextView);
PathUtilities.setRelationshipTextAndColorForGender(pathPerson, mRelationshipTextView);
if (position == 1) {
mTopDividerView.setVisibility(View.VISIBLE);
mBottomDividerView.setVisibility(View.VISIBLE);
} else if (position == pathDetailAdapter.mPathList.size() - 1) {
mTopDividerView.setVisibility(View.GONE);
mBottomDividerView.setVisibility(View.GONE);
} else {
mTopDividerView.setVisibility(View.GONE);
mBottomDividerView.setVisibility(View.VISIBLE);
}
}
}
Turns out the problem is fixed by removing one of the import statements. In my case I needed to keep the one that had the extra download button (the super-set of the two)

Custom views inside ListView with itemViewType

I have a ListView in my application. The adapter for this listview contains multiple item view types (around 5 till now), via which I can inflate different types of row views inside the listview.
All row views inflated inside the adapter are custom subclassed view/view group.
public class CustomView1 extends RelativeLayout {
Bundle bundle;
public CustomView1(Bundle bundle) {
super(context);
this.bundle = bundle;
addSubViews(bundle.getBundleList("list"));
}
private void addSubViews(ArrayList<Bundle> list) {
for(Bundle element : list) {
//add sub views via reflection
View view = (View) Class.forName(packageName + type).getConstructor(Bundle.class).newInstance(element);
addView(view);
}
}
//called from getView() in adapter when convertView != null
public void onRecycle(Bundle bundle) {
if(bundle != this.bundle) {
this.bundle = bundle;
removeAllViews();
addSubViews(bundle.getBundleList("list"));
}
}
}
Bundle passed to each custom view contains layout info for that view. In this way, I can create and add any view/viewgroup inside any viewgroup. All well till now.
Now the problem comes when this code runs inside ListView. Since all the view types are created by the adapter initially, scrolling jerks a lot because the adapter keeps on creating new custom views of different itemViewType. How to reduce those jerks in listview ? Any ideas? In the listview, all viewTypes are different at the top 5 positions, so the adapter has to create these views and that makes the experience sluggish.
Even when the adapter recycles similar view type convertViews after 5th index, I clear the container using removeAllViews() and run this loop again because the subView bundle list of the incoming bundle from 6th position onwards might be different. So in the end, adapter is only recycling empty ViewGroups. Since the subView list can possibly contain anything (maybe one more bundle list inside any element bundle), I have to do removeAllViews() to accommodate new subview tree in the recycled convertView.
I thought of using vertical ScrollView but that would take too much memory upfront, and the number of custom views inflated is dynamic, can increase to 20.
The app is running but the scroll is so bad there is hardly any usability left, so its looking like till now I have achieved nothing by adding so much dynamic behavior also. Please suggest me ways to counter this problem.
I am suspecting that the use of setLayoutParams inside CustomView classes may be stopping the scroll because I set the width/height of all views after they are created.
Update #1 getView() code using ViewHolder pattern
ViewHolder holder;
if(convertView == null) {
holder = new ViewHolder();
holder.customView1 = new CustomView1(bundle);
convertView = holder.customView1;
convertView.setTag(holder);
} else {
holder = (ViewHolder)convertView.getTag();
}
holder.customView1.onRecycle(bundle);
ListView has excellent support for different View types. Just make sure to use view holder pattern to avoid jerky scrolling and then override getViewTypeCount() and getItemViewType().
More detail http://android.amberfog.com/?p=296

Android: Duplicating View element in the ViewGroup

I am working on class that extends ViewGroup to arrange the View items for the GridView.
I can easily add a new View item inside it by:
ImageView view = new ImageView(context);
view.setImageBitmap(BitmapFactory.decodeResource( getResources(), R.drawable.ic_launcher));
addView(view);
Or removing View item is also easy
removeViewAt(remove_index)
Swapping the item can be done by
addView(new_index, removeViewAt(old_index));
but I want to duplicate the View item when one item is dragged over the another one.
I tried to duplicate the the item by
addView(getChildAt(index))
And this shows the exception error
The specified child already has a parent. You must call removeView() on the child's parent first
I also tried to store all the view items in the List, called the method removeAllView() and again added the views in class.
ArrayList<View> children = new ArrayList<View>();
for (int i = 0; i < getChildCount(); i++){
children.add(getChildAt(i));
}
children.add(getChildAt(index)); // duplicate this item
removeAllViews();
for (int i = 0; i < children.size(); i++){
addView(children.get(i));
}
This still shows the exception error as above:
The view inflating may work but I want to copy the same view without going for the external resource.
So I want the method to detach that View from parent ViewGroup and make multiple copy (Duplicate) of it inside the class.
Any help is appreciated.
First, you're trying to add that same object again, which doesn't really make sense - the new view has to be a separate object, you'd have to duplicate the original first, e.g. using .clone() method.
But, unfortunately, even if you did, you couldn't add the cloned view to the ViewGroup, here's why.
The exception you get is the result of ViewGroup checking your View's parent for null
So, in order to add the cloned view, you'd have to set your view's mParent member to null, which you can't do directly because the method that does that is not public: View.assignParent()
You could try to clone the View after you call .removeViewAt() so that it doesn't have a parent at the time of cloning, then add the original view back to it's position and then proceed with adding the clone to the required place, but as S.D. mentioned you'd have to have some hassle with cloning plus this way is very obscure and will require the ViewGroup to relayout 2 times.
A better solution is to assign a tag to each view that contains the necessary info to create another View like that and use it when you need to clone.
I would do something like this:
public interface ViewCloner {
public View clone(Context context);
}
public static class ImageViewCloner implements ViewCloner {
private int mImgResId;
public ImageViewCloner(int imgResourceId) {
this.mImgResId = imgResourceId;
}
#override
public View clone(Context context) {
ImageView view = new ImageView(context);
view.setImageBitmap(BitmapFactory.decodeResource( context.getResources(), mImgResId));
// Add the tag to the clone as well, so it, too, can be cloned
view.setTag(new ImageViewCloner(mImgResId));
return view;
}
}
// When creating the original view
int resId = R.drawable.ic_launcher;
ImageView view = new ImageView(context);
view.setImageBitmap(BitmapFactory.decodeResource( getResources(), resId));
view.setTag(new ImageViewCloner(resId));
// When cloning the view
ViewCloner vc = (ViewCloner) getChildAt(index).getTag();
View clone = vc.clone(getContext());
addView(clone);
For any additional view or group you'll want to use instead of the single ImageView thing just create another implementation of ViewCloner and you're good to go without having to modify your container's behaviour.
Duplicating an object requires a good implementation of clone() method.
I don't think Android's view classes do this well, so you may need to create a custom type of view that can produce a copy of itself. View class does have methods to save/restore state: with onSaveInstanceState () and onRestoreInstanceState() which you can use to copy View's state.
Also, you will need to take care of event listeners registered on that view.
Thanks for the answer S.D and Ivan.
After the long break I could find my own answer keeping those solutions in my mind.
The Clone method directly cannot be added in the View and adding the interface makes more complexity for the codes.
Even my requirement was to clone the view for which the image was dynamically added and source was unknown.
Some trick must be done just to copy the view,
first of all get the another instance of view and copying the properties such as drawable, background, padding, etc on the second one.
The solution was much easier by using following codes.
// Create new Instance of imageView
ImageView view = new ImageView(context);
// Get the original view
ImageView org = (ImageView)getChildAt(index);
// Copy drawable of that image
view.setImageDrawable(org.getDrawable());
// Copy Background of that image
view.setBackground(org.getBackground());
// Copy other required properties
....
// Lastly add that view
addView(view);

Children views are null after inflate

Okay so I do this (not actual code)
try {
final View view = LayoutInflater.from(context).inflate(R.layout.ownComponent, null);
} catch (InflateExpection e) {
}
if (view != null) {
// This child is null about 5/10 times!
View child = view.findViewById(R.id.ownComponentChild);
}
I read that after inflate it is not guaranteed that child views are inflated, so what would be neat way to get callback when all childs are ready?
Maybe I misunderstood what you're trying to do, but it seems like you're inflating a View and not a layout...
Try
View view = LayoutInflater.from(context).inflate(R.layout.LAYOUT_THAT_HOLDS_ownComponent, this, true);
and then view will hold the entire layout, from which you can find the child by Id with
view.findViewById(...);
Edit:
Hard to know if it's related as you didn't post enough code but try this:
Get the View view out of the try/catch and put it as a class member. loose the final and cast the child.
example (assuming ownComponentChild is a FrameLayout):
FrameLayout child = (FrameLayout)view.findViewById(R.id.ownComponentChild);
This seems to happen randomly so my only guess is that memory is getting low in this case, because I have to recreate so many UI components fast to get this reproduced.

Categories

Resources