美文网首页
android view的绘制

android view的绘制

作者: 梦语少年 | 来源:发表于2017-06-15 23:19 被阅读16次

    1.为什么要自定义View?

    参考链接之超精辟的自定义view 绘制

    • UI的奇葩设计。
    • 多界面的组件复用。

    2.知识点

    2.1 MeasureSpec

    MeasureSpec是一个32为的整数值,前两位表示测量的模式Spec
    Mode,后30位表示该模式下的规格大小SpecSize。

    MeasureSpec核心代码如下:

      public static class MeasureSpec {
            private static final int MODE_SHIFT = 30;
            private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
            /** @hide */
            @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
            @Retention(RetentionPolicy.SOURCE)
            public @interface MeasureSpecMode {}
    
            /**
             * Measure specification mode: The parent has not imposed any constraint
             * on the child. It can be whatever size it wants.
             */
            public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    
            /**
             * Measure specification mode: The parent has determined an exact size
             * for the child. The child is going to be given those bounds regardless
             * of how big it wants to be.
             */
            public static final int EXACTLY     = 1 << MODE_SHIFT;
    
            /**
             * Measure specification mode: The child can be as large as it wants up
             * to the specified size.
             */
            public static final int AT_MOST     = 2 << MODE_SHIFT;
    
            /**
             * Creates a measure specification based on the supplied size and mode.
             *
             * The mode must always be one of the following:
             * <ul>
             *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
             *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
             *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
             * </ul>
             *
             * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
             * implementation was such that the order of arguments did not matter
             * and overflow in either value could impact the resulting MeasureSpec.
             * {@link android.widget.RelativeLayout} was affected by this bug.
             * Apps targeting API levels greater than 17 will get the fixed, more strict
             * behavior.</p>
             *
             * @param size the size of the measure specification
             * @param mode the mode of the measure specification
             * @return the measure specification based on size and mode
             */
            public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                              @MeasureSpecMode int mode) {
                if (sUseBrokenMakeMeasureSpec) {
                    return size + mode;
                } else {
                    return (size & ~MODE_MASK) | (mode & MODE_MASK);
                }
            }
    
            /**
             * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
             * will automatically get a size of 0. Older apps expect this.
             *
             * @hide internal use only for compatibility with system widgets and older apps
             */
            public static int makeSafeMeasureSpec(int size, int mode) {
                if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
                    return 0;
                }
                return makeMeasureSpec(size, mode);
            }
    
            /**
             * Extracts the mode from the supplied measure specification.
             *
             * @param measureSpec the measure specification to extract the mode from
             * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
             *         {@link android.view.View.MeasureSpec#AT_MOST} or
             *         {@link android.view.View.MeasureSpec#EXACTLY}
             */
            @MeasureSpecMode
            public static int getMode(int measureSpec) {
                //noinspection ResourceType
                return (measureSpec & MODE_MASK);
            }
    
            /**
             * Extracts the size from the supplied measure specification.
             *
             * @param measureSpec the measure specification to extract the size from
             * @return the size in pixels defined in the supplied measure specification
             */
            public static int getSize(int measureSpec) {
                return (measureSpec & ~MODE_MASK);
            }
    
            static int adjust(int measureSpec, int delta) {
                final int mode = getMode(measureSpec);
                int size = getSize(measureSpec);
                if (mode == UNSPECIFIED) {
                    // No need to adjust size for UNSPECIFIED mode.
                    return makeMeasureSpec(size, UNSPECIFIED);
                }
                size += delta;
                if (size < 0) {
                    Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
                            ") spec: " + toString(measureSpec) + " delta: " + delta);
                    size = 0;
                }
                return makeMeasureSpec(size, mode);
            }
    
            /**
             * Returns a String representation of the specified measure
             * specification.
             *
             * @param measureSpec the measure specification to convert to a String
             * @return a String with the following format: "MeasureSpec: MODE SIZE"
             */
            public static String toString(int measureSpec) {
                int mode = getMode(measureSpec);
                int size = getSize(measureSpec);
    
                StringBuilder sb = new StringBuilder("MeasureSpec: ");
    
                if (mode == UNSPECIFIED)
                    sb.append("UNSPECIFIED ");
                else if (mode == EXACTLY)
                    sb.append("EXACTLY ");
                else if (mode == AT_MOST)
                    sb.append("AT_MOST ");
                else
                    sb.append(mode).append(" ");
    
                sb.append(size);
                return sb.toString();
            }
        }
    
    • 主要方法
    方法 用法
    makeMeasureSpec 重置MeasureSpec约束
    getMode 获取MeasureSpec的模式specMode
    getSize 获取Measure的模式specSize
    • 三种模式
    方法(音标) 用法
    UNSPECTIFIED(ʌn'spɛsɪfaɪd) 未指明尺寸
    EXACTLY(ɪɡ'zæktli) 精确的尺寸
    AT_MOST 父视图允许的最大尺寸

    2.2 Measure

    Measure操作用来计算View的实际大小,用于确定当前View或ViewGroup的实际宽高。
    常用方法如下:

    • onMeasure的源码
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
        }
    

    由onMeasure的源码可以看出,该方法的目的,就是为了确定view的具体宽高;

    • setMeasuredDimension的源码
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
            boolean optical = isLayoutModeOptical(this);
            if (optical != isLayoutModeOptical(mParent)) {
                Insets insets = getOpticalInsets();
                int opticalWidth  = insets.left + insets.right;
                int opticalHeight = insets.top  + insets.bottom;
    
                measuredWidth  += optical ? opticalWidth  : -opticalWidth;
                measuredHeight += optical ? opticalHeight : -opticalHeight;
            }
            setMeasuredDimensionRaw(measuredWidth, measuredHeight);
        }
    

    用于设置View的宽和高,在每一次view尺寸运算完成使用,可以理解为,onMeasure中最后一个必调的方法;

    • setMeasuredDimensionRaw的源码
    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
            mMeasuredWidth = measuredWidth;
            mMeasuredHeight = measuredHeight;
    
            mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
        }
    

    设置每一行的宽高,很少使用,略;

    • getMeasuredWidth的源码
    public final int getMeasuredWidth() {
            return mMeasuredWidth & MEASURED_SIZE_MASK;
        }
    

    获取view的宽度;

    • getLayoutParams()的源码
     @ViewDebug.ExportedProperty(deepExport = true, prefix = "layout_")
        public ViewGroup.LayoutParams getLayoutParams() {
            return mLayoutParams;
        }
    

    获取View的Layoutparams(备注:layout_开头的所有参数都放在了里面);

    • measureChildren的源码(ViewGroup)
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
            final int size = mChildrenCount;
            final View[] children = mChildren;
            for (int i = 0; i < size; ++i) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                    measureChild(child, widthMeasureSpec, heightMeasureSpec);
                }
            }
        }
    

    遍历所有的子View,测量全部View的宽高。一般与getMeasuredWidth()或getMeasuredHeight()配合使用;

    • measureChild的源码(ViewGroup)
    protected void measureChild(View child, int parentWidthMeasureSpec,
                int parentHeightMeasureSpec) {
            final LayoutParams lp = child.getLayoutParams();
    
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom, lp.height);
    
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    

    用于测量具体子view的宽和高;一般与getMeasuredWidth()或getMeasuredHeight()配合使用;

    • getChildAt(i)(ViewGroup)
      获取子View

    • getChildCount()(ViewGroup)
      获取子View的数量

    • 备注:layout_width 三种设置值的方式,以及对应的模式流程

    赋值方式 模式流程
    wrap_content AT_MOST
    match_parent AT_MOST --> EXACTLY
    100dp EXACTLY

    2.3 Layout(ViewGroup)

    Layout的过程是用于确定View在父布局中的位置。由父布局获取参数,然后将参数传递给子View的layout方法中,将view放在具体的位置;
    在onLayout中没有太复杂的逻辑需要处理,相应的参数都可以在onMeasure中获得。建议使用LayoutParams进行传值,若子view的值固定,可以使用makeMeasureSpec进行重置约束,通过setMeasuredDimension或onMeasure进行设置子view的约束。

    实例代码如下

    @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                child.layout(l, t, r, b);//这里不能直接使用onLayout的参数,具体的参数值可以通过,全局变量或LayoutParams进行传递;
            }
        }
    

    2.4 Draw(多用于View)

    onDraw操作用于绘制view的具体界面,可以绘制常见的图形、文字等,通过对view的操作,也可以实现具体的动画,如MD动画的实现。
    同时,由于ViewGroup大多为容器,用户承载view,很少会使用onDraw,当然,也可以使用Draw绘制背景等。

    核心知识点
    • Paint 画笔
      Paint方法用于在Canvas上绘制内容,可以设置Paint的宽度、颜色、笔触、以及对图片进行滤镜处理等。
      详细的Paint效果,请查看Android 画笔Paint

    常用的Paint代码:

     private void initPaint() {
            mPaint = new Paint();
            mPaint.setStyle(Paint.Style.STROKE);//设置画笔样式
            mPaint.setStrokeWidth(10);// 设置画笔宽度
            mPaint.setColor(ContextCompat.getColor(getContext(), R.color.colorAccent));//设置画笔颜色
            mPaint.setAntiAlias(true);//设置抗锯齿
            mPaint.setTextSize(60);//设置文字尺寸
        }
    

    设置

    笔触类型 描述
    FILL_AND_STROKE 填充内部且描边
    STROKE 描边
    FILL 填充内部
    • Canvas 画布

      绘制文字

            /**
             * @param text  文本
             * @param x     水平方向起点
             * @param y     竖直方向的文字底部
             * @param paint 画笔
             */
            canvas.drawText("画圆:", 50, 1000, mPaint);
    

    绘制矩形

           /**
             * RectF : 屏幕左上角是坐标原点,4个参数:left,top,right,bottom
             * left:屏幕左边到矩形左边的距离
             * top:屏幕顶部到矩形顶边的距离
             * right:屏幕左边到矩形右边的距离
             * bottom:屏幕顶边到矩形底边的距离
             *
             * @desc 绘制一个宽50的矩形
             */
            canvas.drawRect(new RectF(50,50,100,100),mPaint);
    

    ** 将整个画布绘制成画笔的颜色**

           /**
             * 将整个画布绘制成画笔的颜色
             */
            canvas.drawPaint(mPaint);
    

    ** 绘制一条水平直线**

          /**
             * @param startX X轴的起点
             * @param startY Y轴的起点
             * @param stopX X轴的终点
             * @param stopY Y轴的终点
             * @param paint
             */
            canvas.drawLine(10,10,100,10,mPaint);
       
    

    ** 绘制一条水平直线**

          /**
             * @param startX X轴的起点
             * @param startY Y轴的起点
             * @param stopX X轴的终点
             * @param stopY Y轴的终点
             * @param paint
             */
            canvas.drawLine(10,10,100,10,mPaint);
       
    

    绘制圆弧或扇形

            /**
               * oval:圆弧所在的RectF对象。
               * startAngle:圆弧的起始角度。
               * sweepAngle:圆弧的结束角度。
               * useCenter:是否显示半径连线,true表示显示圆弧与圆心的半径连线,false          表示不显示。
               * paint:绘制时所使用的画笔。
               */
           //绘制扇形
            RectF oval1 = new RectF(50, 50, 300, 300);
            canvas.drawArc(oval1,90,90,true,mPaint);
            //绘制圆弧
            mPaint.setStyle(Paint.Style.STROKE);
            RectF oval2 = new RectF(150, 150, 600, 600);
            canvas.drawArc(oval2,90,90,false,mPaint);
           
    

    绘制圆角矩形

    /**
             * RectF : 屏幕左上角是坐标原点,4个参数:left,top,right,bottom
             * left:屏幕左边到矩形左边的距离
             * top:屏幕顶部到矩形顶边的距离
             * right:屏幕左边到矩形右边的距离
             * bottom:屏幕顶边到矩形底边的距离
             * @param rx 圆角X轴半径
             * @param ry 圆角Y轴半径
             * @desc 绘制一个宽50的矩形
             */
            canvas.drawRoundRect(new RectF(50,50,100,100),mPaint);
    

    绘制图片

    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher);
    /**
     * @param bitmap The bitmap to be drawn
     * @param left   左上角x坐标
     * @param top    左上角y坐标
     * @param paint  画笔
     */
    canvas.drawBitmap(bitmap, 360, 1300, mPaint);
    

    绘制点

    /**
     * x,y坐标
     */
    //画一个点
    canvas.drawPoint(500, 1200, p);
    //画多个点
    canvas.drawPoints(new float[]{600, 1200, 650, 1250, 700, 1200}, mPaint);
    
    

    绘制三角形

    Path path = new Path();
    path.moveTo(500, 750);// 此点为多边形的起点
    path.lineTo(400, 850);
    path.lineTo(600, 850);
    path.close(); // 使这些点构成封闭的多边形
    canvas.drawPath(path, mPaint);
    

    绘制贝塞尔曲线

    Path path2 = new Path();
    path2.moveTo(500, 1050);//设置Path的起点
    //设置贝塞尔曲线的控制点坐标和终点坐标
    path2.quadTo(600, 950, 700, 1050);
    path2.quadTo(800, 1150, 900, 1050); 
    canvas.drawPath(path2, mPaint);
    

    备注:画线的时候,一定要将画笔设置为Paint.Style.STROKE

    2.5 自定义xml中的属性

    1. 创建attrs.xml文件

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="FlowLayout">
            <attr name="space" format="dimension"></attr>
        </declare-styleable>
    </resources>
    

    declare-styleable 设置自定义view的名称,attr中设置view中需要使用的具体属性,以及相应属性的具体格式;

    所有属性的格式如下:

    格式 描述 使用
    reference 资源ID @drawable/图片ID
    color 颜色值 #ffffff
    boolean 布尔值 false or true
    dimension 尺寸dp值 100dp or 100dip
    float 浮点值 1.0
    integer 整型值 100
    string 字符串 "str"
    fraction 百分数 100%
    enum 枚举类型 使用enum标签设置
    flag 位或运算 类事枚举,使用flag标签设置

    2. 在View中引用
    获取自定义属性值

      void init(Context context, AttributeSet attrs) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
            space = ta.getDimensionPixelSize(R.styleable.FlowLayout_space, 0);
            ta.recycle();
        }
    

    init方法应该放在以下的构造方法中。(备注:默认都会调用以下构造方法,无论是否进行findViewById)

    public FlowLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
            Log.e(TAG, "FlowLayout(Context context, AttributeSet attrs)");
            init(context, attrs);
            Log.e(TAG,space+"");
        }
    

    3. 在布局文件中使用

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="study.zxh.com.viewdemo.MainActivity">
    
        <study.zxh.com.viewdemo.FlowLayout
            android:id="@+id/fl"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:space="10dp" />
    
    </android.support.constraint.ConstraintLayout>
    

    在跟标签引入

    xmlns:app="http://schemas.android.com/apk/res-auto"
    

    在自定义的布局中使用

    app:space="10dp"
    

    2.6 LayoutParam

    • 自定义LayoutParams
        @Override
        protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
            return p instanceof LayoutParams;
        }
    
        @Override
        public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
            return new LayoutParams(getContext(), attrs);
        }
    
        @Override
        protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
            return new LayoutParams(p);
        }
    
        @Override
        protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
            return new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        }
    
        private static class LayoutParams extends ViewGroup.LayoutParams {
            private int left;
            private int top;
            private int right;
            private int bottom;
    
            public LayoutParams(Context c, AttributeSet attrs) {
                super(c, attrs);
            }
    
            public LayoutParams(int width, int height) {
                super(width, height);
            }
    
            public LayoutParams(ViewGroup.LayoutParams source) {
                super(source);
            }
    
            public LayoutParams(LayoutParams source) {
                super(source);
                this.left = source.left;
                this.top = source.top;
                this.right = source.right;
                this.bottom = source.bottom;
            }
        }
    
    • 引用步骤
    1. 实现静态内部类LayoutParams,并继承自ViewGroup.LayoutParams或其子类;
    2. 重写checkLayoutParams用于检测其类型。
    3. 重写generateLayoutParams实现自定义LayoutParams的构造;
    4. 重写generateDefaultLayoutParams()传入默认的LayoutParams,一般传入ViewGroup.LayoutParams。
    5. 使用时,直接通过view.getLayoutParams(),获取子view的LayoutParams使用;

    3.自定义View之实现MD按钮动画

    4.自定义ViewGroup之流式布局

    实现的最终效果

    实现的最终效果

    最终代码

    /**
     * Created by zhangxuehui on 2017/6/16.
     * 实现动态添加文字标签
     */
    public class FlowLayout extends ViewGroup {
        private static final String TAG = "FlowLayout";
        private int space = 0;//文字间的间距,以及四周的边距;
    
    
        public FlowLayout(Context context) {
            super(context);
        }
    
        public FlowLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
            init(context, attrs);
            Log.e(TAG, space + "");
        }
    
        public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        //初始化自定义的属性
        void init(Context context, AttributeSet attrs) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
            space = ta.getDimensionPixelSize(R.styleable.FlowLayout_space, 0);
            ta.recycle();
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int curWidth = 0;
            int curHeight = 0;
            int maxWidth = 0;
            int maxHeight = 0;
            int pos = 0;//用于判读最后一行是否有数据
            for (int i = 0; i < this.getChildCount(); i++) {
                View child = this.getChildAt(i);
                //测量child的宽高
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                lp.left = curWidth + space;
                lp.top = maxHeight + space;
                lp.right = lp.left + child.getMeasuredWidth();
                lp.bottom = lp.top + child.getMeasuredHeight();
                curWidth = lp.right;
                curHeight = Math.max(child.getMeasuredHeight(), curHeight);
                if (curWidth > widthSize - space) {//翻页,需要去除右边距,否则,最右侧标签可能会贴紧屏幕;
                    //翻页后,初始化相应的参数
                    maxWidth = Math.max(maxWidth, curWidth);
                    pos = i;
                    curHeight = 0;
                    curWidth = 0;
                    maxHeight = lp.bottom;
                    //重新设置超出屏幕的view
                    lp.left = curWidth + space;
                    lp.top = maxHeight + space;
                    lp.right = lp.left + child.getMeasuredWidth();
                    lp.bottom = lp.top + child.getMeasuredHeight();
                    //取得最新的参数
                    curWidth = lp.right;
                    curHeight = Math.max(child.getMeasuredHeight(), curHeight);
                }
                Log.e(TAG, lp.toString());
            }
            if (getChildCount() > pos) {
                maxHeight += curHeight + space * 2;
            }
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY);//重建约束
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);//重建约束
            setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, MeasureSpec.EXACTLY), resolveSizeAndState(maxHeight, heightMeasureSpec, MeasureSpec.EXACTLY));
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            for (int i = 0; i < this.getChildCount(); i++) {
                View child = this.getChildAt(i);
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                child.layout(lp.left, lp.top, lp.right, lp.bottom);
            }
        }
    
        @Override
        protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
            return p instanceof LayoutParams;
        }
    
        @Override
        public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
            return new LayoutParams(getContext(), attrs);
        }
    
        @Override
        protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
            return new LayoutParams(p);
        }
    
        @Override
        protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
            return new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        }
    
        private static class LayoutParams extends ViewGroup.LayoutParams {
            private int left;
            private int top;
            private int right;
            private int bottom;
    
            public LayoutParams(Context c, AttributeSet attrs) {
                super(c, attrs);
            }
    
            public LayoutParams(int width, int height) {
                super(width, height);
            }
    
            public LayoutParams(ViewGroup.LayoutParams source) {
                super(source);
            }
    
            public LayoutParams(LayoutParams source) {
                super(source);
                this.left = source.left;
                this.top = source.top;
                this.right = source.right;
                this.bottom = source.bottom;
            }
    
            public void clear() {
                this.left = 0;
                this.top = 0;
                this.right = 0;
                this.bottom = 0;
            }
    
            @Override
            public String toString() {
                return "LayoutParams{" +
                        "left=" + left +
                        ", top=" + top +
                        ", right=" + right +
                        ", bottom=" + bottom +
                        '}';
            }
        }
    }
    

    5.组合view之微信聊天界面

    最终效果
    实现思想
    首先整个聊天界面通过系统的ListView实现,通过组合控件实现聊天气泡和输入框,全部代码可以划分为三个组合控件,WeChatMsgList、WeChatMsgInput、WeChatBubble
    最终代码

    • WeChatMsgList
    
    
    • WeChatMsgInput
    
    
    • WeChatBubble
    
    

    参考地址:
    Canvas绘图
    Android 画笔Paint
    官方api Canvas

    相关文章

      网友评论

          本文标题:android view的绘制

          本文链接:https://www.haomeiwen.com/subject/tweyqxtx.html