android – ListView with triangular shaped items-ThrowExceptions

Exception or error:

enter image description here

I need to implement a ListView with triangular shaped items as shown in this image. The views that one adds to a ListView generally are rectangular in shape. Even in the documentation, a View is described as “occupies a rectangular area on the screen and is responsible for drawing and event handling”.

How can I add non-rectangular shapes to the ListView and at the same time making sure that the click area is restricted to the shape, in this case a triangle.

Thank you!

How to solve:

My solution would use overlapping Views that are cropped to alternating triangles and only accept touch events within its triangle.

The issue is that the ListView does not really support overlapping item Views, therefore my example just loads all items at once into a ScrollView, which may be bad if you have more than, say, 30 items. Maybe this is doable with a RecyclerView but I haven’t looked into that.

I have chosen to extend the FrameLayout to implement the triangle View logic, so you can use it as the root View of a list item and put anything you want in it:

public class TriangleFrameLayout extends FrameLayout {

    // TODO: constructors

    public enum Align { LEFT, RIGHT };

    private Align alignment = Align.LEFT;

    /**
     * Specify whether it's a left or a right triangle.
     */
    public void setTriangleAlignment(Align alignment) {
        this.alignment = alignment;
    }

    @Override
    public void draw(Canvas canvas) {
        // crop drawing to the triangle shape
        Path mask = new Path();
        Point[] tria = getTriangle();
        mask.moveTo(tria[0].x, tria[0].y);
        mask.lineTo(tria[1].x, tria[1].y);
        mask.lineTo(tria[2].x, tria[2].y);
        mask.close();

        canvas.save();

        canvas.clipPath(mask);
        super.draw(canvas);

        canvas.restore();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // check if touch event is within the triangle shape
        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
            Point touch = new Point((int) event.getX(), (int) event.getY());
            Point[] tria = getTriangle();

            if (!isPointInsideTrigon(touch, tria[0], tria[1], tria[2])) {
                // ignore touch event outside triangle
                return false;
            }
        }

        return super.onTouchEvent(event);
    }

    private boolean isPointInsideTrigon(Point s, Point a, Point b, Point c) {
        // stolen from http://stackoverflow.com/a/9755252
        int as_x = s.x - a.x;
        int as_y = s.y - a.y;
        boolean s_ab = (b.x - a.x) * as_y - (b.y - a.y) * as_x > 0;
        if ((c.x - a.x) * as_y - (c.y - a.y) * as_x > 0 == s_ab)
            return false;
        if ((c.x - b.x) * (s.y - b.y) - (c.y - b.y) * (s.x - b.x) > 0 != s_ab)
            return false;
        return true;
    }

    private Point[] getTriangle() {
        // define the triangle shape of this View
        boolean left = alignment == Align.LEFT;
        Point a = new Point(left ? 0 : getWidth(), -1);
        Point b = new Point(left ? 0 : getWidth(), getHeight() + 1);
        Point c = new Point(left ? getWidth() : 0, getHeight() / 2);
        return new Point[] { a, b, c };
    }

}

An example item XML layout, with the TriangleFrameLayout as root, could look like this:

<?xml version="1.0" encoding="utf-8"?>
<your.package.TriangleFrameLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root_triangle"
    android:layout_width="match_parent"
    android:layout_height="160dp"
    android:layout_marginTop="-80dp"
    android:clickable="true"
    android:foreground="?attr/selectableItemBackground">

    <TextView
        android:id="@+id/item_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:padding="20dp"
        android:textSize="30dp"
        android:textStyle="bold"
        android:textColor="#ffffff" />

</your.package.TriangleFrameLayout>

Here we have a fixed height of 160dp that you can change to whatever you want. The important thing is the negative top margin of half the height, -80dp in this case, that causes the items to overlap and the different triangles to match up.

Now we can inflate multiple such items and add it to a list, i.e. ScrollView. This shows an example layout for our Activity or Framgent:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

    </LinearLayout>

</ScrollView>

And the code to populate the list:

Here I created a dummy Adapter, analog to a ListView, that just enumerates our items from 0 to 15.

    ListAdapter adapter = new BaseAdapter() {
        @Override
        public int getCount() { return 16; }

        @Override
        public Integer getItem(int position) { return position; }

        @Override
        public long getItemId(int position) { return position; }

        @Override
        public View getView(int position, View view, ViewGroup parent) {
            if (view == null) {
                view = getLayoutInflater().inflate(R.layout.item_tria, parent, false);
            }

            // determine whether it's a left or a right triangle
            TriangleFrameLayout.Align align =
                    (position & 1) == 0 ? TriangleFrameLayout.Align.LEFT : TriangleFrameLayout.Align.RIGHT;

            // setup the triangle
            TriangleFrameLayout triangleFrameLayout = (TriangleFrameLayout) view.findViewById(R.id.root_triangle);
            triangleFrameLayout.setTriangleAlignment(align);
            triangleFrameLayout.setBackgroundColor(Color.argb(255, 0, (int) (Math.random() * 256), (int) (Math.random() * 256)));

            // setup the example TextView
            TextView textView = (TextView) view.findViewById(R.id.item_text);
            textView.setText(getItem(position).toString());
            textView.setGravity((position & 1) == 0 ? Gravity.LEFT : Gravity.RIGHT);

            return view;
        }
    };

    // populate the list
    LinearLayout list = (LinearLayout) findViewById(R.id.list);
    for (int i = 0; i < adapter.getCount(); ++i) {
        final int position = i;
        // generate the item View
        View item = adapter.getView(position, null, list);
        list.addView(item);
        item.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                Toast.makeText(v.getContext(), "#" + position, Toast.LENGTH_SHORT).show();
            }
        });
    }

At the end we have a result that looks like this:

enter image description here

###

Two item of list for one row and make text clickable.

  • Design images for each row and two image for one item.
  • For each option make only text of both item clickable.

enter image description here

###

I do not think is is possible to create actual triangle shaped views and add them to list view. And what layout would you use in such a scenario?

One way to do it is using background images to create an illusion. Think of each segment separated by red lines as an item in list view. Therefore, you’ll have to create the background images as required for each item in the list view and set them in the correct order.

enter image description here

Update: This is what i mean by consistent slicing of the background image in my comments below.

enter image description here

###

For those who are now using RecyclerView, an implementation of ItemDecoration can be set on RecyclerView which will:

  1. Offset items : Override getItemOffsets() of decoration. Here one can move items so that they overlap.
  2. Draw shapes : Override onDraw() to draw triangles as background.

Leave a Reply

Your email address will not be published. Required fields are marked *