16

I find myself needing to create a View completely in Java without knowing what concrete type the parent is.

example:

public View getView(int position, View convertView, ViewGroup parent){
    if(null == convertView){
        convertView = new TextView(parent.getContext());
    }
    ((TextView) convertView).setText(getItem(position).getName());
}

Now suppose I wanted to change this so that the convertView was wrap_content in both directions. Since this is an Adapter, I'd like to avoid coupling the Adapter with the concrete type of the parent, but the LayoutParams I give it in setLayoutParams() has to be the correct concrete type otherwise the app will crash (i.e. if parent is a ListView it has to be ListView.LayoutParams, if it's a LinearLayout it must be a LinearLayout.LayoutParams, etc.). I don't want to use a switch statement either since that's just a more flexible form of coupling, and if I attach this adapter to a view I didn't anticipate I still end up with a crash. Is there a generic way to do this?

keyboardr
  • 841
  • 1
  • 8
  • 20

4 Answers4

12

You can do this using the following code:

LayoutParams params = parent.generateLayoutParams(null);

EDIT: The method above doesn't work because ViewGroup.generateLayoutParams() requires android:layout_width and android:layout_height to be set in the passed AttributeSet.

If you use ViewGroup.LayoutParams with any layout then everything will work fine. But if you use LinearLayout.LayoutParams with RelativeLayout for example, then an exception will be thrown.

EDIT: There's one working solution for this problem which I don't really like. The solution is to call generateLayoutParams() with valid AttributeSet. You can create an AttributeSet object using at least two different approaches. One of them I've implemented:

res\layout\params.xml:

<?xml version="1.0" encoding="utf-8"?>

<view xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="20dip" />

SomeActivity.java:

private void addView(ViewGroup viewGroup, View view) {
    viewGroup.addView(view);
    view.setLayoutParams(generateLayoutParams(viewGroup));
}

