Android開發: View - 自定義

  • View是是Android中所有控件的基類,界面層控件控件的一種抽象,它代表的是一個控件。
  • View是一個控件,多個View組成用戶界面(User Interface)。體現視覺上的美觀,交互過程中的便捷。
  • 自定義View有三種選擇,自繪控件、組合控件、以及繼承控件。

重點方法介紹

onMeasure

控件申請大小的模式。AT_MOST、EXACTLY、UNSPECIFIED。出現情況簡單測試了下。因爲此方法會多次調用,自至完成:UNSPECIFIED-> AT_MOST-> EXACTLY 過程,所以之探討第一次調用的情況。

  • match_parent
    • 上級是什麼就是什麼
  • fill_parent
    • 上級是什麼就是什麼
  • wrap_content
    • 上級不能確定大小,就是UNSPECIFIED
    • 其他情況,就是AT_MOST
  • weight (0dp)
    • UNSPECIFIED。會在此調用onMeasure()方法直到測量出值
  • 精確值5dp、5px
    • EXACTLY。
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // 獲取的寬高mode和size
        int modeW = MeasureSpec.getMode(widthMeasureSpec);      
        switch (modeW) {
        // wrap_content;fill_parent(父控件大小不確定,父使用了wrap_content,weight(0dp)之類的)
        case MeasureSpec.AT_MOST:
            Log.d("xxx", "AT_MOST");
            break;
        // match_parent;精確值5dp、5px;fill_parent(父控件大小確定)
        case MeasureSpec.EXACTLY:
            Log.d("xxx", "EXACTLY");
            break;
        // 暫時沒有測試出來
        case MeasureSpec.UNSPECIFIED:
            Log.d("xxx", "UNSPECIFIED");
            break;
        }
        // 根據需要計算自己所需要的大小
        int sizeW = ....;
        int sizeH= ....;
        // 請求需要的寬度、高度大小
        setMeasuredDimension(sizeW, sizeH);
    }

onSizeChanged

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        // w、h是分配給View的寬、高尺寸
        // 在此進行View所用參數的計算
    }

onLayout

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // ViewGroup 用來置放子 View 的方法
        // 使用 onSizeChanged 中計算好的參數,調用 View.layout(...)來實現View的置放
    }

自定義View流程

 - 1.準備工作,獲取資源、解析XML,做些初始化工作
 - 2.*onMeasure* 確認View想要的大小。考慮padding等屬性的影響
 - *onSizeChanged*根據View實際使用的大小進行縮放比例、位置的計算
 - *onDraw* View的繪製,使用*onSizeChanged*確認好的參數進行繪製
 - 完善View,添加行爲、動作

下面兩個也是按照

時鐘 仿milter的文章

  • 考慮 padding
  • 考慮 wrap_content match_parent 和 固定值
public class ClockView extends View {

    private Context mContext;
    private Drawable mDial, mHourHand, mMinuteHand;
    private boolean mAttached;//是否顯示在View上

    //時間屬性
    private GregorianCalendar mCalendar;
    private float mHourNum, mMinuteNum;

    //用於尺寸沒發生變化時的繪製屬性,每次變化是會重新賦值
    private float scaleNum;
    //繪製圖形選定的錶盤中心位置,用於縮放和指針旋轉的變化的參數
    private int dialCenterX, dialCenterY;

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

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

