Android自定義佈局

首先我們觀察Android API:

View.java

// 注意final修飾,該方法永遠不會被覆蓋,整個佈局結構 measure方法唯一

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {

         onMeasure(widthMeasureSpec, heightMeasureSpec);

}

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}



//注意final修飾,該方法永遠不會被覆蓋,整個佈局結構layout方法唯一

public final void layout(int l, int t, int r, int b) {

        boolean changed = setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {

                  onLayout(changed, l, t, r, b);

        }

}

protected void onLayout(boolean changed, int left, int top, int right, int bottom) { } 空方法


ViewGroup.java extends View.java 

  @Override

   protected abstract void onLayout(boolean changed, int l, int t, int r, int b);

// 測量該ViewGroup所包含的所有佈局

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {}

protected void measureChild(View child, int parentWidthMeasureSpec,

            int parentHeightMeasureSpec) {}



//我會單講mChildren數組mChildren中的View是如何來的。

public View getChildAt(int index) {  return mChildren[index];  }

public int getChildCount() {  return mChildrenCount; }
RelativeLayout.java extends ViewGroup.java

//當繼承RelativeLayout佈局時,我們應當覆蓋該方法,以實現測量該佈局包含的View,//此處的實現並不能測量所有的View

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}



protected void onLayout(boolean changed, int l, int t, int r, int b) {}

private void measureChild(View child, LayoutParams params, int myWidth, int myHeight) {}

//還包含一個重要的內部類,代表RelativeLayout所包含的每一個view大小及位置信息

public static class LayoutParams extends ViewGroup.MarginLayoutParams{

          private int mLeft, mTop, mRight, mBottom;

}

下面我要自定義一個佈局,定義佈局的目的肯定是爲了向其內部放置View

CustomGridLayout.java extends RelativeLayout.java

初學者會問,我們到底需要繼承RelativeLayout類的哪個方法呢!!

拋去一切,我們自己想象,佈局控件需要

第一:控件(View)的大小 

第二:控件(View)的位置  

第三:知道要放置多少個View

通過熟讀文檔,我們應該知道:

onMeasure方法負責測量將要放在CustomGridLayout內部的View的大小。

onLayout方法負責分配尺寸及位置給將要放在CustomGridLayout內部的View。

所以很明顯,需要我們繼承的方法是

  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}

功能:測量該佈局所包含的所有View的大小(會在框架層循環取得每一個View,然後測量其大小),該方法會被View.java中的measure方法調用。而measure方法會被Window對象的DecorView調用

  1. protected void onLayout(boolean changed, int l, int t, int r, int b) {}

功能:在相應的位置放置相應的View,該方法會被View.java中的layout方法調用,而layout方法會被誰調用呢?

(1) 調用requestLayout()方法. 該方法內部會執行Object.layout(…)

(2) 直接調用 Object.layout(…)

(3) 調用addView(View child, …)時,

調用addView(…)之前一般需要先調用android.view.View.setLayoutParams(LayoutParams params)
這裏寫圖片描述

也許有人會問當調用addView時,會和框架層的layout,onLayout,measure, onMeasure等幾個佈局方法有什麼關係,或者說後者幾個方法是怎麼被觸發的,彆着急,看見addView(…)的底層實現了嗎?

public void addView(View child, int index, LayoutParams params) {

        // addViewInner() will call child.requestLayout() when setting the new LayoutParams

        // therefore, we call requestLayout() on ourselves before, so that the child's 

        // request will be blocked at our level

        requestLayout(); 

        invalidate();

        addViewInner(child, index, params, false);

   }

很明顯,addView在底層調用了requestLayout方法,該方法如時序圖所示,會依次觸發我們的onMeasure,onLayout方法。

示例1-子控件橫向滾動的容器控件

public class LandspaceLayout extends ComplexLayout {
    private Rect mMaxChildSpace = new Rect();
    private Rect mLastChildSpace = new Rect();

    public LandspaceLayout(int w, int h) {
        super(w, h);
    }

    @Override
    public void reportChildLayout(Layout childLayout) {
        // 子控件佈局時,會調用父控件佈局類的這個方法。在這裏記錄需要傳遞給父控件佈局的子控件佈局信息。
        // 記錄最大的子控件大小
        final Rect prune = childLayout.getNativeSpace();
        mMaxChildSpace.left = Math.max(mMaxChildSpace.left, prune.left);
        mMaxChildSpace.right = Math.max(mMaxChildSpace.right, prune.right);
        mMaxChildSpace.top = Math.max(mMaxChildSpace.top, prune.top);
        mMaxChildSpace.bottom = Math.max(mMaxChildSpace.bottom, prune.bottom);
    }