private ViewGroup.LayoutParams generateLayoutParams(ViewGroup viewGroup) {
    XmlResourceParser parser = getResources().getLayout(R.layout.params);
    try {
        while(parser.nextToken() != XmlPullParser.START_TAG) {
            // Skip everything until the view tag.
        }
        return viewGroup.generateLayoutParams(parser);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

Another way to create an AttributeSet object is to implement AttributeSet interface and make it return android:layout_width, android:layout_height and other layout attributes you need.

Dori
  • 18,283
  • 17
  • 74
  • 116
Michael
  • 53,859
  • 22
  • 133
  • 139
  • This causes a runtime exception: You must supply a layout_width attribute. – keyboardr Aug 10 '11 at 23:11
  • That seems to be true according to the source code. To use this method you have to provide an object that implements `AttributeSet` and can be used to get `layout_width` and `layout_height` values. And I don't know any simple way to create such an object. Are you sure you need to provide concrete `LayoutParams` subclasses? I always use `ViewGroup.LayoutParams` and everything works fine. – Michael Aug 11 '11 at 06:14
  • I've had it crash on me before and troubleshot it down to that. – keyboardr Aug 12 '11 at 03:07
  • I think if you use `ViewGroup.LayoutParams` with any layout then everything will work fine. But if you use `LinearLayout.LayoutParams` with `RelativeLayout` for example, then an exception will be thrown. – Michael Aug 12 '11 at 04:49
  • 1
    I know I'm late... but this answer is wrong. Using Viewgroup.LayoutParams instead of, for instance, TableRow.LayoutParams is not good. I ran into many cases where views simply do not display as a result. Furthermore, ViewGroup.LayoutParams lacks methods you may want to use, such as setMargin. – aleph_null Nov 13 '11 at 16:46
  • 1
    Doesn't work. Set a ViewGroup.LayoutParams on your view and then add it to a RelativeLayout. Result: java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.widget.RelativeLayout$LayoutParams – cdhabecker Aug 31 '12 at 22:44
  • @aleph-null, I've added a working solution to the answer. It's not very elegant, but I hope it will help you. – Michael Sep 02 '12 at 20:18
  • @cdhabecker, take a look at the new solution. Maybe it will be useful for you. – Michael Sep 02 '12 at 20:18
  • @Michael Do you know how long ive tried creating my own AttributeSet object!!! You just made my day, thanks! – samus Oct 10 '12 at 13:31
  • @Michael well, I dont see the AttributeSet example, but your suggestion on how to use it will suffice. – samus Oct 10 '12 at 13:51
  • @SamusArin, maybe I'll add it if I have enough time to do it. But I can't promise. If you have a working sample, you can add it yourself. – Michael Oct 11 '12 at 09:04
2

I have the following workaround for this:

View view = new View(context);
parent.addView(view);

LayoutParams params = view.getLayoutParams();
//Do whatever you need with the parameters
view.setLayoutParams(params);

You cannot autogenerate the correct LayoutParams yourself unless you do something hacky, so you should just create a situation where they will be autogenerated for you: just add the view to the container. After that you can get them from the view and do what you need.

The only caveat is that if you don't need to add the view to the container yourself, you'll have to remove the view from it later, but this shouldn't be a problem.

Malcolm
  • 41,014
  • 11
  • 68
  • 91
  • This doesn't work for `ListAdapters` since the `View` will be added to the parent after `getView()` returns – keyboardr Apr 24 '15 at 22:57
  • 2
    @keyboardr It's not a problem, you can add a view yourself, get the parameters, and remove it. – Malcolm Apr 25 '15 at 00:52
2

All LayoutParams classes have one general superclass: ViewGroup.LayoutParams. And all popular layouts (FrameLayout, LinearLayout, ConstraintLayout, etc.) use ViewGroup.MarginLayoutParams as base class for their respective LayoutParams classes.

So if you just need width, height and margins, you can create ViewGroup.MarginLayoutParams and pass it as layout params to any subclass of ViewGroup. What will happen then is container will automatically convert more generic ViewGroup.MarginLayoutParams into its own layout parameters using protected LayoutParams ViewGroup#generateLayoutParams(ViewGroup.LayoutParams p).

This code will work for any container class, including LinearLayout, RelativeLayout, etc:

val layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
container.addView(view, layoutParams)
Alexey
  • 7,262
  • 4
  • 48
  • 68
1

why no one (yet -> see 2016.05) has mentioned here an approach based on reflections ?

1. entry point:

 /**
 *  generates default layout params for given view group 
 *  with width and height set to WLayoutParams.RAP_CONTENT 
 *
 * @param viewParent - parent of this layout params view 
 * @param <L> - layout param class 
 * @return layout param class object 
 * @throws NoSuchMethodException
 * @throws InvocationTargetException
 * @throws IllegalAccessException
 */
@NonNull
private <L extends ViewGroup.LayoutParams> L generateDefaultLayoutParams(@NonNull ViewGroup viewParent)
        throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Method generateDefaultLayoutParamsMethod = ViewGroup.class.getDeclaredMethod("generateDefaultLayoutParams");
      // caution: below way to obtain method has some flaw  as we need traverse superclasses to obtain method in case we look in object and not a class             
      // = viewParent.getClass().getDeclaredMethod("generateDefaultLayoutParams");
    generateDefaultLayoutParamsMethod.setAccessible(true);
    return (L) generateDefaultLayoutParamsMethod.invoke(viewParent);
}

2. usages:

@NonNull
protected <T extends ViewGroup.LayoutParams> T createLayoutParamsForView(ViewGroup viewParent,
                                                                         @IdRes int belowViewId)
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    return createLayoutParamsForView(null,null,viewParent,belowViewId);
}

@NonNull
protected <T extends ViewGroup.LayoutParams> T createLayoutParamsForView(@NonNull Context context,
                                                                         @NonNull Class<? extends ViewGroup> parentClass,
                                                                         @IdRes int belowViewId)
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    return createLayoutParamsForView(context,parentClass,null,belowViewId);
}

@NonNull
private <T extends ViewGroup.LayoutParams> T createLayoutParamsForView(@Nullable Context context,
                                                                       @Nullable Class<? extends ViewGroup> parentClass,
                                                                       @Nullable ViewGroup parent,
                                                                       @IdRes int belowViewId)
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    if(context == null && parent == null) throw new IllegalStateException("either context and parent class or must be non null!");
    T layoutParams = (T) (parent != null ? generateDefaultLayoutParams(parent) : generateDefaultLayoutParams(context, parentClass));
    if (belowViewId != NO_ID  && RelativeLayout.LayoutParams.class.isAssignableFrom(layoutParams.getClass())){
        ((RelativeLayout.LayoutParams)layoutParams).addRule(RelativeLayout.BELOW, belowViewId);
    }
    return layoutParams;
}

@NonNull
private <P extends ViewGroup> P instantiateParent(Class parentClass, Context context) 
        throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    Constructor constructor = parentClass.getDeclaredConstructor(Context.class);
    constructor.setAccessible(true);
    return (P) constructor.newInstance(context);
}

@NonNull
private <L extends ViewGroup.LayoutParams, P extends ViewGroup> L generateDefaultLayoutParams(Context context, @NonNull Class<? extends ViewGroup> viewParentClass) 
        throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
    P viewParent = instantiateParent(viewParentClass, context);
    return generateDefaultLayoutParams(viewParent);
}
ceph3us
  • 7,326
  • 3
  • 36
  • 43