android自定义9宫格图片视图

类似微信朋友圈中的图片展示大家肯定很熟悉了,这篇文章讲述的自定义View就是类似这个展示方式的View了。先看效果图:


Screenshot_20160715-020937.png

展示规则:

1,如果只有1张图片,则图片宽度占父控件总宽度的2/3(图片高度和宽度相同)
2,如果超出1张图片(不为4张的情况),则按照每行3列的方式排列图片
3,如果正好有4张图片,则用2*2的方式排列,如图:


Screenshot_20160715-015813.png

图片之间的间隙可以在布局文件中进行调整

实现思路:

实现自定义控件的方式有很多,比如:

  1. 继承View(比较适合不包含子控件的场景)
  2. 继承ViewGroup(适合包含有子控件的场景)
  3. 继承特定的View的子类,比如Button等(适合在已有的控件上扩展功能的场景)

很显然,咱们要实现的这个是有自己的子控件的,最多情况有9个ImageView子控件,所以我们需要继承ViewGroup。
继承ViewGroup需要我们自己测量控件的大小以及控制子控件的位置。

假设有LGNineGrideView继承了ViewGroup,那么我们面临的问题有:

  1. 如果确定LGNineGrideView的大小(宽,高)
  2. 如果确定LGNineGrideView的子控件的大小和位置
  3. 如果处理LGNineGrideView在类似ListView这样的列表视图中的复用问题

实际前两个问题还是比较简单的:

未命名.jpg

确定父控件大小

如上图:假设父控件LGNineGrideView是红色框标示的,其子控件用每个小的黑色框标示(每个黑色框的宽高相同,绘图有点偏差请见谅)。
pl:即paddingleft, pr:paddingright, pt:paddingtop, pb:padingbottom, sp:space(子控件之间间隙)
那么:
LGNineGrideView的高度为:rows * grideHeight + (rows - 1) * sp + pt + pb。rows:表示子控件的行数
LGNineGrideView的宽度需要在布局文件中定义

子控件的位置和大小如何确定:

  1. 确定大小
    很显然子控件的宽度需要根据父控件(LGNineGrideView)来确定,假定其值为:totalWidth,则子控件的宽度需要根据列数colums来计算如下:
    availableWidth:totalWidth - pl - pr
    grideWidth = (availableWidth - sp * (colums - 1)) / 3;
    注意特殊情况子控件只有1个的时候:
    grideWidth = availableWidth * 2 / 3
    grideHeight = grideWidth
  2. 确定位置核心代码如下:
for (int i = 0; i < size; ++i) {
    x = i % colums * (grideWidth + sp) + pl;
    y = i / colums * (grideHeight + sp) + pt;
    r = x + tmpWidth;
    b = y + tmpHeight;
    view.layout(x, y, r, b);//布局子控件
}

size:子控件的个数,理解代码并不难,只要把每个子控件标上0~size的编号如上图,再找到控件是落在3*3的矩阵中的行号和列号。举例:假设编号为7的控件,再矩阵中的行号为:i / colums ==> 7 / 3 = 2,列号为:i % colums ==> 7 % 3 = 1,也就是说其落在矩阵的2行1列的位置,再计算坐标就不难了。

子控件的复用问题

处理这个问题时,我也是在网上找了些参考的,但是大部分都是删除再重新创建来实现,有些更加粗暴直接删除全部子控件,再根据数据重新创建新的控件。
举个例子:假设现在A有6个子控件,如果其在类似listview的视图中难免会出现对A的复用,假设现在只需要A显示2个子控件,为了不出现脏区现象,直接将之前6个全部删除,再重新创建2个添加到A里也是可行的。但我个人认为如果用户滑动的很频繁,那么会出现频繁的删除和添加操作,内存抖动会比较频繁影响了性能同时还会出现视图闪动。
解决方案:如果A需要显示的控件大于已有的控件则创建并添加子控件,否则将多余的控件隐藏,核心代码:

int childSize = getChildCount();

for (int i = 0; i < size; ++i) {
    ImageView view = (ImageView) getChildAt(i);
    if (view == null) {
        view = new ImageView();
    }
    view.setVisibility(VISIBLE);
    ...
}

if (size < childSize) {
    for (int i = size; i < childSize; ++i) {
        ImageView view = (ImageView) getChildAt(i);
        view.setVisibility(GONE);
    }
}