    public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ClockView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mContext = context;
        initData();
    }

    /**
     * ##############################
     * 對外方法
     * ##############################
     */
    public void refreshClock() {
        onTimeChanged();
        invalidate();
    }


    /**
     * ##############################
     * 準備工作
     * ##############################
     */
    private void initData() {
        mDial = mContext.getDrawable(R.mipmap.clock_dial);
        mHourHand = mContext.getDrawable(R.mipmap.clock_hand_hour);
        mMinuteHand = mContext.getDrawable(R.mipmap.clock_hand_minute);
    }


    /**
     * #############################
     * 確認 View “想要”的大小
     * #############################
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //錶盤圖片的本身寬高(作爲時鐘的整體寬高)
        int intrinsicWidth = mDial.getIntrinsicWidth();
        int intrinsicHeight = mDial.getIntrinsicHeight();

        //獲取分配的寬高
        int modeW = MeasureSpec.getMode(widthMeasureSpec);
        int sizeW = MeasureSpec.getSize(widthMeasureSpec);
        int modeH = MeasureSpec.getMode(heightMeasureSpec);
        int sizeH = MeasureSpec.getSize(widthMeasureSpec);


        /** 第一次計算,計算縮放比例,確定View請求大小。(請求大小 不代表 具體顯示大小)
         * 請求的大小發生改變改變觸發 {@link ClockView#onSizeChanged(int, int, int, int)} 方法*/
        float wScale = 1.0f;
        float hScale = 1.0f;
        if (modeW != MeasureSpec.UNSPECIFIED && sizeW < intrinsicHeight) {
            wScale = (float) sizeW / intrinsicWidth;
        }
        if (modeH != MeasureSpec.UNSPECIFIED && sizeH < intrinsicHeight) {
            hScale = (float) sizeH / intrinsicHeight;
        }
        float scale = Math.min(wScale, hScale);


        /**
         * getDefaultSize()方法:AT_MOST、EXACTLY效果一樣(實際使用) -- 只對Matc和具體值適配
         * resolveSizeAndState()方法:AT_MOST(太大加標記,表示會考慮)、EXACTLY(實際使用)
         *
         * 由 {@link android.view.ViewRootImpl#getRootMeasureSpec(int, int)} 剋制
         * 解析的 Mode :wrap_content-->AT_MOST 、match_parent、(具體值)-->EXACTLY
         *
         * 這句代碼:result = specSize | MEASURED_STATE_TOO_SMALL
         *          當控件索取的空間大於實際使用的數值時,添加的一個標記
         *
         * 請求寬高時 -- 加入Padding值
         */
        int paddingX = getPaddingLeft() + getPaddingRight();
        int paddingY = getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(resolveSizeAndState((int) (scale * intrinsicWidth) + paddingX, widthMeasureSpec, 0)
                , resolveSizeAndState((int) (scale * intrinsicHeight) + paddingY, heightMeasureSpec, 0));
    }


    /**
     * ########################
     * 根據實際使用大小  計算View中個部件的大小、位置
     * ########################
     *
     * @param w 經過考慮後顯示的 寬(實際)
     * @param h 經過考慮後顯示的 高(實際)
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {

        //錶盤圖片的大小 需求寬高
        int intrinsicWidth = mDial.getIntrinsicWidth();
        int intrinsicHeight = mDial.getIntrinsicHeight();


        //View尺寸設置,根據 “錶盤” 縮放比例設置
        //注意Padding值的印象
        //第二次計算,View尺寸改變。計算縮放比例,並重新設置 Drawable 的顯示區域
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        scaleNum = Math.min((float) (w - paddingLeft - paddingRight) / intrinsicWidth,
                (float) (h - paddingTop - paddingBottom) / intrinsicHeight);//縮放比例


        //View位置設定
        //錶盤Drawable 位置設置
        mDial.setBounds(paddingLeft, paddingTop, paddingLeft + intrinsicWidth, paddingTop + intrinsicHeight);


        //因爲  時鐘指針和分鐘指針  資源圖片就是以中心點繪製的,這裏以錶盤繪製中心點設置
        // 時間改變尺寸不變時,只需要改變旋轉角度就可以了(在onDraw()方法中執行)
        dialCenterX = paddingLeft + intrinsicWidth / 2;
        dialCenterY = paddingTop + intrinsicHeight / 2;

        //時鐘Drawable 位置設置
        intrinsicWidth = mHourHand.getIntrinsicWidth();
        intrinsicHeight = mHourHand.getIntrinsicHeight();
        mHourHand.setBounds(dialCenterX - intrinsicWidth / 2, dialCenterY - intrinsicHeight / 2,
                dialCenterX + intrinsicWidth / 2, dialCenterY + intrinsicHeight / 2);


        //分鐘Drawable 位置設置
        intrinsicWidth = mMinuteHand.getIntrinsicWidth();
        intrinsicHeight = mMinuteHand.getIntrinsicHeight();
        mMinuteHand.setBounds(dialCenterX - intrinsicWidth / 2, dialCenterY - intrinsicHeight / 2,
                dialCenterX + intrinsicWidth / 2, dialCenterY + intrinsicHeight / 2);
    }

    /**
     * ##############################
     * 設置View的顯示位置, 此方法 ParentView 調用
     * 當ParentView 發生變化激活其 {@link android.view.ViewGroup#onLayout(boolean, int, int, int, int)}方法
     * 在其中進行子 View的位置重新計算,並條用此方法將子View(本View)設置到指定的地點去顯示
     * ##############################
     */
    @Override
    public void layout(int l, int t, int r, int b) {
        super.layout(l, t, r, b);
    }


    /**
     * #######################
     * 繪製View並確認View中圖形的位置
     * <p/>
     * save():用來保存Canvas的狀態。save之後,可以調用Canvas的平移、放縮、旋轉、錯切、裁剪等操作
     * restore:用來恢復Canvas之前保存的狀態。防止save後對Canvas執行的操作對後續的繪製有影響。
     * 使用時:restore調用次數比save多,會引發Error
     * <p/>
     * {@link ClockView#onDraw(Canvas)}  -- 方法多次調用
     * <p/>
     * 最開始時父試圖中是沒有子試圖的,當你從xml文件中加載子試圖或者在java代碼中添加子試圖時,父試圖的狀態會發生變化
     * 這個變化會引起 {@link ClockView#onLayout(boolean, int, int, int, int)} 甚至是 {@link ClockView#onMeasure(int, int)} 方法
     * <p/>
     * #######################
     */
    @Override
    protected void onDraw(Canvas canvas) {
        //使用計算好了的尺寸數據進行view的繪製
        canvas.save();
        canvas.scale(scaleNum, scaleNum, dialCenterX, dialCenterY);


        //錶盤的繪製
        canvas.save();
        mDial.draw(canvas);
        canvas.restore();


        //時鐘Drawable 角度設置
        canvas.save();
        canvas.rotate(mHourNum / 12 * 360, dialCenterX, dialCenterY);
        mHourHand.draw(canvas);
        canvas.restore();


        //分鐘Drawable 角度設置
        canvas.save();
        canvas.rotate(mMinuteNum / 60 * 360, dialCenterX, dialCenterY);
        mMinuteHand.draw(canvas);
        canvas.restore();


        //恢復縮放操作之前的狀態
        canvas.restore();

    }

    /**
     * ###########################
     * 行爲動作設置 -- 時間廣播接收器
     * ###########################
     */
    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            //時區更改
            if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {
                String tz = intent.getStringExtra("time-zone");
                mCalendar.setTimeZone(TimeZone.getTimeZone(tz));
            }
            onTimeChanged();
            invalidate();
        }
    };

    private void onTimeChanged() {

        mCalendar.setTimeInMillis(System.currentTimeMillis());

        int hour12 = mCalendar.get(Calendar.HOUR);
        int minute = mCalendar.get(Calendar.MINUTE);
        int second = mCalendar.get(Calendar.SECOND);

        //Calendar的可以是Linient模式,此模式下,second和minute是可能超過60和24的,具體這裏就不展開了
        mHourNum = hour12 + minute / 60f;
        mMinuteNum = minute + second / 60f;
    }


    /**
     * 添加到Windows時調用
     */
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (!mAttached) {
            mAttached = true;
            IntentFilter filter = new IntentFilter();
            filter.addAction(Intent.ACTION_TIME_TICK);//每分鐘一次
            filter.addAction(Intent.ACTION_TIME_CHANGED);
            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
            mContext.registerReceiver(mBroadcastReceiver, filter);
        }
        mCalendar = new GregorianCalendar();
        onTimeChanged();
    }


    /**
     * 從Windows分離時調用
     */
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mAttached) {
            mAttached = false;
            mContext.unregisterReceiver(mBroadcastReceiver);
        }
    }
}

