ListView with triangular shaped items
Asked Answered
E

4

20

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!

Elated answered 2/1, 2016 at 11:47 Comment(3)
This could be done by setting background images in a sequence that creates an illusion of triangles.Aqualung
could you tell me where did you found this layout picture?Magneton
@piotrek1543, A layout that my UI/UX designer gave me.Elated
R
15

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

Ribonuclease answered 2/1, 2016 at 15:37 Comment(3)
Thanks a lot @Floern. The approach looks solid. Only concern being the restriction on number of items. Anyway, will try this out.Elated
I guess this is the proper way we can use the layouts for our needs +1 for u @RibonucleaseStratfordonavon
@Floern. Hey The approach worked. I could get around 40 items working with no lag on my old Nexus 4 with this approach. Will try to mod it for a RecyclerView and check if the item count restriction can be removed. Thanks a lot!Elated
A
6

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

Apthorp answered 2/1, 2016 at 12:12 Comment(2)
Thanks @Umar, but keeping only the text clickable is not an option since I need the entire triangle to be clickable.Elated
@NarayanAcharya I think defining the clickable area for a single row should be possible. Have a look hereLithology
A
2

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

Aqualung answered 2/1, 2016 at 12:14 Comment(11)
In my opinion, that's only one background created by author, but yes it's mix of triangles. I have similar idea to yours ;-)Magneton
Thanks @Virus, this will not allow me to define triangular shaped click areas.Elated
single background may cause issues with alignment of the item bounds and the expected positions on screens in case of auto scaling on devices with different dimensions. Wont it?Aqualung
@NarayanAcharya: you can also create an illusion while clicking by identifying where the list view item was clicked and then reacting to it, May not give a pixel perfect point detection but sure can be managed by defining ranges and reacting to click based on which range was clicked in each list view item.Aqualung
its a one time effort in slicing the background right to keep them consistent in terms of ranges and then writing a area detection method to determine which range was clicked in the list item view.Aqualung
@Virus, I see only fragment of layout which would be done only for tablets. Hard to say who of us have the right, but llok at elevation and shades at lines of triangle. Are you sure you can do it with adding one after one triangles? that's another hard questionMagneton
@Virus, Could you please give an example to elaborate the process. Do you mean to define click areas for each list item? How can I define a click region for each list item?Elated
@Magneton Yes, what Narayan wants is not simple. So getting it to work is also going to be hard. :-) ... There is no right known or off-the-shelf way, hence this question is more like brain-storming on finding the best approach. I like questions like these.Aqualung
@NarayanAcharya it could be done by finding the relative position of click from the (0,0) of the list view item layout. and when we know that we can write an algorithm to know which part of the image it is on. (Assuming that the slicing of background has been done to keep the areas consistent.) Image i attached has random segments in each row item. But those segments need to be consistent to be able to do what i am suggesting. more of geometry and relative position calculation...Aqualung
@Virus, Got what you were saying. Will try this and let you know how it went. Thanks.Elated
also added an example to demonstrate how you could slice consistently with only two segments per list view item. Note: The text will then need to be a part of image and also split on the slices if you want to keep them vertically centered. Haven't added that.Aqualung
C
1

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.
Carvey answered 5/1, 2016 at 6:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.