閱讀308 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Android仿拚多多拚團堆疊頭像

序言

做電商的都知道,作為商品的一種售賣方式,拚團是是提供商品售賣的一種及時有效的方式,而在拚團市場中,拚多多無疑是做的最好的一家。於是,研究拚多多的售賣方式之後,我們的產品也開始了這方麵的開發。本文將要給大家介紹的就是通過自定義的方式實現堆疊頭像,這種效果在直播app中非常常見。下麵是部分效果:
這裏寫圖片描述

通過分析,上麵是一個使用ViewPager實現的一個可以左右無線循環的Galllery,相關實現可以訪問我之前的介紹:PageTransformer使用簡介
下麵是一個列表的方式,可以通過下拉來加載更多的Cell數據,也比較簡單。對於組合頭像的實現也是比較簡單的,其實就是一個簡單的流式布局,在本篇實現上,本文也參考了張鴻洋的FlowLayout,對於流式布局來說,隻要按照某種線性規則依次排列即可。

我相信很多朋友之前一定遇到過這種需求:富文本自動換行,如下所示:
這裏寫圖片描述
要實現這種富文本換行,最重要的就是對onMeasure方法,通常的做法是,測量出子View的寬度,當大於屏幕的寬度的時候就換行(當然,需要考慮文字本來就很長,一行顯示不下的情況)。相關代碼如下:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);


        //AT_MOST
        int width = 0;
        int height = 0;
        int rawWidth = 0;//當前行總寬度
        int rawHeight = 0;// 當前行高

        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if(child.getVisibility() == GONE){
                if(i == count - 1){
                    //最後一個child
                    height += rawHeight;
                    width = Math.max(width, rawWidth);
                }
                continue;
            }

            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            int childWidth = child.getMeasuredWidth()  + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            Log.e("=====", "childWidth 1: " + childWidth);
            if(rawWidth + childWidth > widthSpecSize - getPaddingLeft() - getPaddingRight()){
                //換行
                width = Math.max(width, rawWidth);
                rawWidth = childWidth;
                height += rawHeight;
                rawHeight = childHeight;
            } else {
                rawWidth += childWidth;
                rawHeight = Math.max(rawHeight, childHeight);
            }

            if(i == count - 1){
                width = Math.max(rawWidth, width);
                height += rawHeight;
            }
        }

        setMeasuredDimension(
                widthSpecMode == MeasureSpec.EXACTLY ? widthSpecSize : width + getPaddingLeft() + getPaddingRight(),
                heightSpecMode == MeasureSpec.EXACTLY ? heightSpecSize : height + getPaddingTop() + getPaddingBottom()
        );
    }

實現

說了這麼多,那麼具體怎麼實現的呢?由於時間關係,這裏我就直接貼代碼了。首先自定義ViewGrop,實現後一個頭像會覆蓋一部分到前一個頭像上,為了方便使用者控製堆疊頭像的重疊大小,我們通過自定義屬性來解決。

PileView.java

public class PileView extends ViewGroup {

    protected float vertivalSpace;//垂直間隙
    protected float pileWidth=0;//重疊寬度

    public PileView(Context context) {
        this(context, null, 0);
    }

    public PileView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PileView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttr(context, attrs);
    }

    private void initAttr(Context context, AttributeSet attrs) {
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PileLayout);
        vertivalSpace = ta.getDimension(R.styleable.PileLayout_PileLayout_vertivalSpace, dp2px(4));
        pileWidth = ta.getDimension(R.styleable.PileLayout_PileLayout_pileWidth, dp2px(10));
        ta.recycle();
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        //AT_MOST
        int width = 0;
        int height = 0;
        int rawWidth = 0;//當前行總寬度
        int rawHeight = 0;// 當前行高

        int rowIndex = 0;//當前行位置
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if(child.getVisibility() == GONE){
                if(i == count - 1){
                    //最後一個child
                    height += rawHeight;
                    width = Math.max(width, rawWidth);
                }
                continue;
            }

            //調用measureChildWithMargins 而不是measureChild
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

            int childWidth = child.getMeasuredWidth()  + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if(rawWidth + childWidth  - (rowIndex > 0 ? pileWidth : 0)> widthSpecSize - getPaddingLeft() - getPaddingRight()){
                //換行
                width = Math.max(width, rawWidth);
                rawWidth = childWidth;
                height += rawHeight + vertivalSpace;
                rawHeight = childHeight;
                rowIndex = 0;
            } else {
                rawWidth += childWidth;
                if(rowIndex > 0){
                    rawWidth -= pileWidth;
                }
                rawHeight = Math.max(rawHeight, childHeight);
            }

            if(i == count - 1){
                width = Math.max(rawWidth, width);
                height += rawHeight;
            }

            rowIndex++;
        }

        setMeasuredDimension(
                widthSpecMode == MeasureSpec.EXACTLY ? widthSpecSize : width + getPaddingLeft() + getPaddingRight(),
                heightSpecMode == MeasureSpec.EXACTLY ? heightSpecSize : height + getPaddingTop() + getPaddingBottom()
        );
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int viewWidth = r - l;
        int leftOffset = getPaddingLeft();
        int topOffset = getPaddingTop();
        int rowMaxHeight = 0;
        int rowIndex = 0;//當前行位置
        View childView;
        for( int w = 0, count = getChildCount(); w < count; w++ ){
            childView = getChildAt(w);
            if(childView.getVisibility() == GONE) continue;

            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            // 如果加上當前子View的寬度後超過了ViewGroup的寬度,就換行
            int occupyWidth = lp.leftMargin + childView.getMeasuredWidth() + lp.rightMargin;
            if(leftOffset + occupyWidth + getPaddingRight() > viewWidth){
                leftOffset = getPaddingLeft();  // 回到最左邊
                topOffset += rowMaxHeight + vertivalSpace;  // 換行
                rowMaxHeight = 0;

                rowIndex = 0;
            }

            int left = leftOffset + lp.leftMargin;
            int top = topOffset + lp.topMargin;
            int right = leftOffset+ lp.leftMargin + childView.getMeasuredWidth();
            int bottom =  topOffset + lp.topMargin + childView.getMeasuredHeight();
            childView.layout(left, top, right, bottom);

            // 橫向偏移
            leftOffset += occupyWidth;
            // 試圖更新本行最高View的高度
            int occupyHeight = lp.topMargin + childView.getMeasuredHeight() + lp.bottomMargin;
            if(rowIndex != count - 1){
                leftOffset -= pileWidth;
            }
            rowMaxHeight = Math.max(rowMaxHeight, occupyHeight);
            rowIndex++;
        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

    public float dp2px(float dpValue) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, getResources().getDisplayMetrics());
    }
}