環形進度條

  • 考慮 padding
  • 考慮 wrap_content match_parent 和 固定值
  • 考慮 數據保存和恢復
package com.elife.mobile.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import com.cy_life.mobile.R;

/**
 * 環形進度顯示
 * @author huangjf
 */
public class CircleProgressView extends View{

    private static final String INSTANCE_STATUS = "instance_status";
    private static final String END_TIME = "end_time_millis";

    // TODO 如有需要轉到attr中設置
    private final int progressBgColor = R.color.theme_main_blue;
    private final int prgressSolidColor = R.color.c_0296f3;
    private final int mRingWidth = 8;// 圓環寬度

    private Paint mPaint; // 畫筆
    private RectF mRectF; // 扇形位置信息,用於話圓環
    private int mDiameterMax;// 最大圓直徑
    private int mTextHeight;// 就是 TextSize

    // 倒計時相關
    private Handler mHandler;
    private long endTimeMillis = -1, timeLeftMillis, durationMillis;
    private OnProgressListener callBack;
    private Runnable timingRunnable = new Runnable() {

        @Override
        public void run() {

            timeLeftMillis = endTimeMillis - SystemClock.elapsedRealtime();

            Log.d("hjf", "TimeLeft:" + timeLeftMillis);

            if (timeLeftMillis <= 0) {
                onEnd();
            }else {
                long delay = timeLeftMillis % 1000;
                mHandler.postDelayed(this, delay);
            }
            invalidate();
        }
    };

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

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

