Android自定義9宮格圖片視圖

297179121 8年前發布 | 26K 次閱讀 Android Android開發 移動開發

類似微信朋友圈中的圖片展示大家肯定很熟悉了,這篇文章講述的自定義View就是類似這個展示方式的View了。

先看效果圖:

 

展示規則

1、如果只有1張圖片,則圖片寬度占父控件總寬度的2/3(圖片高度和寬度相同)
2、如果超出1張圖片(不為4張的情況),則按照每行3列的方式排列圖片
3、如果正好有4張圖片,則用2*2的方式排列,如圖:

圖片之間的間隙可以在布局文件中進行調整

實現思路

實現自定義控件的方式有很多,比如:

  1. 繼承View(比較適合不包含子控件的場景)
  2. 繼承ViewGroup(適合包含有子控件的場景)
  3. 繼承特定的View的子類,比如Button等(適合在已有的控件上擴展功能的場景)

很顯然,咱們要實現的這個是有自己的子控件的,最多情況有9個ImageView子控件,所以我們需要繼承ViewGroup。
繼承ViewGroup需要我們自己測量控件的大小以及控制子控件的位置。

假設有LGNineGrideView繼承了ViewGroup,那么我們面臨的問題有:

  1. 如果確定LGNineGrideView的大小(寬,高)
  2. 如果確定LGNineGrideView的子控件的大小和位置
  3. 如果處理LGNineGrideView在類似ListView這樣的列表視圖中的復用問題

實際前兩個問題還是比較簡單的:

確定父控件大小

如上圖:假設父控件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.)

寫在最后

demo開源github地址如下:
LGNineGrideView
1,實現了單獨修改子控件個數的demo
2,在listview中使用,并實現加載更多
3,點擊圖片回調監聽
歡迎大家訪問并star,如果有任何問題可以在評論中加以提問,謝謝~~


閱讀原文

 

 本文由用戶 297179121 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!