自定义View全解

自定义View的分类
  1. 继承View重写onDraw

常用于绘制不规则的图片。这种方式需要自己实现支持wrap_content和padding

  1. 继承ViewGroup实现特殊的Layout

这种方法主要用于实现自定义布局,即除了LinearLayout,RelativeLayout等系统布局之外的一种重新定义的全新的布局,当某种效果很像
几种View组合在一起的时候就可以采用这种方法。
这种方法稍微复杂一些,需要合适的处理ViewGroup的测量和布局这俩个过程

  1. 继承特殊的View比如TextView

这种方法一般用于扩展某种已有的View功能。
这种方法不需要自己支持wrap_content,padding等。

  1. 继承特定的ViewGroup(比如LinearLayout等

某种效果很像几种View组合在一起的时候就可以采用这种方法。这种方法不需要自己处理ViewGroup的测量和布局这俩个过程。
与1.2比较,一般来说,1.2能实现的效果1.4也都能实现,但1.2更接近View的底层。

自定义View注意事项
  1. wrap_content

    直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理的话,wrap_content是失效的,这是因为在view的measure中默认实现 AT_MOST和EXACTLY俩种测量模式的效果是相同的

    解决方法:位View指定一个默认的宽高,并在wrap_content时即测量模式是AT_MOST时设置此宽高即可;

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int mWidth=600;
            int mHeight =600;
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
            if(widthMode==MeasureSpec.AT_MOST && heightMode==MeasureSpec.AT_MOST){
                setMeasuredDimension(Math.min(mWidth, widthSize),Math.min(mHeight, heightSize));
            }else if(widthMode==MeasureSpec.AT_MOST){
                setMeasuredDimension(Math.min(mWidth, widthSize),heightSize);
            }else if( heightMode==MeasureSpec.AT_MOST){
                setMeasuredDimension(widthSize,Math.min(mWidth, widthSize));
            }
        }
    
  1. 让View支持padding

    直接继承View的控件,如果不再draw方法中处理padding,那么这个属性是无法起作用的。

    解决方法:在onDraw绘制时把padding考虑进去即可

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            final int paddingLeft = getPaddingLeft();
            final int paddingRight = getPaddingRight();
            final int paddingTop = getPaddingTop();
            final int paddingBottom = getPaddingBottom();
            int width = getWidth() - (paddingLeft + paddingRight);
            int height = getHeight() - (paddingTop + paddingBottom);
            int radius = Math.min(width, height) / 2;
            canvas.drawCircle(paddingLeft+width / 2, paddingTop+height / 2, radius, mPaint);
        }
    
  2. 不要在View中使用Handler

这是因为View内部本身就提供了post系列方法,完全可以替代Handler的作用。除非你很明确要用Handler来发送消息。

  1. View中如果有线程和动画,及时停止

如果有线程和动画需要停止的时候,onDetachedFromWindow就恶意做到。这是因为当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法就会被调用。
相对的,当包含此View的Activity启动时onAttachedToWindow会被调用。同时,View不可见时,我们也需要停止线程和动画,如果不及时停止,可能会导致内存泄漏。

  1. 如果有滑动嵌套时,当然要处理好滑动冲突的问题

继承View重写 onDraw 方法

3.1 代码
public class CircleView extends View {
    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    public CircleView(Context context) {
        this(context, null);
    }
    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
        mPaint.setColor(mColor);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(width / 2, height / 2, radius, mPaint);
    }
}

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.peak.app.c_view.CustomViewMainActivity">
    <com.peak.app.c_view.CircleView
        android:layout_width="120dip"
        android:layout_height="120dip"
        android:padding="40dip"
        android:background="@color/colorPrimaryDark" />
</LinearLayout>

上述代码就完成了基本的自定义View,同样上述1.1,2.2,2.2中提到过,我们需要自己实现使其支持wrap_content和padding,现在我们就用上述代码来验证当前自定义view是否支持wrap_content和padding

