Android自定义9宫格图片视图

297179121 8年前
   <p>类似微信朋友圈中的图片展示大家肯定很熟悉了,这篇文章讲述的自定义View就是类似这个展示方式的View了。</p>    <p>先看效果图:</p>    <p><img src="https://simg.open-open.com/show/93fff2eb9f235998bccece55b8be4b64.png"></p>    <p> </p>    <h2>展示规则</h2>    <p>1、如果只有1张图片,则图片宽度占父控件总宽度的2/3(图片高度和宽度相同)<br> 2、如果超出1张图片(不为4张的情况),则按照每行3列的方式排列图片<br> 3、如果正好有4张图片,则用2*2的方式排列,如图:</p>    <p><img src="https://simg.open-open.com/show/bd57242e2e9b6afdd90704e1230612e1.png"></p>    <p>图片之间的间隙可以在布局文件中进行调整</p>    <h2>实现思路</h2>    <p>实现自定义控件的方式有很多,比如:</p>    <ol>     <li>继承View(比较适合不包含子控件的场景)</li>     <li>继承ViewGroup(适合包含有子控件的场景)</li>     <li>继承特定的View的子类,比如Button等(适合在已有的控件上扩展功能的场景)</li>    </ol>    <p>很显然,咱们要实现的这个是有自己的子控件的,最多情况有9个ImageView子控件,所以我们需要继承ViewGroup。<br> 继承ViewGroup需要我们自己测量控件的大小以及控制子控件的位置。</p>    <p>假设有LGNineGrideView继承了ViewGroup,那么我们面临的问题有:</p>    <ol>     <li>如果确定LGNineGrideView的大小(宽,高)</li>     <li>如果确定LGNineGrideView的子控件的大小和位置</li>     <li>如果处理LGNineGrideView在类似ListView这样的列表视图中的复用问题</li>    </ol>    <p>实际前两个问题还是比较简单的:</p>    <p><img src="https://simg.open-open.com/show/5dbb0a11f453575e5d0de06d90a7ec15.jpg"></p>    <p>确定父控件大小</p>    <p>如上图:假设父控件LGNineGrideView是红色框标示的,其子控件用每个小的黑色框标示(每个黑色框的宽高相同,绘图有点偏差请见谅)。<br> pl:即paddingleft, pr:paddingright, pt:paddingtop, pb:padingbottom, sp:space(子控件之间间隙)<br> 那么:<br> LGNineGrideView的高度为:rows * grideHeight + (rows - 1) * sp + pt + pb。rows:表示子控件的行数<br> LGNineGrideView的宽度需要在布局文件中定义</p>    <p>子控件的位置和大小如何确定:</p>    <ol>     <li>确定大小<br> 很显然子控件的宽度需要根据父控件(LGNineGrideView)来确定,假定其值为:totalWidth,则子控件的宽度需要根据列数colums来计算如下:<br> availableWidth:totalWidth - pl - pr<br> grideWidth = (availableWidth - sp * (colums - 1)) / 3;<br> 注意特殊情况子控件只有1个的时候:<br> grideWidth = availableWidth * 2 / 3<br> grideHeight = grideWidth</li>     <li>确定位置核心代码如下: <pre>  <code class="language-java">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);//布局子控件  }</code></pre> size:子控件的个数,理解代码并不难,只要把每个子控件标上0~size的编号如上图,再找到控件是落在3*3的矩阵中的行号和列号。举例:假设编号为7的控件,再矩阵中的行号为:i / colums ==> 7 / 3 = 2,列号为:i % colums ==> 7 % 3 = 1,也就是说其落在矩阵的2行1列的位置,再计算坐标就不难了。</li>    </ol>    <p>子控件的复用问题</p>    <p>处理这个问题时,我也是在网上找了些参考的,但是大部分都是删除再重新创建来实现,有些更加粗暴直接删除全部子控件,再根据数据重新创建新的控件。<br> 举个例子:假设现在A有6个子控件,如果其在类似listview的视图中难免会出现对A的复用,假设现在只需要A显示2个子控件,为了不出现脏区现象,直接将之前6个全部删除,再重新创建2个添加到A里也是可行的。但我个人认为如果用户滑动的很频繁,那么会出现频繁的删除和添加操作,内存抖动会比较频繁影响了性能同时还会出现视图闪动。<br> 解决方案:如果A需要显示的控件大于已有的控件则创建并添加子控件,否则将多余的控件隐藏,核心代码:</p>    <pre>  <code class="language-java">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);      }  }</code></pre>    <p>以上讲述了实现的总体核心思路,最后总结代码如下:<br> <code>LGNineGrideView.java</code></p>    <pre>  <code class="language-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();//数据变化,重新布局  }</code></pre>    <h2>代码说明</h2>    <p>由于ImageView的创建和加载是因项目而定的,现在的加载框架有UIL,Picaso,Fresco,xUtil等等,所以在onLayout中创建并加载图片的代码如下:</p>    <pre>  <code class="language-java">if (imageCreator == null) {      imageCreator = DefaultImageCreator.getInstance();  }  view = imageCreator.createImageView(context);  imageCreator.loadImage(context, url, view);</code></pre>    <p>通过简单的工厂模式来进行解耦,你可以自己实现ImageCreator来决定如何生成ImageView并使用自己的加载框架加载图片,否则用的是我编写的默认工厂实现的创建和加载(使用了UIL加载框架),整个项目的代码结构如下图(包含了model:ninegrideview以及测试Demo.)</p>    <p><img src="https://simg.open-open.com/show/a5d2182e5ac4ced8e2b0dde460af2928.png"></p>    <h2>写在最后</h2>    <p>demo开源github地址如下:<br> <strong><a href="/misc/goto?guid=4959675374401157078">LGNineGrideView</a></strong><br> 1,实现了单独修改子控件个数的demo<br> 2,在listview中使用,并实现加载更多<br> 3,点击图片回调监听<br> 欢迎大家访问并star,如果有任何问题可以在评论中加以提问,谢谢~~</p>    <p><br> <a href="/misc/goto?guid=4959675374482206169">阅读原文</a></p>    <p> </p>