4

I'm trying to create a custom view, inherit from view group, and layout custom sub-views inside this view group in a customized way. Basically I'm trying to create a calendar view similar to the one in outlook, where each event takes up screen height relative to its length.

I initialize an ArrayList of View in the ViewGroup's constructor, override onMeasure, onLayout and onDraw, and everything works well, except... the rendered views all render starting at (0,0), even though I set their left and right properties to other values. Their width and height come out ok, only their top and left are wrong.

This is the code, which I abbreviated for clarity and simplicity:

public class CalendarDayViewGroup extends ViewGroup {
    private Context mContext;
    private int mScreenWidth = 0;

    private ArrayList<Event> mEvents;
    private ArrayList<View> mEventViews;

    // CalendarGridPainter is a class that draws the background grid. 
    // this one works fine so I didn't write its actual code here.
    // it just takes a Canvas and draws lines on it.
    // I also tried commenting out this class and got the same result,
    // so this is DEFINITELY not the problem.
    private CalendarGridPainter mCalendarGridPainter;

    public CalendarDayViewGroup(Context context, Date date) {
        super(context);
        init(date, context);
    }

    //... other viewGroup constructors go here...

    private void init(Date date, Context context) {
        mContext = context;
        // the following line loads events from a database
        mEvents = AppointmentsRepository.getByDateRange(date, date);

        // inflate all event views
        mEventViews = new ArrayList<>();
        LayoutInflater inflater = LayoutInflater.from(mContext);
        for (int i = 0; i < mEvents.size(); i++) {
            View view = getSingleEventView(mEvents.get(i), inflater);
            mEventViews.add(view);
        }

        // set this flag so that the onDraw event is called
        this.setWillNotDraw(false);
    }

    private View getSingleEventView(Event event, LayoutInflater inflater) {
        View view = inflater.inflate(R.layout.single_event_view, null);
        // [set some properties in the view's sub-views]
        return view;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));

        // get screen width and create a new GridPainter if needed
        int screenWidth = MeasureSpec.getSize(widthMeasureSpec);
        if (mScreenWidth != screenWidth)
        {
            mScreenWidth = screenWidth;
            mCalendarGridPainter = new CalendarGridPainter(screenWidth);
        }

        int numChildren = mEvents.size();
        for (int i = 0; i < numChildren; i++) {
            View child = mEventViews.get(i);
            Event event = mEvents.get(i);

            // event width is the same as screen width
            int specWidth = MeasureSpec.makeMeasureSpec(mScreenWidth, MeasureSpec.EXACTLY);

            // event height is calculated by its length, the calculation was ommited here for simplicity
            int eventHeight = 350; // actual calculation goes here...
            int specHeight = MeasureSpec.makeMeasureSpec(eventHeight, MeasureSpec.EXACTLY);
            child.measure(specWidth, specHeight);
        }
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int numChildren = mEvents.size();
        for (int i = 0; i < numChildren; i++) {
            View child = mEventViews.get(i);
            Event event = mEvents.get(i);

            int eventLeft = 0;
            int eventTop = (i + 1) * 200; // test code, make each event start 200 pixels after the previous one
            int eventWidth = eventLeft + child.getMeasuredWidth();
            int eventHeight = eventTop + child.getMeasuredHeight();
            child.layout(eventLeft, eventTop, eventWidth, eventHeight);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
         // draw background grid
        mCalendarGridPainter.paint(canvas);
        // draw events
        for (View view : mEventViews) {
            view.draw(canvas);
        }
        super.onDraw(canvas);
    }
}
user884248
  • 2,134
  • 3
  • 32
  • 57
  • Have you logged the values that set the width? – Daniel Kobe Dec 19 '15 at 08:22
  • I debugged and tracked all locals. The values are set correctly. Also, the width is not the problem - width and height are set correctly. The top value is the problem: although it is set to 200, 400, 600 etc, all views get rendered at 0. – user884248 Dec 19 '15 at 08:42

1 Answers1

2

For some reason, it seems like the way children are drawn with ViewGroups is that the ViewGroup translates the canvas to child's position then draws the child at 0,0.

But as it turns out, ViewGroup will handle all the drawing of children for you. I think if you simplify your onDraw() method you should be all set:

@Override
protected void onDraw(Canvas canvas) {
     // draw background grid
    mCalendarGridPainter.paint(canvas);
    // draw events
    super.onDraw(canvas);
}

Now that I'm looking at your code further, I noticed you are inflating your child views within the code for your ViewGroup. It would be best to do all that outside your ViewGroup, add those views using addView(), then use getChildCount() and getChildAt() to access the child views during onLayout().

kris larson
  • 30,387
  • 5
  • 62
  • 74
  • I assume I'll have to override getView() and getViewCount() for this to work? – user884248 Dec 19 '15 at 16:22
  • I don't think so. Try the `onDraw()` override I posted, also try not overriding `onDraw()` at all, just to see the default behavior. – kris larson Dec 19 '15 at 16:25
  • Oh wait a second, did you not call `CalendarDayViewGroup.addView()` on your event views? Updating my answer... – kris larson Dec 19 '15 at 16:27
  • That's amazing. It worked! Thank you so much! I commented out the line that draws the views, called addView() after inflating, and it works. So strange. I don't think I saw this in any of the tutorials I read about the subject, including on developer.android.com. Why is it better to inflate the views outside of the ViewGroup? – user884248 Dec 19 '15 at 17:23
  • It's just a little better design because you're decoupling the container view from the child view. Here's an example: Once your calendar day is already displayed, you should be able to add a new event view and see the calendar day update immediately with the new event view. Think of how useful that would be if you were, say, receiving messages from a server somewhere that there are new events available. Tutorials are great, but they tend to just skim the surface. Eventually there comes a point where you have to get under the hood and open the Android source to see what's going on. Cheers – kris larson Dec 19 '15 at 17:39
  • Thanks, this is helping a lot. – user884248 Dec 19 '15 at 17:44