    public CircleProgressView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {

        // 畫筆
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeWidth(mRingWidth);

        // 扇形位置信息,用於畫圓環
        mRectF = new RectF();
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {

        // 支持 padding 屬性
        int showWidth = w - getPaddingLeft() - getPaddingRight();
        int showHeight = h - getPaddingTop() -getPaddingBottom();

        mDiameterMax = Math.min(showWidth, showHeight);

        // 規劃扇形區域
        mRectF = new RectF();
        mRectF.left = mRingWidth / 2;
        mRectF.top = mRingWidth / 2;
        mRectF.right = mDiameterMax - mRingWidth / 2;
        mRectF.bottom = mDiameterMax - mRingWidth / 2;

        // 中間文字的 TextSize 值
        mTextHeight = mDiameterMax / 4 ;
    }

    @Override
    protected void onDraw(Canvas canvas) {

        // 畫圓環背景
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(getResources().getColor(progressBgColor));
        canvas.drawArc(mRectF, -90, 360, false, mPaint);

        // 畫扇形圓環進度
        mPaint.setColor(getResources().getColor(prgressSolidColor));
        float rate = 0;
        if (durationMillis != 0) {
            rate = 1 - timeLeftMillis * 1f / durationMillis;
        }
        canvas.drawArc(mRectF, -90, 360 * rate, false, mPaint);

        // 寫字
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextSize(mTextHeight);
        String text = String.valueOf((int) Math.ceil(timeLeftMillis * 1f / 1000));
        float textWidth = mPaint.measureText(text, 0 , text.length());
        canvas.drawText(text, (mDiameterMax - textWidth) / 2 , (mDiameterMax + mTextHeight) / 2, mPaint);
    }

    /**
     * 開始倒計時
     * @param durationMillis 倒計時的持續時間
     */
    public void startTiming(long durationMillis){
        if (this.mHandler != null) {
            return;
        }
        this.endTimeMillis = SystemClock.elapsedRealtime() + durationMillis;
        this.durationMillis = durationMillis;
        this.mHandler = new Handler(Looper.getMainLooper());
        this.mHandler.post(this.timingRunnable);
    }


    // 1. 保存
    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        bundle.putLong(END_TIME, endTimeMillis);
        bundle.putParcelable(INSTANCE_STATUS, super.onSaveInstanceState());
        return bundle;
    }


    // 2. 分離
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        stopTiming();
    }


    // 3. 恢復
    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof Bundle)) {
            super.onRestoreInstanceState(state);
            return;
        }
        Bundle bundle = (Bundle) state;
        endTimeMillis = bundle.getLong(END_TIME);   
        super.onRestoreInstanceState(bundle.getParcelable(INSTANCE_STATUS));
    }

    // 4. 關聯
    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        continueTiming();
    }


    /**
     * 恢復View時調用,例如屏幕旋轉
     */
    private void continueTiming(){
        if (this.endTimeMillis == -1) {
            return;
        }
        if (this.mHandler == null) {
            this.mHandler = new Handler(Looper.getMainLooper());
            this.mHandler.post(this.timingRunnable);
        }
    }

    // 因外力等因素暫停倒計時,非倒計時結束自動結束
    public void stopTiming(){
        if (this.mHandler == null) {
            return;
        }
        this.mHandler.removeCallbacks(this.timingRunnable);
        this.mHandler = null;
    }

    // 倒計時結束後的操作
    private void onEnd() {
        timeLeftMillis = 0;
        this.endTimeMillis = -1;
        if (this.callBack != null) {
            this.callBack.onEnd();
        }
    }

    public void setProgressListener(OnProgressListener progressCallBack){
        this.callBack = progressCallBack;
    }

    public static interface OnProgressListener{
        void onEnd();
    }
}

attr屬性


    <declare-styleable name="CircleProgress">
        <attr name="innerCircleRadius" format="dimension" />
        <attr name="innerCircleColor" format="color" />
        <attr name="outRingColorBG" format="color" />
        <attr name="outRingColor" format="color" />
        <attr name="outRingWidth" format="dimension" />
    </declare-styleable>

圖片1

自定義ViewGroup

  • onMeasure中 計量本所需要的尺寸
  • onLayout中 計算子View顯示的位置,並調用子View的layout(…)進行位置設置

左->右 上->下 的ViewGroup

public class SequenceViewGroup extends ViewGroup {
    public SequenceViewGroup(Context context) {
        this(context, null);
    }

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

