Search code examples
androidiosandroid-custom-viewandroid-viewgroup

Create Android subviews like iOS subviews


I'm trying to programmatically (not using XML files) create custom subviews in Android (that's what I call it in iOS) that is a basically a number of basic views (labels, buttons, text fields etc) put together into a reusable subview class so I can use it inside my UIViewControllers or Activity in Android.

I don't know what is the correct terminology in Android. There seems to be a million different terminologies.

Custom View, ViewGroups, Layouts, Widgets, Components, whatever you want to call it.

In iOS this is simply done like this:

CustomView.h

@interface CustomView : UIView

@property (nonatomic, strong) UILabel *message;
@property (nonatomic, strong) UIButton *button;

@end

CustomView.m

@implementation CustomView

-(id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];

    if(self)
    {
        [self initViews];
        [self initConstraints];
    }

    return self;
}

-(void)initViews
{
    self.message = [[UILabel alloc] init];
    self.button = [[UIButton alloc] init];

    [self addSubview:self.message];
    [self addSubview:self.button];
}

-(void)initConstraints
{
    id views = @{
                 @"message": self.message,
                 @"button": self.button
                 };

    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[message]|" options:0 metrics:nil views:views]];
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[button]|" options:0 metrics:nil views:views]];
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[message][button]|" options:0 metrics:nil views:views]];
}

@end

Now I can reuse this custom view in any ViewController (Android Activity) I chose.

How does one achieve something like that in Android?

I've been looking around and from what I gather in Android, to add subviews, I add them to Layouts:

RelativeLayout relativeLayout = new RelativeLayout(...);
TextView textView = new TextView(...);

relativeLayout.addSubview(textView);

Does that mean I need extend RelativeLayout or ViewGroup?

Looking at this page: http://developer.android.com/reference/android/view/ViewGroup.html

It seems like we need to write some really complicated logic to layout the custom view such as:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();

        // These keep track of the space we are using on the left and right for
        // views positioned there; we need member variables so we can also use
        // these for layout later.
        mLeftWidth = 0;
        mRightWidth = 0;

        // Measurement will ultimately be computing these values.
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        // Iterate through all children, measuring them and computing our dimensions
        // from their size.
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                // Measure the child.
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);

                // Update our size information based on the layout params.  Children
                // that asked to be positioned on the left or right go in those gutters.
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (lp.position == LayoutParams.POSITION_LEFT) {
                    mLeftWidth += Math.max(maxWidth,
                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                } else if (lp.position == LayoutParams.POSITION_RIGHT) {
                    mRightWidth += Math.max(maxWidth,
                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                } else {
                    maxWidth = Math.max(maxWidth,
                            child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                }
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
            }
        }

        // Total width is the maximum width of all inner children plus the gutters.
        maxWidth += mLeftWidth + mRightWidth;

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Report our final dimensions.
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
    }

All I'm trying to do is use multiple basic android labels, views, buttons in a custom view like the iOS example above, why is it so hard in Android ?

I was hoping for something simple like this:

public class CustomView extends View
{
    public RelativeLayout mainLayout;

    public TextView message;
    public Button button;

    // default constructor
    public CustomView()
    {
        ...

        initViews();
        initLayouts();
        addViews();
    }

    public initViews()
    {
        mainLayout = new RelativeLayout(this);

        message = new TextView(this);

        button = new Button(this);

        ...
    }

    public initLayouts()
    {
        // --------------------------------------------------
        // use Android layout params to position subviews 
        // within this custom view class
        // --------------------------------------------------
    }

    public addViews()
    {
        mainLayout.addView(message);
        mainLayout.addView(button);

        setContentView(mainLayout);  
    }
}

I'm sorry I am sincerely trying to learn and build a basic Android application and not trying to bash Android's way of doing things.

I know how to add and layout subviews inside an Activity and have been doing so for the past two days but not inside a custom View/View Group/Layout. I don't want to end up constructing the exact same subview for each of my Activity in the Android app, that just goes against good coding practice right ? :D

Just need a bit of guidance here from others who have done both iOS and Android development.

Edit

It seems like what I'm looking for is called a Compound Control: http://developer.android.com/guide/topics/ui/custom-components.html

I'll keep digging and hopefully achieve the result I'm after :D

Just need to work out this Inflater business.


Solution

  • OK, I think I got it, not sure if it's the best solution but it does what I want.

    So it goes something like this:

    public class CustomView extends RelativeLayout
    {
        private Context context;
    
        public TextView message;
        public Button button;
    
        public CustomView(Context context)
        {
            super(context);
    
            // ---------------------------------------------------------
            // store context as I like to create the views inside 
            // initViews() method rather than in the constructor
            // ---------------------------------------------------------
            this.context = context;
    
            initViews();
            initLayouts();
            addViews();
        }
    
        public CustomView(Context context, AttributeSet attrs)
        {
            super(context, attrs);
    
            // ---------------------------------------------------------
            // store context as I like to create the views inside 
            // initViews() method rather than in the constructor
            // ---------------------------------------------------------
            this.context = context;
    
            initViews();
            initLayouts();
            addViews();
        }
    
        public initViews()
        {
            // ----------------------------------------
            // note "context" refers to this.context
            // that we stored above.
            // ----------------------------------------
            message = new TextView(context);
            ...
    
            button = new Button(context);
            ...
    
        }
    
        public initLayouts()
        {
            // --------------------------------------------------
            // use Android layout params to position subviews 
            // within this custom view class
            // --------------------------------------------------
    
            message.setId(View.generateViewId());
            button.setId(View.generateViewId());
    
            RelativeLayout.LayoutParams messageLayoutParams = new RelativeLayout.LayoutParams(
                    LayoutParams.MATCH_PARENT,
                    LayoutParams.MATCH_PARENT
            );
    
            message.setLayoutParams(messageLayoutParams);
    
    
            RelativeLayout.LayoutParams buttonLayoutParams = new RelativeLayout.LayoutParams(
                    LayoutParams.MATCH_PARENT,
                    LayoutParams.WRAP_CONTENT
            );
    
            button.setLayoutParams(buttonLayoutParams);
        }
    
        public addViews()
        {
            // adding subviews to layout
            addView(message);
            addView(button); 
        }
    }
    

    Now I can use this custom view in any of my Activity:

    public class MainActivity extends ActionBarActivity {
    
        // custom view instance
        protected CustomView approvalView;
    
        @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                ...
                initViews();
            }
    
            public initViews()
            {
                ...
    
                approvalView = new CustomView(this);
                approvalView.message.setText("1 + 1 = 2");
                approvalView.button.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        Log.d("Logger", "Math formula approved! :D");
                    }
                });
    
            }
    }
    

    Inflater is used if we create our layout using XML which isn't something I like to do, so I generated my view's layout programmatically :D

    The above "RelativeLayout" in "extends RelativeLayout" can be replace with "LinearLayout" or other layouts of course.