自定义的签到打卡头像重叠排列ViewGroup

先看看几种展示效果图:

效果展示.png

之前在实现一个打卡功能的时候,设计图上要求展示打卡用户的头像,只是这些头像不是正常的从左至右排排坐,如示例图上的3、4排的效果,而是1所展示的效果。如果只是像3或者4这样展示,我们可以用RecyclerView轻松实现。


思路

分析下1这种效果的特殊之处,从左至右都是左边的图片压在右边图片的上方,看起来它是从右往左layout的,最容易想到的实现方法有两种:

  • 自定义ViewGroup,重写layout方法,本文就是采用这种方式;
  • 利用RecyclerView的自定义LayoutManager来实现,大致思路是继承RecyclerView.LayoutManager,重写onLayoutChildren来实现,有兴趣的同学可以自己动手试试。
    下面结合代码看看自定义ViewGroup的实现方式。

具体实现

我们的主要工作是measure、layout、setData,首先考虑下我们需要自定义哪些属性便于拓展

<declare-styleable name="CoverView">
        <attr name="coverWidth" format="dimension" />
        <attr name="itemDia" format="dimension" />
        <attr name="display_style">
            <enum name="left_to_right" value="0" />
            <enum name="right_to_left" value="1" />
        </attr>
    </declare-styleable>

这里我定义了三个,两个图片重叠的宽度(coverWidth)、每张图片的直径(itemDia)、展示风格(display_style)、这里定义了两种:left_to_right从左至右顺序摆放,即2、3、4所对应的样式;right_to_left刚好相反,示例中1的效果。

  • measure过程,因为设计只让在限定区域内显示几个完整的头像,所以我们需要根据测量的宽度计算出能显示的最大图片数maxShowCounts,从而知道最终需要展示的是哪些数据。
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (data == null || data.isEmpty()) return;
        int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingRight() - getPaddingLeft();
        //根据图片的直径,重叠宽度和父控件允许的最大宽度计算出能显示的最大图片数
        int maxShowCounts = (width - coverWidth) / (itemDia - coverWidth);
        showData.clear();
        if (maxShowCounts < data.size()) {
            for (int i = 0; i < maxShowCounts; i++) {
                showData.add(data.get(i));
            }
        } else {
            showData.addAll(data);
        }
        showCount = showData.size();
        int totalWidth = MeasureSpec.getSize(widthMeasureSpec);
        setMeasuredDimension(totalWidth, itemDia + getPaddingTop() + getPaddingBottom());
    }
  • layout过程,最核心的过程,但其实代码没有几行……
@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (showData == null || adapter == null) return;
        if (displayStyle == STYLE_RIGHT_TO_LEFT) {
          //如果是right_to_left的效果我们需要将展示的list翻转一下
            Collections.reverse(showData);
        }
        for (int i = 0; i < showCount; i++) {
            ImageView childView = (ImageView) getChildAt(i);
            adapter.onDisplayImage(getContext(), childView, showData.get(i));
            int left;
            // 画个草图,很容易计算
            if (displayStyle == STYLE_RIGHT_TO_LEFT) {
                left = getPaddingLeft() + (itemDia - coverWidth) * (showCount - i - 1);
            } else {
                left = getPaddingLeft() + (itemDia - coverWidth) * i;
            }
            int right = left + itemDia;
            int top = getPaddingTop();
            int bottom = getPaddingTop() + itemDia;
            //辛辛苦苦只为这一下
            childView.layout(left, top, right, bottom);
        }
    }
  • 为CoverView填充数据,setData其实就是addview()然后requestLayout();
public void setData(List<T> list) {
        if (list == null || list.isEmpty()) {
            setVisibility(View.GONE);
            return;
        } else {
            setVisibility(View.VISIBLE);
        }
        data = list;
        for (int i = 0; i < data.size(); i++) {
            ImageView iv = getImageView(i);
            if (iv == null) {
                return;
            }
            addView(iv, generateDefaultLayoutParams());
        }
        requestLayout();
    }
  • 这里也需要一个adapter来作为数据与View的桥梁,通过adapter来展示图片和获取ImageView对象,当然还可以做很多其他的事情,例如设置监听…
public abstract class CoverAdapter<T> {

    public abstract void onDisplayImage(Context context, ImageView imageView, T t);

