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 的封装。

项目下载地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值