Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ListView with triangular shaped items

Tags:

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!

like image 499
Narayan Acharya Avatar asked Jan 02 '16 11:01

Narayan Acharya


2 Answers

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

like image 73
Floern Avatar answered Sep 24 '22 11:09

Floern


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

like image 37
Umer Avatar answered Sep 22 '22 11:09

Umer