3.2 代码验证当前自定义View是否支持wrap_content和padding
QQ截图20160621120732.png
QQ截图20160621120956.png

图一:证明支持layout_margin属性
图二:证明不支持padding属性
图三、图五:证明不支持wrap_content

layout_margin属性是由父容器控制的,所以支持,不需要我们处理

3.3让View支持wrap_content

之所以wrap_content失效,是因为:如果View在使用wrap_content时,那么他的specMode是AT_MOST模式,这种模式下它的宽高等于specSize,而这种情况下specSize是parentSize,而parentSize是父容器当前剩余的大小。
解决方法就是:我们为View指定一个默认的宽高,并在wrap_content时使用此宽高即可。当然这个默认值如何取舍,没有一个固定的依据,我们只能根据需求自行选取一个合适的值。下面的代码是通过Math.min函数来设置默认值的,道理一样。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int mWidth=600;
        int mHeight =600;
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if(widthMode==MeasureSpec.AT_MOST && heightMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(Math.min(mWidth, widthSize),Math.min(mHeight, heightSize));
        }else if(widthMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(Math.min(mWidth, widthSize),heightSize);
        }else if( heightMode==MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize,Math.min(mWidth, widthSize));
        }
    }

测试修改后的View:

        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
QQ截图20160621135301.png
3.4让View支持padding属性。

这个只需要在绘制的时候把padding考虑进去即可。
首先在onDraw方法中通过

int width = getWidth() - (paddingLeft + paddingRight);
int height = getHeight() - (paddingTop + paddingBottom);

重新计算圆的实际宽高,
然后,通过

canvas.drawCircle(paddingLeft+width / 2, paddingTop+height / 2, radius, mPaint);

重新定义圆的圆点,使其四周留下空白(也就是padding)
全部代码如下:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();
        int width = getWidth() - (paddingLeft + paddingRight);
        int height = getHeight() - (paddingTop + paddingBottom);
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(paddingLeft+width / 2, paddingTop+height / 2, radius, mPaint);
    }

    <com.peak.app.c_view.CircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark"
        android:padding="10dip"/>

继承特定的ViewGroup(比如LinearLayout等)

这种方式主要用于实现自定义布局,实现起来也比较简单,直接上代码:

ActionBarView.java:
public class ActionBarView extends LinearLayout {
    public static final int KEY_BACK = 111;
    public static final int KEY_CONTENT = 222;
    private Context context = null;
    private ImageView iv_back;
    private TextView tv_context;
    private OnClickListener onClickListener = null;
    public ActionBarView(Context context) {
        this(context, null);
    }
    public ActionBarView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        View contentView = LayoutInflater.from(context).inflate(R.layout.actionbar_layout, this);
        iv_back = (ImageView) contentView.findViewById(R.id.iv_back);
        tv_context = (TextView) contentView.findViewById(R.id.tv_context);
        iv_back.setOnClickListener(new MyOnClickListener(KEY_BACK));
        tv_context.setOnClickListener(new MyOnClickListener(KEY_CONTENT));
    }
    // 通过这个方法来动态设置标题
    public void setContent(String str) {
        tv_context.setText(str);
    }
    // 通过回调接口在外部监听点击事件
    private class MyOnClickListener implements android.view.View.OnClickListener {
        private int key = -1;
        private MyOnClickListener(int position) {
            this.key = position;
        }
        @Override
        public void onClick(View v) {
            if (onClickListener != null) {
                onClickListener.onClick(key, v);
            }
        }
    }
    public void setOnClickListener(OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }
    public interface OnClickListener {
        public void onClick(int key, View v);
    }
}

actionbar_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="44dip"
    android:background="@drawable/bg_top">
    <ImageView
        android:id="@+id/iv_back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_top_return" />
    <TextView
        android:id="@+id/tv_context"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="center_horizontal"
        android:gravity="center"
        android:text="标题" />
</FrameLayout>

activity的xml文件中引用:
    <com.peak.app.c_view.ActionBarView
        android:id="@+id/barView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="20dip"
        android:padding="20dip"
        android:background="@color/colorPrimary"/>