    public SequenceViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public SequenceViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    /**
     * #######################################
     * 計量本 ViewGroup 所需要的尺寸
     * #######################################
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int childCount = getChildCount();

        //本ViewGroup需要的大小
        int groupViewWidth = MeasureSpec.getSize(widthMeasureSpec);
        int groupViewHeight = 0;


        //當前View在X軸插入的點
        int inputPointX = getPaddingLeft() + getPaddingRight();
        //當前行子View中高度最大值
        int childViewMaxHeight = 0;


        //遍歷所有子View 計算本ViewGroup的高度
        for (int i = 0; i < childCount; i++) {

            //獲取當前子View
            View childView = getChildAt(i);

            //GONE 忽略不算
            if (childView.getVisibility() == View.GONE) continue;

            //遍歷測量子View寬高
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);

            //計算將當前子View加入當前行後,此時所使用的寬度
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();

            //換行顯示:計算當前子View加入後,當前行的顯示所需寬度超過 X軸界線; 否則記錄所在行View高度最大值
            if (inputPointX + childWidth > groupViewWidth) {
                //重置 X軸View插入座標
                inputPointX = 0;
                groupViewHeight += childViewMaxHeight;
                //換行後將最高高度重置 -- 當前View的高度(連續換行,每行只有一個View的情況)
                childViewMaxHeight = childHeight;
            } else {
                //沒有超過時,記錄本行最高子 View的高度
                childViewMaxHeight = Math.max(childViewMaxHeight, childHeight);
            }

            //跟新 X軸View插入座標
            inputPointX += childWidth;
        }

        //加上最後一個View的高度
        groupViewHeight += childViewMaxHeight;
        //padding 影響
        groupViewHeight += getPaddingTop() + getPaddingBottom();

        //這裏將寬度和高度與Google爲我們設定的建議最低寬高對比,確保我們要求的尺寸不低於建議的最低寬高。
        groupViewWidth = Math.max(groupViewWidth, getSuggestedMinimumWidth());
        groupViewHeight = Math.max(groupViewHeight, getSuggestedMinimumHeight());

        //請求寬高
        setMeasuredDimension(resolveSizeAndState(groupViewWidth, widthMeasureSpec, 0),
                resolveSizeAndState(groupViewHeight, heightMeasureSpec, 0));
    }


    /**
     * #################################
     * 計算子View顯示的位置 並位置屬性設置給子 View
     * #################################
     * 通過調用子View的 {@link View#layout(int, int, int, int)} 方法實現對子View位置的控制
     * <p/>
     * ViewGroup 的父控件調用這個方法 {@link ViewGroup#layout(int, int, int, int)} 給本 ViewGroup 確認位置
     * 此方法在 ViewGroup類 用 “ final ” 字段修飾了,我們就無需考慮了
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        //計算佈局可用空間的距離:可添加View空間起始 X座標,Y座標,X軸最大值座標,添加的View的顯示不能超過此 X軸
        int useableSpaceLeft = getPaddingLeft();
        int useableSpaceTop = getPaddingTop();
        int useableSpaceRight = r - getPaddingRight() - l;

        //添加過程中的數據記錄:當前View添加位置 X座標,Y座標,當前行View高度的最大值
        int inputPointX = useableSpaceLeft;
        int inputPointY = useableSpaceTop;
        int childViewMaxHeight = 0;

        //遍歷所有
        for (int i = 0; i < getChildCount(); i++) {

            //獲取子View 及其寬高
            View childView = getChildAt(i);
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();


            //換行顯示:計算當前子View加入後,當前行的顯示所需寬度超過 X軸界線
            if (inputPointX + childWidth > useableSpaceRight) {
                //跟新 X、Y軸 View插入座標
                inputPointX = useableSpaceLeft;
                inputPointY += childViewMaxHeight;
                //換行後將最高高度重置 -- 當前View的高度(連續換行,每行只有一個View的情況)
                childViewMaxHeight = childHeight;
            } else {
                childViewMaxHeight = Math.max(childViewMaxHeight, childHeight);
            }


            //設置子View的顯示區域座標
            childView.layout(inputPointX, inputPointY, inputPointX + childWidth, inputPointY + childHeight);


            //跟新 X軸View插入座標
            inputPointX += childWidth;
        }
    }
}

圖片2

組合控件就是使用系統給的View封裝成的一個View,比如常見的Title欄封裝。

繼承控件可以看這個例子 水滴刷新動畫的RecyclerView 的封裝。

項目下載地址

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