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!
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:
Two item of list for one row and make text clickable.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With