自定義的屬性如下:

<declare-styleable name="PileLayout">
        <attr name="PileLayout_vertivalSpace" format="dimension"/>
        <attr name="PileLayout_pileWidth" format="dimension"/>
    </declare-styleable>

為了方便用戶使用,我們在PileView的基礎上再封裝一下,封裝完成後,隻需要用戶提供數據源即可實現頭像堆疊。例如,下麵的代碼給控件設置數據源即可:

List<String> urls=new ArrayList<>();
        urls.clear();
        urls.add("https://ohe65w0xx.bkt.clouddn.com/u=2263418180,3668836868&fm=206&gp=0.jpg");
        urls.add("https://ohe65w0xx.bkt.clouddn.com/u=3637404049,2821183587&fm=214&gp=0.jpg");
       urls.add("https://ohe65w0xx.bkt.clouddn.com/avert.png");
//設置數據源
itemAvertView.setAvertImages(urls);

要完成上麵的封裝,主要會涉及如下的代碼:
PileAvertView.java

public class PileAvertView extends LinearLayout {

    @BindView(R.id.pile_view)
    PileView pileView;

    private Context context = null;
    public static final int VISIBLE_COUNT = 3;//默認顯示個數

    public PileAvertView(Context context) {
        this(context, null);
        this.context = context;
    }

    public PileAvertView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        initView();
    }

    private void initView() {
        View view = LayoutInflater.from(context).inflate(R.layout.layout_group_pile_avert, this);
        ButterKnife.bind(view);
    }

    public void setAvertImages(List<String> imageList) {
        setAvertImages(imageList,VISIBLE_COUNT);
    }

    //如果imageList>visiableCount,顯示List最上麵的幾個
    public void setAvertImages(List<String> imageList, int visibleCount) {
        List<String> visibleList = null;
        if (imageList.size() > visibleCount) {
            visibleList = imageList.subList(imageList.size() - 1 - visibleCount, imageList.size() - 1);
        }
        pileView.removeAllViews();
        for (int i = 0; i < imageList.size(); i++) {
            CircleImageView image= (CircleImageView) LayoutInflater.from(context).inflate(R.layout.item_group_round_avert, pileView, false);
            CommonImageUtil.loadImage(imageList.get(i), image);
            pileView.addView(image);
        }
    }

}

相關的布局:
layout_group_pile_avert.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:andro
    xmlns:app="https://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal">

    <com.lanshan.shihuicommunity.grouppurchase.view.PileView
        android:
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:PileLayout_pileWidth="10dp"/>
</LinearLayout>

item_group_round_avert.xml

<?xml version="1.0" encoding="utf-8"?>
<com.makeramen.rounded.CircleImageView xmlns:andro
    xmlns:app="https://schemas.android.com/apk/res-auto"
    android:
    android:layout_width="30dp"
    android:layout_height="30dp"
    android:orientation="vertical"
    app:round_borderColor="#ffffff"
    app:round_borderWidth="1dp" />

最後更新:2017-10-19 11:33:24

  上一篇:go  《阿裏巴巴Java開發規約》插件使用詳細指南
  下一篇:go  盒馬創始人侯毅首次解讀:盒馬是什麼