activity中:
        ActionBarView barView = (ActionBarView) findViewById(R.id.barView);
        barView.setContent("自定义View标题");
        barView.setOnClickListener(new ActionBarView.OnClickListener() {
            @Override
            public void onClick(int key, View v) {
                if (key == ActionBarView.KEY_BACK) {
                    Log.i("CustomViewMainActivity", "ActionBarView 点击返回按钮");
                }
                if (key == ActionBarView.KEY_CONTENT) {
                    Log.i("CustomViewMainActivity", "ActionBarView 点击标题");
                }
            }
        });
QQ截图20160621144146.png

图中,蓝色区域是整个ActionBarView 所占的区域,蓝色区域外是的空白是父布局设置的padding,蓝色区域和其围着的白色区域之间的距离是ActionBarView 设置的padding。所以,这种方式会自动支持padding属性,不需要我们自己处理。

代码中通过public void setContent(String str)方法来动态设置标题,其实我们还可以在xml中来设置,不过这需要我们自定义属性,这个稍后来说。

继承ViewGroup派生特殊的Layout

这种方式使用起来比较复杂,而且也不是很常用,这里就先不说了。

继承特定的View

这里我们继承EditText,实现全部删除的功能,【源码见CSDN】

自定义属性

有时候,业务需要,或者是单纯的为了使用方便,我们需要自定义属性,使其可以在xml中快速配置属性。比如CircleView中我们可以配置背景色的属性,ActionBarView我们可以配置标题属性。
这里我们以CircleView为例(ActionBarView的自定义属性见【源码】,这里就不往出贴了):
第一步:在res/values/下创建atts开头的自定义属性xml,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color" />
    </declare-styleable>
</resources>

上述代码首先定义了一个名称为CircleView的属性集合,当然,既然是集合,就可以有多个属性。这里的属性名是circle_color,格式是color
第二步:在View的构造方法中解析自定义属性的值并做处理,代码如下:

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
//        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CircleView, defStyleAttr, 0);
//        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView, defStyleAttr, 0);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.YELLOW);
        a.recycle();
        mPaint.setColor(mColor);
    }

**第三步:在xml布局中使用自定义属性,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.peak.app.c_view.CustomViewMainActivity">
    <com.peak.app.c_view.CircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark"
        android:padding="10dip"
        app:circle_color="@color/colorAccent" />
</LinearLayout>

这里需要注意:必须在布局文件中添加schemas声明:

xmlns:app="http://schemas.android.com/apk/res-auto"。
```其中,app是自定义属性的前缀,与下边的```app:circle_color="@color/colorAccent"```呼应。当然这个前缀是可以改的,是可以自定义的。

另外自定义属性有如下格式:
> 1. reference:指资源ID<attr name = "background" format = "reference" />
2. color:颜色值。<attr name = "textColor" format = "color" />
3. boolean:布尔值<attr name = "focusable" format = "boolean" />
4. dimension:尺寸值。<attr name = "layout_width" format = "dimension" />
5. float:浮点值。<attr name = "fromAlpha" format = "float" />
6. integer:整型值<attr name = "frameDuration" format="integer" />
7. string:字符串。<attr name = "text" format = "string" />
8. fraction:百分数<attr name = "pivotX" format = "fraction" /><attr name = "pivotY" format = "fraction" />
9. enum:枚举值
<attr name="orientation"><enum name="horizontal" value="0" /><enum name="vertical" value="1" /></attr>
10. flag:位或运算<attr name="windowSoftInputMode"><flag name = "stateUnspecified" value = "0" /><flag name = "stateUnchanged" value = "1" /></attr>
11. 多类型。<attr name = "background" format = "reference|color" />



最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,565评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,021评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,003评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,015评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,020评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,856评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,178评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,824评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,264评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,788评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,913评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,535评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,130评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,102评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,334评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,298评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,622评论 2 343

推荐阅读更多精彩内容