1

I'm writing a calendar application for Android. The calendar needs to a have a day display similar to the default application, or MS outlook: a grid showing a line for each hour, and the appointments shown as rectangles.

Here's a similar sample image from Google Images:

enter image description here

I downloaded the source code for the calendar app from Google's Android Open Source Project, and saw that they implemented this display as a custom view which simplay uses Canvas.drawRect() to draw the rectangles, and then they implemented their own hit-test when the user clicks, to see which appointment was clicked.

I already wrote some of that stuff on my own and it works great, and isn't too complicated.

The problem is that I need the different lines of text inside the rectangles (the subject, the time) to be links to various functionality, and I'm wondering how I can do that.

When I draw, I already create Rects for each appointment. I was thinking I could create Rects for each piece of text as well, cache all of these in an ArrayList, and then perform the histtest against the cached rects. I'm only afraid this whole thing will be too heavy... does this sound like a solid design?

Or should I avoid the custom drawing altogether and programmatically generate and place views (TextViews maybe?) I'm an Android novice and I'm not sure what my options are...

Thanks for helping out!

user884248
  • 2,134
  • 3
  • 32
  • 57
  • If you make a custom layout but a custom view you can just measure and layout whatever children you want. you'd just have to use a interface / timeslot or similar to generate parameters. You also would not need to handle clicks yourself. – David Medenjak Nov 25 '15 at 09:16
  • I'm sorry, I couldn't follow your comment... create a custom view for each rectangle? – user884248 Nov 25 '15 at 09:21
  • I may give you an exampe in 10 hours :p Create a custom layout, fill it with views however you see fit. – David Medenjak Nov 25 '15 at 09:25
  • An example would be awesome :-) I'm sorry, but again, I'm a newbie, and I'm not sure I understand... custom layout - great. Then just fill it with standard Android views? Like TextView etc? That would be lighter than custom drawing? – user884248 Nov 25 '15 at 09:34
  • It would be a whole lot easier, since those views you add handle their drawing themselves ;) – David Medenjak Nov 25 '15 at 09:53

1 Answers1

2

Alright, as announced, here some example:

If you just use a custom view, you have to keep lists of objects and draw them yourself, as opposed to a custom layout where you just have to measure and layout the children. Since you can just add a button, there's no need to use hit-tests or whatsoever, since if you don't mess up the view will just receive the onClick() call.

Also, you can easily preview your layout in the editor if you correctly implement layout parameters. Which makes development much faster.

E.g. you can define your own layout parameters

<resources>
    <declare-styleable name="TimeLineLayout_Layout">
        <attr name="time_from" format="string"/>
        <attr name="time_to" format="string"/>
    </declare-styleable>
</resources>

Then use them like this...

<com.github.bleeding182.timelinelayout.TimeLineLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#22662222">

    <TextView
        android:layout_width="80dp"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_green_dark"
        android:padding="8dp"
        android:text="12:00 - 16:00"
        app:time_from="12:00"
        app:time_to="16:00"/>

</com.github.bleeding182.timelinelayout.TimeLineLayout>

And the result would look something like this (I know it's ugly, but I made this just for testing :/ )

Calendar View

To do this, you create a basic layout where you measure and layout the views. You can then add any views to your layout, and by setting a time from / to and correctly measuring / layouting you can easily display all sorts of items.

The code for the screenshot is attached below, onDraw will create those ugly hour/half hour lines. onMeasure is for calculating view heights and onLayout is drawing the views to their correct time slot.

I hope this helps, it's sure easier to use than handling everything in one view.

public class TimeLineLayout extends ViewGroup {

    private int tIntervalSpan = 24 * 60;
    private float mMeasuredMinuteHeight;

    public TimeLineLayout(Context context) {
        super(context);
    }

    public TimeLineLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TimeLineLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public TimeLineLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        for (int i = 0; i < getChildCount(); i++) {
            final View child = getChildAt(i);
            ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
            if (layoutParams instanceof LayoutParams) {
                LayoutParams params = (LayoutParams) layoutParams;
                final int top = (int) (params.tFrom * mMeasuredMinuteHeight);
                child.layout(l, top, child.getMeasuredWidth(), top + child.getMeasuredHeight());
            }
        }
    }

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

        mMeasuredMinuteHeight = getMeasuredHeight() / (float) tIntervalSpan;

        for (int i = 0; i < getChildCount(); i++) {
            final View child = getChildAt(i);
            ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
            if (layoutParams instanceof LayoutParams) {
                LayoutParams params = (LayoutParams) layoutParams;
                child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec((int) ((params.tTo - params.tFrom) * mMeasuredMinuteHeight), MeasureSpec.EXACTLY));
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        final float height = mMeasuredMinuteHeight * 60;
        Paint paint = new Paint();
        paint.setColor(Color.RED);
        for(int i = 0; i < 24; i++) {
            paint.setStrokeWidth(2f);
            paint.setAlpha(255);
            canvas.drawLine(0, i * height, getMeasuredWidth(), i*height, paint);
            if(i < 23) {
                paint.setStrokeWidth(1f);
                paint.setAlpha(50);
                canvas.drawLine(0, i * height + 30 * mMeasuredMinuteHeight, getMeasuredWidth(), i * height + 30 * mMeasuredMinuteHeight, paint);
            }
        }
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    public static class LayoutParams extends ViewGroup.LayoutParams {

        private final int tFrom;
        private final int tTo;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.TimeLineLayout_Layout);
            final String from = a.getString(R.styleable.TimeLineLayout_Layout_time_from);
            final String to = a.getString(R.styleable.TimeLineLayout_Layout_time_to);
            a.recycle();
            tFrom = Integer.parseInt(from.split(":")[0]) * 60 + Integer.parseInt(from.split(":")[1]);
            tTo = Integer.parseInt(to.split(":")[0]) * 60 + Integer.parseInt(to.split(":")[1]);
        }
  }
David Medenjak
  • 33,993
  • 14
  • 106
  • 134
  • @Hector thanks :) I tried making this for myself some weeks ago...so.. thought I'd share the infos :D – David Medenjak Nov 25 '15 at 18:11
  • Thanks for the detailed explanation. I didn't know the ViewGroup until now. I'll definitely check this out. I can create a custom view for each appointment (the square itself, including text for subject, location, time, etc) and return these from the viewgroup, right? – user884248 Nov 26 '15 at 10:29
  • @user884248 Yes. Just go ahead inflate your views and add them to the viewgroup. Be sure to set the correct layout parameters, so that it get's drawn into the correct slot. – David Medenjak Nov 26 '15 at 10:50
  • @bleeding182 - are you sure about this example? because I finally got around to implementing this, and it seems like ViewGroup doesn't ever call onDraw. The Android online example doesn't use onDraw either: http://developer.android.com/reference/android/view/ViewGroup.html – user884248 Dec 16 '15 at 14:53
  • @user884248 I just used the layout preview mode for testing, and I don't really care about the background on the viewgroup, since it is more of a debug ouput. It will most likely be this flag `setWillNotDraw(false)` that will enable viewgroup drawing, see here http://developer.android.com/reference/android/view/View.html#setWillNotDraw(boolean) – David Medenjak Dec 16 '15 at 15:19