    @Override
    public void adjustSpace(Rect mySpace, Rect childSpace) {
        // 該方法會在子控件都佈局完後調用,根據子控件的佈局調整當前控件的一些佈局設置,在這裏可以調整每個子控件的佈局位置
        // 未給定高度時,根據子控件大小改變當前控件高度
        if (!fixHeight()) {
            mySpace.bottom = mMaxChildSpace.bottom + mPaddingRect.bottom;
        }

        // 調整每個子控件的位置
        Element element = null;
        NodeList nodeList = mElement.getChildNodes();
        final int size = nodeList.getLength();
        for (int i = 0; i < size; i++) {
            Node child = nodeList.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                element = (Element) child;
                Layout layout = (Layout) element.getUserData(Entity.NODE_USER_STYLE);
                if (layout != null && layout instanceof ComplexLayout) {
                    // 將子控件按順序橫向排列
                    ComplexLayout childComplexLayout = (ComplexLayout) layout;
                    if (childComplexLayout.fixLocation()) {
                        // 子控件固定位置,不作處理
                        continue;
                    }
                    if (childComplexLayout.fixX()) {
                        // 固定X值,Y值調整爲0或上邊距
                        int offsetY = mMarginRect.top;
                        childComplexLayout.setStyleByName(Entity.NODE_ATTRIBUTE_TOP, Integer.toString(offsetY) + "nat");
                        childComplexLayout.requstLayout();
                    } else if (childComplexLayout.fixY()) {
                        // 固定Y值,橫向向後排列
                        int offsetX = 0;
                        if (mLastChildSpace.right == 0) {
                            offsetX = mMarginRect.left;
                        } else {
                            offsetX = mLastChildSpace.right + childComplexLayout.mSpacingX;
                        }
                        childComplexLayout.setStyleByName(Entity.NODE_ATTRIBUTE_LEFT, Integer.toString(offsetX) + "nat");
                        childComplexLayout.requstLayout();
                    } else {
                        int offsetX = 0;
                        if (mLastChildSpace.right == 0) {
                            offsetX = mMarginRect.left;
                        } else {
                            offsetX = mLastChildSpace.right +     childComplexLayout.mSpacingX;
                        }
                        childComplexLayout.setStyleByName(Entity.NODE_ATTRIBUTE_LEFT, Integer.toString(offsetX) + "nat");
                        int offsetY = mMarginRect.top;
                        childComplexLayout.setStyleByName(Entity.NODE_ATTRIBUTE_TOP, Integer.toString(offsetY) + "nat");
                        childComplexLayout.requstLayout();
                    }
                    mLastChildSpace.set(childComplexLayout.getDisplaySpace());
                }
            }
        }
    }
}

示例2-子控件按圓形佈局的容器控件

public class CircleLayout extends ComplexLayout {

    private Rect mMaxChildSpace = new Rect();
    private int mChildNum = 0;
    private int mRadius = 0;

    public CircleLayout(int w, int h) {
        super(w, h);
    }

    @Override
    public void reportChildLayout(Layout childLayout) {
        // 子控件佈局時,會調用父控件佈局類的這個方法。在這裏記錄需要傳遞給父控件佈局的子控件佈局信息。
        // 記錄子控件個數
        mChildNum++;
        // 記錄最大的子控件大小
        final Rect prune = childLayout.getNativeSpace();
        mMaxChildSpace.left = Math.min(mMaxChildSpace.left, prune.left);
        mMaxChildSpace.right = Math.max(mMaxChildSpace.right, prune.right);
        mMaxChildSpace.top = Math.min(mMaxChildSpace.top, prune.top);
        mMaxChildSpace.bottom = Math.max(mMaxChildSpace.bottom, prune.bottom);
    }

    @Override
    public void adjustSpace(Rect mySpace, Rect childSpace) {
        // 計算圓的半徑
        mRadius = (getWidth() - mMaxChildSpace.width()) / 2;

        // 調整每個子控件的位置
        int count = 0;
        childSpace.setEmpty();
        Element element = null;
        NodeList nodeList = mElement.getChildNodes();
        final int size = nodeList.getLength();
        for (int i = 0; i < size; i++) {
            Node child = nodeList.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                element = (Element) child;
                Layout layout = (Layout) element.getUserData(Entity.NODE_USER_STYLE);
                if (layout != null && layout instanceof ComplexLayout) {
                    // 按照子控件個數,將園N等分,從正上方開始,順時針算出每個點的座標,作爲每個子控件中心點的位置
                    double angle = count * Math.PI * 2 / mChildNum - Math.PI / 2;
                    ComplexLayout childComplexLayout = (ComplexLayout) layout;
                    int offsetX = (int) (getWidth() / 2 + mRadius * Math.cos(angle));
                    offsetX -= childComplexLayout.mWidth / 2;
                    childComplexLayout.setStyleByName(Entity.NODE_ATTRIBUTE_LEFT, Integer.toString(offsetX) + "nat");
                    int offsetY = (int) (getWidth() / 2 + mRadius * Math.sin(angle));
                    offsetY -= childComplexLayout.mHeight / 2;
                    childComplexLayout.setStyleByName(Entity.NODE_ATTRIBUTE_TOP, Integer.toString(offsetY) + "nat");
                    childComplexLayout.requstLayout();

                    childSpace.union(childComplexLayout.getDisplaySpace());

                    count++;
                }
            }
        }
        // 未給定高度時,根據子控件大小改變當前控件高度
        if (!fixHeight()) {
            mySpace.bottom = childSpace.bottom + mPaddingRect.bottom;
        }
    }
}