    public ImageView generateImageView(Context context) {
        //可以在这里更改Imageview的类型
        CircleImageView circleImageView = new CircleImageView(context);
//        ImageView imageView = new ImageView(context);
        circleImageView.setBorderColor(Color.parseColor("#dcdcdc"));
        circleImageView.setBorderWidth(4);
        return circleImageView;
    }
}

如何使用

贴下我们展示的示例图片代码:

public class MainActivity extends Activity {

    private CoverView coverView;
    private CoverView coverView2;
    private CoverView coverView3;
    private CoverView coverView4;

    private CoverAdapter<String> adapter;

    private List<String> list;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        coverView = (CoverView) findViewById(R.id.cover_view);
        coverView2 = (CoverView) findViewById(R.id.cover_view2);
        coverView3 = (CoverView) findViewById(R.id.cover_view3);
        coverView4 = (CoverView) findViewById(R.id.cover_view4);
        initData();

        adapter = new CoverAdapter<String>() {
            @Override
            public void onDisplayImage(Context context, ImageView imageView, String s) {
                Picasso.with(MainActivity.this).load(s).into(imageView);
            }
        };
        coverView.setAdapter(adapter);
        coverView.setData(list);

        coverView2.setAdapter(adapter);
        coverView2.setData(list);

        coverView3.setAdapter(adapter);
        coverView3.setData(list);

        coverView4.setAdapter(adapter);
        coverView4.setData(list);
    }

    private void initData() {
        list = new ArrayList<>();
        list.add("https://pic4.zhimg.com/02685b7a5f2d8cbf74e1fd1ae61d563b_xll.jpg");
        list.add("https://pic4.zhimg.com/fc04224598878080115ba387846eabc3_xll.jpg");
        list.add("https://pic3.zhimg.com/d1750bd47b514ad62af9497bbe5bb17e_xll.jpg");
        list.add("https://pic4.zhimg.com/da52c865cb6a472c3624a78490d9a3b7_xll.jpg");
        list.add("https://pic3.zhimg.com/0c149770fc2e16f4a89e6fc479272946_xll.jpg");
        list.add("https://pic1.zhimg.com/76903410e4831571e19a10f39717988c_xll.png");
        list.add("https://pic3.zhimg.com/33c6cf59163b3f17ca0c091a5c0d9272_xll.jpg");
        list.add("https://pic4.zhimg.com/52e093cbf96fd0d027136baf9b5cdcb3_xll.png");
        list.add("https://pic3.zhimg.com/f6dc1c1cecd7ba8f4c61c7c31847773e_xll.jpg");
    }
}

xml文件

 <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#DFEFF0">

    <com.hdz.signview.CoverView
        android:id="@+id/cover_view"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_alignParentTop="true"
        android:layout_marginTop="40dp"
        android:background="#FEBFB3"
        android:paddingBottom="10dp"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:paddingTop="10dp"
        app:coverWidth="10dp"
        app:display_style="right_to_left"
        app:itemDia="50dp" />

    <com.hdz.signview.CoverView
        android:id="@+id/cover_view2"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_below="@id/cover_view"
        android:layout_marginTop="20dp"
        android:background="#009378"
        android:paddingBottom="10dp"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:paddingTop="10dp"
        app:coverWidth="10dp"
        app:display_style="left_to_right"
        app:itemDia="50dp" />

    <com.hdz.signview.CoverView
        android:id="@+id/cover_view3"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_below="@id/cover_view2"
        android:layout_marginTop="20dp"
        android:background="#009378"
        android:paddingBottom="10dp"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:paddingTop="10dp"
        app:coverWidth="0dp"
        app:display_style="left_to_right"
        app:itemDia="50dp" />

    <!-- coverWidth设置成负值的时候就分开啦-->
    <com.hdz.signview.CoverView
        android:id="@+id/cover_view4"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_below="@id/cover_view3"
        android:layout_marginTop="20dp"
        android:background="#009378"
        android:paddingBottom="10dp"
        android:paddingLeft="20dp"
        android:paddingRight="20dp"
        android:paddingTop="10dp"
        app:coverWidth="-10dp"
        app:display_style="left_to_right"
        app:itemDia="50dp" />
</RelativeLayout>

…………写个简书比造轮子的时间还要长…………
<p>
完整的demo已上传到我的GitHub ,有需要的可以去下载,有错误请在评论指出,共同学习进步。
<p>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容