带有三角形项目的ListView

20

这里输入图像描述

我需要实现一个带有三角形项的ListView,如图所示。一般在ListView中添加的视图都是矩形的。即使在文档中,View也被描述为“占据屏幕上的矩形区域,并负责绘制和事件处理”。

我该如何向ListView中添加非矩形形状,并同时确保点击区域仅限于此形状,即三角形。

谢谢!


1
这可以通过设置背景图像序列来创建三角形的错觉来实现。 - Viral Patel
你能告诉我你在哪里找到这个布局图片的吗? - piotrek1543
@piotrek1543,这是我的UI/UX设计师给我的布局。 - Narayan Acharya
4个回答

15

我的解决方案将使用重叠的视图,这些视图被裁剪成交替的三角形,并且仅在其三角形内接受触摸事件。

问题在于ListView并不真正支持重叠的item视图,因此我的示例将所有项一次性加载到ScrollView中,如果你有超过30个项,这可能会很糟糕。也许RecyclerView可以做到这一点,但我还没有研究过。

我选择扩展FrameLayout来实现三角形视图逻辑,因此您可以将其用作列表项的根视图,并在其中放置任何您想要的内容:

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 https://dev59.com/7XI-5IYBdhLWcg3wBjjk#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 };
    }

}

一个示例项目的XML布局,以TriangleFrameLayout作为根元素,可能看起来像这样:

<?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>

这里我们有一个高度固定为160dp的框架,您可以将其更改为任何您想要的高度。重要的是半高度的负上边距,在这种情况下为-80dp,这会导致项目重叠并使不同的三角形匹配。

现在我们可以填充多个此类项并将其添加到列表中,即ScrollView。 这显示了我们的 Activity 或 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>

而填充列表的代码:

这里我创建了一个虚拟适配器,类似于ListView,只是从0到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();
            }
        });
    }

最终我们得到了如下结果:

输入图像描述


1
非常感谢 @Floern。这个方法看起来很可靠。唯一的顾虑是对项目数量的限制。不管怎样,我会试试看。 - Narayan Acharya
我猜这就是我们可以根据需求使用布局的正确方式,+1 给你 @Floern - Hardy
1
@Floern。嗨,这种方法很有效。我可以使用这种方法在我的旧Nexus 4上运行约40个项目而没有延迟。我将尝试对其进行修改以适用于RecyclerView,并检查是否可以删除项目计数限制。非常感谢! - Narayan Acharya

6
  • 每行展示两个列表项并让文字可点击。
  • 为每一行设计图片,每个列表项有两幅图像。
  • 对于每一个选项,仅使两个列表项的文本可点击。

enter image description here


谢谢@Umar,但仅使文本可点击不是选项,因为我需要整个三角形都可点击。 - Narayan Acharya
1
@NarayanAcharya 我认为定义单行的可点击区域应该是可能的。在这里看看:http://stackoverflow.com/questions/9489977/is-there-a-way-to-create-a-triangular-button-on-android - siddhant

2
我认为不可能创建实际的三角形视图并将它们添加到列表视图中。在这种情况下,您会使用什么布局呢?
一种方法是使用背景图像来创建幻觉。将由红线分隔的每个部分视为列表视图中的一个项目。因此,您将需要为列表视图中的每个项目按需创建背景图像,并将它们设置在正确的顺序中。

enter image description here

更新:这是我在下面评论中所说的背景图像一致切片的意思。

enter image description here


在我看来,这只是作者创造的一个背景,但是确实是由三角形混合而成的。我有类似的想法,哈哈。 - piotrek1543
1
@NarayanAcharya:你也可以通过识别列表视图项的点击位置并对其做出反应来创建一种错觉,这可能不会给出像素完美的点检测,但可以通过定义范围并根据每个列表视图项中所点击的范围来做出反应。 - Viral Patel
1
这是一个一次性的工作,需要对背景进行切割以保持它们在范围上的一致性,然后编写一个区域检测方法来确定在列表项视图中点击了哪个范围。 - Viral Patel
1
@piotrek1543 是的,Narayan想要的不简单。因此,让它正常工作也很难。 :-) ... 没有已知或现成的正确方法,因此这个问题更像是关于找到最佳方法的头脑风暴。我喜欢这样的问题。 - Viral Patel
1
还添加了一个示例,演示如何仅使用每个列表视图项的两个段切片。注意:如果要保持垂直居中,则需要将文本作为图像的一部分并在切片上进行拆分。我还没有添加这个功能。 - Viral Patel
显示剩余6条评论

1

对于现在正在使用RecyclerView的人,可以在RecyclerView上设置ItemDecoration的实现,这将会:

  1. 偏移项目:覆盖装饰的getItemOffsets()。在这里,可以移动项目,使它们重叠。
  2. 绘制形状:覆盖onDraw()以绘制三角形作为背景。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接