自定義viewgroup實現等分格子布局

一般的思路就是,直接寫佈局文件,用LinearLayout 嵌套多層子LinearLayout,然後根據權重layout_weight可以達到上面的效果

還有就是利用gridview了,但是這裏的需求就是不能上下滑動,使用gridview的時候還要計算佈局的高度,否則內容超出下滑;

開始我是用的第一種,直接在佈局文件實現了,但是後來發現代碼太多太噁心哦,所以我繼承viewGroup,重寫兩個關鍵的方法:onLayout(),onMeasure()

我的大致思路:

1.計算當前視圖寬度和高度,然後根據邊距,算出每個佈局的item需要分配的多少寬度和高度:

2.支持adapter的方式,動態添加每一項,還可以設置每一項點擊事件

package com.allen.view;

import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

import com.allen.mygridlayout.R;


/**
 * @author allen
 * @email [email protected]
 * @date 2013-11-26 下午1:19:35
 * @company 富媒科技
 * @version 1.0
 * @description 格子布局(類似4.0中的gridlayout)
 */
public class MyGridLayout extends ViewGroup {
    private final String TAG = "MyGridLayout";

    int margin = 2;// 每個格子的水平和垂直間隔
    int colums = 2;
    private int mMaxChildWidth = 0;
    private int mMaxChildHeight = 0;
    int count = 0;

    GridAdatper adapter;

    public MyGridLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        if (attrs != null) {
            TypedArray a = getContext().obtainStyledAttributes(attrs,
                    R.styleable.MyGridLayout);
            colums = a.getInteger(R.styleable.MyGridLayout_numColumns, 2);
            margin = (int) a.getInteger(R.styleable.MyGridLayout_itemMargin, 2);
        }
    }

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

    public MyGridLayout(Context context) {
        this(context, null);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // TODO Auto-generated method stub
        mMaxChildWidth = 0;
        mMaxChildHeight = 0;

        int modeW = 0, modeH = 0;
        if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED)
            modeW = MeasureSpec.UNSPECIFIED;
        if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.UNSPECIFIED)
            modeH = MeasureSpec.UNSPECIFIED;

        final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                MeasureSpec.getSize(widthMeasureSpec), modeW);
        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                MeasureSpec.getSize(heightMeasureSpec), modeH);

        count = getChildCount();
        if (count == 0) {
            super.onMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            return;
        }
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() == GONE) {
                continue;
            }

            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);

            mMaxChildWidth = Math.max(mMaxChildWidth, child.getMeasuredWidth());
            mMaxChildHeight = Math.max(mMaxChildHeight,
                    child.getMeasuredHeight());
        }
        setMeasuredDimension(resolveSize(mMaxChildWidth, widthMeasureSpec),
                resolveSize(mMaxChildHeight, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // TODO Auto-generated method stub
        int height = b - t;// 佈局區域高度
        int width = r - l;// 佈局區域寬度
        int rows = count % colums == 0 ? count / colums : count / colums + 1;// 行數
        if (count == 0)
            return;
        int gridW = (width - margin * (colums - 1)) / colums;// 格子寬度
        int gridH = (height - margin * rows) / rows;// 格子高度

        int left = 0;
        int top = margin;

        for (int i = 0; i < rows; i++) {// 遍歷行
            for (int j = 0; j < colums; j++) {// 遍歷每一行的元素
                View child = this.getChildAt(i * colums + j);
                if (child == null)
                    return;
                left = j * gridW + j * margin;
                // 如果當前佈局寬度和測量寬度不一樣,就直接用當前佈局的寬度重新測量
                if (gridW != child.getMeasuredWidth()
                        || gridH != child.getMeasuredHeight()) {
                    child.measure(makeMeasureSpec(gridW, EXACTLY),
                            makeMeasureSpec(gridH, EXACTLY));
                }
                child.layout(left, top, left + gridW, top + gridH);
                // System.out
                // .println("--top--" + top + ",bottom=" + (top + gridH));

            }
            top += gridH + margin;
        }
    }

    public interface GridAdatper {
        View getView(int index);

        int getCount();
    }

    /** 設置適配器 */
    public void setGridAdapter(GridAdatper adapter) {
        this.adapter = adapter;
        // 動態添加視圖
        int size = adapter.getCount();
        for (int i = 0; i < size; i++) {
            addView(adapter.getView(i));
        }
    }

    public interface OnItemClickListener {
        void onItemClick(View v, int index);
    }

    public void setOnItemClickListener(final OnItemClickListener click) {
        if (this.adapter == null)
            return;
        for (int i = 0; i < adapter.getCount(); i++) {
            final int index = i;
            View view = getChildAt(i);
            view.setOnClickListener(new View.OnClickListener() {

                @Override
                public void onClick(View v) {
                    // TODO Auto-generated method stub
                    click.onItemClick(v, index);
                }
            });
        }
    }

}
發佈了42 篇原創文章 · 獲贊 9 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章