以上讲述了实现的总体核心思路,最后总结代码如下:
LGNineGrideView.java

//测量控件的大小及子控件的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (urls == null) {
        setVisibility(GONE);
        return;
    }
    int size = urls.size();
    int sugMinWidth = getSuggestedMinimumWidth();
    int minWidth = getPaddingLeft() + getPaddingRight() + sugMinWidth;
    int totalWidth = resolveSizeAndState(minWidth, widthMeasureSpec, 0);
    int availableWidth = totalWidth - getPaddingLeft() - getPaddingRight();
    if (size == 1) {
        grideWidth = availableWidth * 2 / 3;
        grideHeight = grideWidth;
    } else {
        grideWidth = (availableWidth - space * (colums - 1)) / 3;
        grideHeight = grideWidth;
    }
    int height = rows * grideHeight + (rows - 1) * space + getPaddingTop() + getPaddingBottom();
    setMeasuredDimension(totalWidth, height);
}
//处理子控件的位置和显示逻辑
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int size = urls.size();
    ...
    int childSize = getChildCount();
    for (int i = 0; i < size; ++i) {
        final String url = urls.get(i);
        ImageView view = (ImageView) getChildAt(i);
        if (view == null) {
            if (imageCreator == null) {
                imageCreator = DefaultImageCreator.getInstance();
            }
            view = imageCreator.createImageView(context);
            imageCreator.loadImage(context, url, view);
            addView(view);
            ...
        }
        view.setVisibility(VISIBLE);
        l = i % colums * (tmpWidth + space) + getPaddingLeft();
        t = i / colums * (tmpHeight + space) + getPaddingTop();
        r = l + tmpWidth;
        b = t + tmpHeight;
        view.layout(l, t, r, b);
    }

    if (size < childSize) {
        for (int i = size; i < childSize; ++i) {
            ImageView view = (ImageView) getChildAt(i);
            view.setVisibility(GONE);
        }
    }
}
//计算行数和列数
private void initRowAndColum(int size) {
    rows = (size - 1) / 3 + 1;
    colums = (size - 1) % 3 + 1;
    if (size == 4) {
        rows = 2;
        colums = 2;
        return;
    } else {
        colums = 3;
    }
}
//this.urls:存储ImageView的url地址
public void setImageDatas(List<String> urls) {
    ...
    this.urls.clear();
    this.urls.addAll(urls);
    initRowAndColum(urls.size());
    requestLayout();//数据变化,重新布局
}

代码说明

由于ImageView的创建和加载是因项目而定的,现在的加载框架有UIL,Picaso,Fresco,xUtil等等,所以在onLayout中创建并加载图片的代码如下:

if (imageCreator == null) {
    imageCreator = DefaultImageCreator.getInstance();
}
view = imageCreator.createImageView(context);
imageCreator.loadImage(context, url, view);

通过简单的工厂模式来进行解耦,你可以自己实现ImageCreator来决定如何生成ImageView并使用自己的加载框架加载图片,否则用的是我编写的默认工厂实现的创建和加载(使用了UIL加载框架),整个项目的代码结构如下图(包含了model:ninegrideview以及测试Demo.)

Paste_Image.png

更新说明

  • 可以通过xml设置单张图片的显示模式如下:java <com.gui.ninegrideview.LGNineGrideView android:layout_width="300dp" android:layout_height="wrap_content" android:id="@+id/grideView" app:singleImageRatio="0.7" app:singleImageRatioMode="@integer/SINGLE_IMAGE_MODE_SPECIFIED_RATIO"></com.gui.ninegrideview.LGNineGrideView>

SINGLE_IMAGE_MODE_SPECIFIED_RATIO表示单张图片的高度为按其宽度的比例来决定(height = singleImageRatio * width)SINGLE_IMAGE_MODE_SPECIFIED_RATIO表示单张图片的宽高为原始图片的宽高(等比例显示)* demo添加了PhotoView用来显示大图PhotoView的用法将在后续文章中解析,我将会从源码分析PhotoView的工作原理,请大家继续关注

写在最后

demo开源github地址如下:
LGNineGrideView
1,实现了单独修改子控件个数的demo
2,在listview中使用,并实现加载更多
3,点击图片回调监听
欢迎大家访问并star,如果有任何问题可以在评论中加以提问,谢谢~~

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

推荐阅读更多精彩内容