精华内容
参与话题
问答
  • Android 自定义View (一)

    万次阅读 多人点赞 2014-04-21 15:20:04
    很多的Android入门程序猿来说对于Android自定义View,可能都是比较恐惧的,但是这又是高手进阶的必经之路,所有准备在自定义View上面花一些功夫,多写一些文章。先总结下自定义View的步骤: 1、自定义View的属性 2、...

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24252901

    很多的Android入门程序猿来说对于Android自定义View,可能都是比较恐惧的,但是这又是高手进阶的必经之路,所有准备在自定义View上面花一些功夫,多写一些文章。先总结下自定义View的步骤:

    1、自定义View的属性

    2、在View的构造方法中获得我们自定义的属性

    [ 3、重写onMesure ]

    4、重写onDraw

    我把3用[]标出了,所以说3不一定是必须的,当然了大部分情况下还是需要重写的。

    1、自定义View的属性,首先在res/values/  下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <attr name="titleText" format="string" />
        <attr name="titleTextColor" format="color" />
        <attr name="titleTextSize" format="dimension" />
    
        <declare-styleable name="CustomTitleView">
            <attr name="titleText" />
            <attr name="titleTextColor" />
            <attr name="titleTextSize" />
        </declare-styleable>
    
    </resources>
    我们定义了字体,字体颜色,字体大小3个属性,format是值该属性的取值类型:

    一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag;不清楚的可以google一把。

    然后在布局中声明我们的自定义View

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
        <com.example.customview01.view.CustomTitleView
            android:layout_width="200dp"
            android:layout_height="100dp"
            custom:titleText="3712"
            custom:titleTextColor="#ff0000"
            custom:titleTextSize="40sp" />
    
    </RelativeLayout>

    一定要引入 xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01"我们的命名空间,后面的包路径指的是项目的package

    2、在View的构造方法中,获得我们的自定义的样式

    /**
    	 * 文本
    	 */
    	private String mTitleText;
    	/**
    	 * 文本的颜色
    	 */
    	private int mTitleTextColor;
    	/**
    	 * 文本的大小
    	 */
    	private int mTitleTextSize;
    
    	/**
    	 * 绘制时控制文本绘制的范围
    	 */
    	private Rect mBound;
    	private Paint mPaint;
    
    	public CustomTitleView(Context context, AttributeSet attrs)
    	{
    		this(context, attrs, 0);
    	}
    
    	public CustomTitleView(Context context)
    	{
    		this(context, null);
    	}
    
    	/**
    	 * 获得我自定义的样式属性
    	 * 
    	 * @param context
    	 * @param attrs
    	 * @param defStyle
    	 */
    	public CustomTitleView(Context context, AttributeSet attrs, int defStyle)
    	{
    		super(context, attrs, defStyle);
    		/**
    		 * 获得我们所定义的自定义样式属性
    		 */
    		TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTitleView, defStyle, 0);
    		int n = a.getIndexCount();
    		for (int i = 0; i < n; i++)
    		{
    			int attr = a.getIndex(i);
    			switch (attr)
    			{
    			case R.styleable.CustomTitleView_titleText:
    				mTitleText = a.getString(attr);
    				break;
    			case R.styleable.CustomTitleView_titleTextColor:
    				// 默认颜色设置为黑色
    				mTitleTextColor = a.getColor(attr, Color.BLACK);
    				break;
    			case R.styleable.CustomTitleView_titleTextSize:
    				// 默认设置为16sp,TypeValue也可以把sp转化为px
    				mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
    						TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
    				break;
    
    			}
    
    		}
    		a.recycle();
    
    		/**
    		 * 获得绘制文本的宽和高
    		 */
    		mPaint = new Paint();
    		mPaint.setTextSize(mTitleTextSize);
    		// mPaint.setColor(mTitleTextColor);
    		mBound = new Rect();
    		mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);
    
    	}

    我们重写了3个构造方法,默认的布局文件调用的是两个参数的构造方法,所以记得让所有的构造调用我们的三个参数的构造,我们在三个参数的构造中获得自定义属性。

    3、我们重写onDraw,onMesure调用系统提供的:

    @Override
    	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    	{
    		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    	}
    
    	@Override
    	protected void onDraw(Canvas canvas)
    	{
    		mPaint.setColor(Color.YELLOW);
    		canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
    
    		mPaint.setColor(mTitleTextColor);
    		canvas.drawText(mTitleText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaint);
    	}
    此时的效果是:

    是不是觉得还不错,基本已经实现了自定义View。但是此时如果我们把布局文件的宽和高写成wrap_content,会发现效果并不是我们的预期:


    系统帮我们测量的高度和宽度都是MATCH_PARNET,当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。

    所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法”:

    重写之前先了解MeasureSpec的specMode,一共三种类型:

    EXACTLY:一般是设置了明确的值或者是MATCH_PARENT

    AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT

    UNSPECIFIED:表示子布局想要多大就多大,很少使用

    下面是我们重写onMeasure代码:

    	@Override
    	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    	{
    		int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    		int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    		int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    		int width;
    		int height ;
    		if (widthMode == MeasureSpec.EXACTLY)
    		{
    			width = widthSize;
    		} else
    		{
    			mPaint.setTextSize(mTitleTextSize);
    			mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
    			float textWidth = mBounds.width();
    			int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());
    			width = desired;
    		}
    
    		if (heightMode == MeasureSpec.EXACTLY)
    		{
    			height = heightSize;
    		} else
    		{
    			mPaint.setTextSize(mTitleTextSize);
    			mPaint.getTextBounds(mTitle, 0, mTitle.length(), mBounds);
    			float textHeight = mBounds.height();
    			int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
    			height = desired;
    		}
    		
    		
    
    		setMeasuredDimension(width, height);
    	}
    

    现在我们修改下布局文件:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
        <com.example.customview01.view.CustomTitleView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            custom:titleText="3712"
            android:padding="10dp"
            custom:titleTextColor="#ff0000"
            android:layout_centerInParent="true"
            custom:titleTextSize="40sp" />
    
    </RelativeLayout>

    现在的效果是:


    完全复合我们的预期,现在我们可以对高度、宽度进行随便的设置了,基本可以满足我们的需求。

    当然了,这样下来我们这个自定义View与TextView相比岂不是没什么优势,所有我们觉得给自定义View添加一个事件:

    在构造中添加:

    this.setOnClickListener(new OnClickListener()
    		{
    
    			@Override
    			public void onClick(View v)
    			{
    				mTitleText = randomText();
    				postInvalidate();
    			}
    
    		});

    private String randomText()
    	{
    		Random random = new Random();
    		Set<Integer> set = new HashSet<Integer>();
    		while (set.size() < 4)
    		{
    			int randomInt = random.nextInt(10);
    			set.add(randomInt);
    		}
    		StringBuffer sb = new StringBuffer();
    		for (Integer i : set)
    		{
    			sb.append("" + i);
    		}
    
    		return sb.toString();
    	}

    下面再来运行:


    我们添加了一个点击事件,每次让它随机生成一个4位的随机数,有兴趣的可以在onDraw中添加一点噪点,然后改写为验证码,是不是感觉很不错。


    好了,各位学习的,打酱油的留个言,顶个呗~


    源码点击此处下载






    展开全文
  • 自定义View

    千次阅读 多人点赞 2018-04-09 19:45:42
    自定义View基础篇安卓自定义View基础 - 坐标系安卓自定义View基础 - 角度弧度安卓自定义View基础 - 颜色进阶篇安卓自定义View进阶 - 分类和流程安卓自定义View进阶 - 绘制基本图形安卓自定义View进阶 - 画布操作安卓...

    自定义View


    雕虫晓技


    教程类


    Markdown


    Tips


    速查表


    混沌水晶

    混沌水晶本身并没有太大功效,但与其他物品合成之后可能产生质的变化。


    开源库


    源码解析

    传送门

    通往异世界的传送门,请谨慎使用。

    时空折跃准备完毕,点击开始传送


    博文修复计划

    由于自己的知识水平有限,书写的文章难免会出现一些问题。

    随着知识增长和知识面的扩大,发现了一些之前未曾注意到的问题,故有此博文修复计划,本次修复不仅会修复之前文章中的瑕疵和纰漏,甚至会对文章的知识结构稍作调整,所有修复的文章和调整后的内容都会在微博重新发布声明,点击下面关注我的微博可以第一时间了解到相关信息,另外据说关注我的微博会变帅哦。

    本次博文修复计划主要针对 个人博客 和 GitHub,由于博主精力有限以及某些平台自身限制,对于其他平台仅能修复部分内容。


    展开全文
  • 自定义android进度条,带有进度指示,项目详情:http://blog.csdn.net/xiaanming/article/details/10298163
  • Android 自定义View (二) 进阶

    万次阅读 多人点赞 2014-04-22 11:39:25
    继续自定义View之旅,前面已经介绍过一个自定义View的基础的例子,Android 自定义View (一),如果你还对自定义View不了解可以去看看。今天给大家带来一个稍微复杂点的例子。 自定义View显示一张图片,下面包含图片...

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24300125

    继续自定义View之旅,前面已经介绍过一个自定义View的基础的例子,Android 自定义View (一)如果你还对自定义View不了解可以去看看。今天给大家带来一个稍微复杂点的例子。

    自定义View显示一张图片,下面包含图片的文本介绍,类似相片介绍什么的,不过不重要,主要是学习自定义View的用法么。

    还记得上一篇讲的4个步骤么:

    1、自定义View的属性
    2、在View的构造方法中获得我们自定义的属性
    [ 3、重写onMesure ]
    4、重写onDraw

    直接切入正题:

    1、在res/values/attr.xml

    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    
        <attr name="titleText" format="string" />
        <attr name="titleTextSize" format="dimension" />
        <attr name="titleTextColor" format="color" />
        <attr name="image" format="reference" />
        <attr name="imageScaleType">
            <enum name="fillXY" value="0" />
            <enum name="center" value="1" />
        </attr>
    
        <declare-styleable name="CustomImageView">
            <attr name="titleText" />
            <attr name="titleTextSize" />
            <attr name="titleTextColor" />
            <attr name="image" />
            <attr name="imageScaleType" />
        </declare-styleable>
    
    </resources>

    2、在构造中获得我们的自定义属性:

    /**
    	 * 初始化所特有自定义类型
    	 * 
    	 * @param context
    	 * @param attrs
    	 * @param defStyle
    	 */
    	public CustomImageView(Context context, AttributeSet attrs, int defStyle)
    	{
    		super(context, attrs, defStyle);
    
    		TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomImageView, defStyle, 0);
    
    		int n = a.getIndexCount();
    
    		for (int i = 0; i < n; i++)
    		{
    			int attr = a.getIndex(i);
    
    			switch (attr)
    			{
    			case R.styleable.CustomImageView_image:
    				mImage = BitmapFactory.decodeResource(getResources(), a.getResourceId(attr, 0));
    				break;
    			case R.styleable.CustomImageView_imageScaleType:
    				mImageScale = a.getInt(attr, 0);
    				break;
    			case R.styleable.CustomImageView_titleText:
    				mTitle = a.getString(attr);
    				break;
    			case R.styleable.CustomImageView_titleTextColor:
    				mTextColor = a.getColor(attr, Color.BLACK);
    				break;
    			case R.styleable.CustomImageView_titleTextSize:
    				mTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
    						16, getResources().getDisplayMetrics()));
    				break;
    
    			}
    		}
    		a.recycle();
    		rect = new Rect();
    		mPaint = new Paint();
    		mTextBound = new Rect();
    		mPaint.setTextSize(mTextSize);
    		// 计算了描绘字体需要的范围
    		mPaint.getTextBounds(mTitle, 0, mTitle.length(), mTextBound);
    
    	}

    3、重写onMeasure

    	@Override
    	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    	{
    		// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    
    		/**
    		 * 设置宽度
    		 */
    		int specMode = MeasureSpec.getMode(widthMeasureSpec);
    		int specSize = MeasureSpec.getSize(widthMeasureSpec);
    
    		if (specMode == MeasureSpec.EXACTLY)// match_parent , accurate
    		{
    			Log.e("xxx", "EXACTLY");
    			mWidth = specSize;
    		} else
    		{
    			// 由图片决定的宽
    			int desireByImg = getPaddingLeft() + getPaddingRight() + mImage.getWidth();
    			// 由字体决定的宽
    			int desireByTitle = getPaddingLeft() + getPaddingRight() + mTextBound.width();
    
    			if (specMode == MeasureSpec.AT_MOST)// wrap_content
    			{
    				int desire = Math.max(desireByImg, desireByTitle);
    				mWidth = Math.min(desire, specSize);
    				Log.e("xxx", "AT_MOST");
    			}
    		}
    
    		/***
    		 * 设置高度
    		 */
    
    		specMode = MeasureSpec.getMode(heightMeasureSpec);
    		specSize = MeasureSpec.getSize(heightMeasureSpec);
    		if (specMode == MeasureSpec.EXACTLY)// match_parent , accurate
    		{
    			mHeight = specSize;
    		} else
    		{
    			int desire = getPaddingTop() + getPaddingBottom() + mImage.getHeight() + mTextBound.height();
    			if (specMode == MeasureSpec.AT_MOST)// wrap_content
    			{
    				mHeight = Math.min(desire, specSize);
    			}
    		}
    		setMeasuredDimension(mWidth, mHeight);
    
    	}

    4、重写onDraw

    @Override
    	protected void onDraw(Canvas canvas)
    	{
    		// super.onDraw(canvas);
    		/**
    		 * 边框
    		 */
    		mPaint.setStrokeWidth(4);
    		mPaint.setStyle(Paint.Style.STROKE);
    		mPaint.setColor(Color.CYAN);
    		canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
    
    		rect.left = getPaddingLeft();
    		rect.right = mWidth - getPaddingRight();
    		rect.top = getPaddingTop();
    		rect.bottom = mHeight - getPaddingBottom();
    
    		mPaint.setColor(mTextColor);
    		mPaint.setStyle(Style.FILL);
    		/**
    		 * 当前设置的宽度小于字体需要的宽度,将字体改为xxx...
    		 */
    		if (mTextBound.width() > mWidth)
    		{
    			TextPaint paint = new TextPaint(mPaint);
    			String msg = TextUtils.ellipsize(mTitle, paint, (float) mWidth - getPaddingLeft() - getPaddingRight(),
    					TextUtils.TruncateAt.END).toString();
    			canvas.drawText(msg, getPaddingLeft(), mHeight - getPaddingBottom(), mPaint);
    
    		} else
    		{
    			//正常情况,将字体居中
    			canvas.drawText(mTitle, mWidth / 2 - mTextBound.width() * 1.0f / 2, mHeight - getPaddingBottom(), mPaint);
    		}
    
    		//取消使用掉的快
    		rect.bottom -= mTextBound.height();
    
    		if (mImageScale == IMAGE_SCALE_FITXY)
    		{
    			canvas.drawBitmap(mImage, null, rect, mPaint);
    		} else
    		{
    			//计算居中的矩形范围
    			rect.left = mWidth / 2 - mImage.getWidth() / 2;
    			rect.right = mWidth / 2 + mImage.getWidth() / 2;
    			rect.top = (mHeight - mTextBound.height()) / 2 - mImage.getHeight() / 2;
    			rect.bottom = (mHeight - mTextBound.height()) / 2 + mImage.getHeight() / 2;
    
    			canvas.drawBitmap(mImage, null, rect, mPaint);
    		}
    
    	}

    代码,结合注释和第一篇View的使用,应该可以看懂,不明白的留言。下面我们引入我们的自定义View:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:zhy="http://schemas.android.com/apk/res/com.zhy.customview02"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        <com.zhy.customview02.view.CustomImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:padding="10dp"
            zhy:image="@drawable/ic_launcher"
            zhy:imageScaleType="center"
            zhy:titleText="hello andorid ! "
            zhy:titleTextColor="#ff0000"
            zhy:titleTextSize="30sp" />
    
        <com.zhy.customview02.view.CustomImageView
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:padding="10dp"
            zhy:image="@drawable/ic_launcher"
            zhy:imageScaleType="center"
            zhy:titleText="helloworldwelcome"
            zhy:titleTextColor="#00ff00"
            zhy:titleTextSize="20sp" />
    
        <com.zhy.customview02.view.CustomImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:padding="10dp"
            zhy:image="@drawable/lmj"
            zhy:imageScaleType="center"
            zhy:titleText="妹子~"
            zhy:titleTextColor="#ff0000"
            zhy:titleTextSize="12sp" />
    
    </LinearLayout>

    我特意让显示出现3中情况:

    1、字体的宽度大于图片,且View宽度设置为wrap_content

    2、View宽度设置为精确值,字体的长度大于此宽度

    3、图片的宽度大于字体,且View宽度设置为wrap_content

    看看显示效果:


    怎么样,对于这三种情况所展示的效果都还不错吧。


    好了,就到这里,各位看官,没事留个言,顶一个呗~


    源码点击下载




    展开全文
  • 不知不觉中,带你一步步深入了解View系列的文章已经写到第四篇了,回顾一下,我们一共学习了...现在前半部分的承诺已经如约兑现了,那么今天我就要来兑现后面部分的承诺,讲一讲自定义View的实现方法,同时这也是带

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/17357967


    不知不觉中,带你一步步深入了解View系列的文章已经写到第四篇了,回顾一下,我们一共学习了LayoutInflater的原理分析、视图的绘制流程、视图的状态及重绘等知识,算是把View中很多重要的知识点都涉及到了。如果你还没有看过我前面的几篇文章,建议先去阅读一下,多了解一些原理方面的东西。


    之前我有承诺过,会在View这个话题上多写几篇博客,讲一讲View的工作原理,以及自定义View的方法。现在前半部分的承诺已经如约兑现了,那么今天我就要来兑现后面部分的承诺,讲一讲自定义View的实现方法,同时这也是带你一步步深入了解View系列的完结篇。


    一些接触Android不久的朋友对自定义View都有一丝畏惧感,总感觉这是一个比较高级的技术,但其实自定义View并不复杂,有时候只需要简单几行代码就可以完成了。


    如果说要按类型来划分的话,自定义View的实现方式大概可以分为三种,自绘控件、组合控件、以及继承控件。那么下面我们就来依次学习一下,每种方式分别是如何自定义View的。


    一、自绘控件


    自绘控件的意思就是,这个View上所展现的内容全部都是我们自己绘制出来的。绘制的代码是写在onDraw()方法中的,而这部分内容我们已经在 Android视图绘制流程完全解析,带你一步步深入了解View(二) 中学习过了。


    下面我们准备来自定义一个计数器View,这个View可以响应用户的点击事件,并自动记录一共点击了多少次。新建一个CounterView继承自View,代码如下所示:

    public class CounterView extends View implements OnClickListener {
    
    	private Paint mPaint;
    	
    	private Rect mBounds;
    
    	private int mCount;
    	
    	public CounterView(Context context, AttributeSet attrs) {
    		super(context, attrs);
    		mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    		mBounds = new Rect();
    		setOnClickListener(this);
    	}
    
    	@Override
    	protected void onDraw(Canvas canvas) {
    		super.onDraw(canvas);
    		mPaint.setColor(Color.BLUE);
    		canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
    		mPaint.setColor(Color.YELLOW);
    		mPaint.setTextSize(30);
    		String text = String.valueOf(mCount);
    		mPaint.getTextBounds(text, 0, text.length(), mBounds);
    		float textWidth = mBounds.width();
    		float textHeight = mBounds.height();
    		canvas.drawText(text, getWidth() / 2 - textWidth / 2, getHeight() / 2
    				+ textHeight / 2, mPaint);
    	}
    
    	@Override
    	public void onClick(View v) {
    		mCount++;
    		invalidate();
    	}
    
    }

    可以看到,首先我们在CounterView的构造函数中初始化了一些数据,并给这个View的本身注册了点击事件,这样当CounterView被点击的时候,onClick()方法就会得到调用。而onClick()方法中的逻辑就更加简单了,只是对mCount这个计数器加1,然后调用invalidate()方法。通过 Android视图状态及重绘流程分析,带你一步步深入了解View(三) 这篇文章的学习我们都已经知道,调用invalidate()方法会导致视图进行重绘,因此onDraw()方法在稍后就将会得到调用。


    既然CounterView是一个自绘视图,那么最主要的逻辑当然就是写在onDraw()方法里的了,下面我们就来仔细看一下。这里首先是将Paint画笔设置为蓝色,然后调用Canvas的drawRect()方法绘制了一个矩形,这个矩形也就可以当作是CounterView的背景图吧。接着将画笔设置为黄色,准备在背景上面绘制当前的计数,注意这里先是调用了getTextBounds()方法来获取到文字的宽度和高度,然后调用了drawText()方法去进行绘制就可以了。


    这样,一个自定义的View就已经完成了,并且目前这个CounterView是具备自动计数功能的。那么剩下的问题就是如何让这个View在界面上显示出来了,其实这也非常简单,我们只需要像使用普通的控件一样来使用CounterView就可以了。比如在布局文件中加入如下代码:

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
        <com.example.customview.CounterView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_centerInParent="true" />
    
    </RelativeLayout>

    可以看到,这里我们将CounterView放入了一个RelativeLayout中,然后可以像使用普通控件来给CounterView指定各种属性,比如通过layout_width和layout_height来指定CounterView的宽高,通过android:layout_centerInParent来指定它在布局里居中显示。只不过需要注意,自定义的View在使用的时候一定要写出完整的包名,不然系统将无法找到这个View。


    好了,就是这么简单,接下来我们可以运行一下程序,并不停地点击CounterView,效果如下图所示。




    怎么样?是不是感觉自定义View也并不是什么高级的技术,简单几行代码就可以实现了。当然了,这个CounterView功能非常简陋,只有一个计数功能,因此只需几行代码就足够了,当你需要绘制比较复杂的View时,还是需要很多技巧的。


    二、组合控件


    组合控件的意思就是,我们并不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但我们可以将几个系统原生的控件组合到一起,这样创建出的控件就被称为组合控件。


    举个例子来说,标题栏就是个很常见的组合控件,很多界面的头部都会放置一个标题栏,标题栏上会有个返回按钮和标题,点击按钮后就可以返回到上一个界面。那么下面我们就来尝试去实现这样一个标题栏控件。


    新建一个title.xml布局文件,代码如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#ffcb05" >
    
        <Button
            android:id="@+id/button_left"
            android:layout_width="60dp"
            android:layout_height="40dp"
            android:layout_centerVertical="true"
            android:layout_marginLeft="5dp"
            android:background="@drawable/back_button"
            android:text="Back"
            android:textColor="#fff" />
    
        <TextView
            android:id="@+id/title_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="This is Title"
            android:textColor="#fff"
            android:textSize="20sp" />
    
    </RelativeLayout>

    在这个布局文件中,我们首先定义了一个RelativeLayout作为背景布局,然后在这个布局里定义了一个Button和一个TextView,Button就是标题栏中的返回按钮,TextView就是标题栏中的显示的文字。


    接下来创建一个TitleView继承自FrameLayout,代码如下所示:

    public class TitleView extends FrameLayout {
    
    	private Button leftButton;
    
    	private TextView titleText;
    
    	public TitleView(Context context, AttributeSet attrs) {
    		super(context, attrs);
    		LayoutInflater.from(context).inflate(R.layout.title, this);
    		titleText = (TextView) findViewById(R.id.title_text);
    		leftButton = (Button) findViewById(R.id.button_left);
    		leftButton.setOnClickListener(new OnClickListener() {
    			@Override
    			public void onClick(View v) {
    				((Activity) getContext()).finish();
    			}
    		});
    	}
    
    	public void setTitleText(String text) {
    		titleText.setText(text);
    	}
    
    	public void setLeftButtonText(String text) {
    		leftButton.setText(text);
    	}
    
    	public void setLeftButtonListener(OnClickListener l) {
    		leftButton.setOnClickListener(l);
    	}
    
    }

    TitleView中的代码非常简单,在TitleView的构建方法中,我们调用了LayoutInflater的inflate()方法来加载刚刚定义的title.xml布局,这部分内容我们已经在 Android LayoutInflater原理分析,带你一步步深入了解View(一) 这篇文章中学习过了。


    接下来调用findViewById()方法获取到了返回按钮的实例,然后在它的onClick事件中调用finish()方法来关闭当前的Activity,也就相当于实现返回功能了。


    另外,为了让TitleView有更强地扩展性,我们还提供了setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法,分别用于设置标题栏上的文字、返回按钮上的文字、以及返回按钮的点击事件。


    到了这里,一个自定义的标题栏就完成了,那么下面又到了如何引用这个自定义View的部分,其实方法基本都是相同的,在布局文件中添加如下代码:

    <RelativeLayout 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" >
    
        <com.example.customview.TitleView
            android:id="@+id/title_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" >
        </com.example.customview.TitleView>
    
    </RelativeLayout>

    这样就成功将一个标题栏控件引入到布局文件中了,运行一下程序,效果如下图所示:




    现在点击一下Back按钮,就可以关闭当前的Activity了。如果你想要修改标题栏上显示的内容,或者返回按钮的默认事件,只需要在Activity中通过findViewById()方法得到TitleView的实例,然后调用setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法进行设置就OK了。


    三、继承控件


    继承控件的意思就是,我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。这种自定义控件的特点就是不仅能够按照我们的需求加入相应的功能,还可以保留原生控件的所有功能,比如 Android PowerImageView实现,可以播放动画的强大ImageView 这篇文章中介绍的PowerImageView就是一个典型的继承控件。


    为了能够加深大家对这种自定义View方式的理解,下面我们再来编写一个新的继承控件。ListView相信每一个Android程序员都一定使用过,这次我们准备对ListView进行扩展,加入在ListView上滑动就可以显示出一个删除按钮,点击按钮就会删除相应数据的功能。


    首先需要准备一个删除按钮的布局,新建delete_button.xml文件,代码如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <Button xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/delete_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/delete_button" >
    
    </Button>

    这个布局文件很简单,只有一个按钮而已,并且我们给这个按钮指定了一张删除背景图。


    接着创建MyListView继承自ListView,这就是我们自定义的View了,代码如下所示:

    public class MyListView extends ListView implements OnTouchListener,
    		OnGestureListener {
    
    	private GestureDetector gestureDetector;
    
    	private OnDeleteListener listener;
    
    	private View deleteButton;
    
    	private ViewGroup itemLayout;
    
    	private int selectedItem;
    
    	private boolean isDeleteShown;
    
    	public MyListView(Context context, AttributeSet attrs) {
    		super(context, attrs);
    		gestureDetector = new GestureDetector(getContext(), this);
    		setOnTouchListener(this);
    	}
    
    	public void setOnDeleteListener(OnDeleteListener l) {
    		listener = l;
    	}
    
    	@Override
    	public boolean onTouch(View v, MotionEvent event) {
    		if (isDeleteShown) {
    			itemLayout.removeView(deleteButton);
    			deleteButton = null;
    			isDeleteShown = false;
    			return false;
    		} else {
    			return gestureDetector.onTouchEvent(event);
    		}
    	}
    
    	@Override
    	public boolean onDown(MotionEvent e) {
    		if (!isDeleteShown) {
    			selectedItem = pointToPosition((int) e.getX(), (int) e.getY());
    		}
    		return false;
    	}
    
    	@Override
    	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
    			float velocityY) {
    		if (!isDeleteShown && Math.abs(velocityX) > Math.abs(velocityY)) {
    			deleteButton = LayoutInflater.from(getContext()).inflate(
    					R.layout.delete_button, null);
    			deleteButton.setOnClickListener(new OnClickListener() {
    				@Override
    				public void onClick(View v) {
    					itemLayout.removeView(deleteButton);
    					deleteButton = null;
    					isDeleteShown = false;
    					listener.onDelete(selectedItem);
    				}
    			});
    			itemLayout = (ViewGroup) getChildAt(selectedItem
    					- getFirstVisiblePosition());
    			RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
    					LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    			params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
    			params.addRule(RelativeLayout.CENTER_VERTICAL);
    			itemLayout.addView(deleteButton, params);
    			isDeleteShown = true;
    		}
    		return false;
    	}
    
    	@Override
    	public boolean onSingleTapUp(MotionEvent e) {
    		return false;
    	}
    
    	@Override
    	public void onShowPress(MotionEvent e) {
    
    	}
    
    	@Override
    	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
    			float distanceY) {
    		return false;
    	}
    
    	@Override
    	public void onLongPress(MotionEvent e) {
    	}
    	
    	public interface OnDeleteListener {
    
    		void onDelete(int index);
    
    	}
    
    }

    由于代码逻辑比较简单,我就没有加注释。这里在MyListView的构造方法中创建了一个GestureDetector的实例用于监听手势,然后给MyListView注册了touch监听事件。然后在onTouch()方法中进行判断,如果删除按钮已经显示了,就将它移除掉,如果删除按钮没有显示,就使用GestureDetector来处理当前手势。


    当手指按下时,会调用OnGestureListener的onDown()方法,在这里通过pointToPosition()方法来判断出当前选中的是ListView的哪一行。当手指快速滑动时,会调用onFling()方法,在这里会去加载delete_button.xml这个布局,然后将删除按钮添加到当前选中的那一行item上。注意,我们还给删除按钮添加了一个点击事件,当点击了删除按钮时就会回调onDeleteListener的onDelete()方法,在回调方法中应该去处理具体的删除操作。


    好了,自定义View的功能到此就完成了,接下来我们需要看一下如何才能使用这个自定义View。首先需要创建一个ListView子项的布局文件,新建my_list_view_item.xml,代码如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:descendantFocusability="blocksDescendants"
        android:orientation="vertical" >
    
        <TextView
            android:id="@+id/text_view"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:layout_centerVertical="true"
            android:gravity="left|center_vertical"
            android:textColor="#000" />
    
    </RelativeLayout>
    然后创建一个适配器MyAdapter,在这个适配器中去加载my_list_view_item布局,代码如下所示:
    public class MyAdapter extends ArrayAdapter<String> {
    
    	public MyAdapter(Context context, int textViewResourceId, List<String> objects) {
    		super(context, textViewResourceId, objects);
    	}
    
    	@Override
    	public View getView(int position, View convertView, ViewGroup parent) {
    		View view;
    		if (convertView == null) {
    			view = LayoutInflater.from(getContext()).inflate(R.layout.my_list_view_item, null);
    		} else {
    			view = convertView;
    		}
    		TextView textView = (TextView) view.findViewById(R.id.text_view);
    		textView.setText(getItem(position));
    		return view;
    	}
    
    }
    到这里就基本已经完工了,下面在程序的主布局文件里面引入MyListView这个控件,如下所示:
    <RelativeLayout 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" >
    
        <com.example.customview.MyListView
            android:id="@+id/my_list_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" >
        </com.example.customview.MyListView>
    
    </RelativeLayout>
    最后在Activity中初始化MyListView中的数据,并处理了onDelete()方法的删除逻辑,代码如下所示:
    public class MainActivity extends Activity {
    
    	private MyListView myListView;
    
    	private MyAdapter adapter;
    
    	private List<String> contentList = new ArrayList<String>();
    
    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
    		super.onCreate(savedInstanceState);
    		requestWindowFeature(Window.FEATURE_NO_TITLE);
    		setContentView(R.layout.activity_main);
    		initList();
    		myListView = (MyListView) findViewById(R.id.my_list_view);
    		myListView.setOnDeleteListener(new OnDeleteListener() {
    			@Override
    			public void onDelete(int index) {
    				contentList.remove(index);
    				adapter.notifyDataSetChanged();
    			}
    		});
    		adapter = new MyAdapter(this, 0, contentList);
    		myListView.setAdapter(adapter);
    	}
    
    	private void initList() {
    		contentList.add("Content Item 1");
    		contentList.add("Content Item 2");
    		contentList.add("Content Item 3");
    		contentList.add("Content Item 4");
    		contentList.add("Content Item 5");
    		contentList.add("Content Item 6");
    		contentList.add("Content Item 7");
    		contentList.add("Content Item 8");
    		contentList.add("Content Item 9");
    		contentList.add("Content Item 10");
    		contentList.add("Content Item 11");
    		contentList.add("Content Item 12");
    		contentList.add("Content Item 13");
    		contentList.add("Content Item 14");
    		contentList.add("Content Item 15");
    		contentList.add("Content Item 16");
    		contentList.add("Content Item 17");
    		contentList.add("Content Item 18");
    		contentList.add("Content Item 19");
    		contentList.add("Content Item 20");
    	}
    
    }

    这样就把整个例子的代码都完成了,现在运行一下程序,会看到MyListView可以像ListView一样,正常显示所有的数据,但是当你用手指在MyListView的某一行上快速滑动时,就会有一个删除按钮显示出来,如下图所示:



    点击一下删除按钮就可以将第6行的数据删除了。此时的MyListView不仅保留了ListView原生的所有功能,还增加了一个滑动进行删除的功能,确实是一个不折不扣的继承控件。

    到了这里,我们就把自定义View的几种实现方法全部讲完了,虽然每个例子都很简单,但是万变不离其宗,复杂的View也是由这些简单的原理堆积出来的。经过了四篇文章的学习,相信每个人对View的理解都已经较为深入了,那么带你一步步深入了解View系列的文章就到此结束,感谢大家有耐心看到最后。

    关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

    微信扫一扫下方二维码即可关注:

            

    展开全文
  • 自定义View基础 - 最易懂的自定义View原理系列(1)

    万次阅读 多人点赞 2017-02-20 11:27:07
    自定义View原理是Android开发者必须了解的基础; 在了解自定义View之前,你需要有一定的知识储备; 本文将全面解析关于自定义View中的所有知识基础。 目录1. View的分类视图View主要分为两类: 类别 解释 特点 ...
  • 网上有大量关于自定义View原理的文章,但存在一些问题:内容不全、思路不清晰、无源码分析、简单问题复杂化 等 今天,我将全面总结自定义View原理中的measure过程,我能保证这是市面上的最全面、最清晰、最易懂的 ...
  • 网上有大量关于自定义View原理的文章,但存在一些问题:内容不全、思路不清晰、无源码分析、简单问题复杂化 等 今天,我将全面总结自定义View原理中的Layout过程,我能保证这是市面上的最全面、最清晰、最易懂的 ...
  • 自定义view是一个综合的技术体系,本说明文档不去分析一个个具体的自定义view的实现,因为自定义view五花八门,是不可能全部分析一遍的。虽然我们不能把自定义view都分析一遍,但是我们能够提取出一种思想,而面对...
  • 网上有大量关于自定义View原理的文章,但存在一些问题:内容不全、思路不清晰、无源码分析、简单问题复杂化 等 今天,我将全面总结自定义View原理中的Draw过程,我能保证这是市面上的最全面、最清晰、最易懂的 ...
  • 我有现成的布局xml文件,现在想定义一个组合的自定义view,怎样把这个view的布局指定为一个xml文件
  • Android 自定义View (三) 圆环交替 等待效果

    万次阅读 多人点赞 2014-04-25 23:24:08
    一个朋友今天有这么个需求(下图),我觉得那自定义View来做还是很适合的,就做了下,顺便和大家分享下,对于自定义View多练没坏处么。如果你看了前两篇,那么这篇一定so easy 。 效果就这样,分析了一下,大概有...
  • 自定义View以及自定义属性

    千次阅读 2016-12-10 14:33:10
    Android自定义View以及自定义属性
  • 转载请标明出处: ... 本文出自:【openXu的博客...继承View重写onDraw方法 自定义属性 onMeasure方法 MeasureSpec 分析为什么我们自定义的MyTextView设置了wrap_content却填充屏幕 重写onMeasure方法 4自动换行 源码下
  • MainActivity如下: package cc.testcompounddrawables; import android.app.Activity; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.widget.TextView;...
  • 自定义view入门

    千次阅读 2016-01-16 21:21:02
    如何自定义控件主要分为以下几个步骤: 1、自定义属性的声明与获取 (1)分析需要的自定义属性 ...(4)在view的构造方法中进行获取 2、测量onMewsure 3、布局onLayout(viewgroup) 4、绘制onDraw 5、onTouchEven
  • 自定义View入门

    千次阅读 2017-07-10 14:31:56
    自定义View类增加属性(两种方式) 绘制控件(导入布局) 响应用户事件 定义回调函数(根据自己需求来选择) 二、哪些方法需要被重写 onDraw() view中onDraw()是个空函数,也就是说具体的视图
  • Android 自定义View

    千次阅读 2014-08-20 23:36:00
    Android 自定义View
  • 转载请标明出处: ...在上一篇博客《Android自定义View(一、初体验)》中我们体验了自定义控件的基本流程: 继承View,覆盖构造方法 自定义属性 重写onMeasure方法测量宽高 重写onDraw方法
  • 觉得博文有用,请点赞,请评论,请关注,谢谢!~ 老规矩,先上效果图,看个效果,如果符合你的项目或者确定你要了解的内容,再往下看吧: MainActivity.java: ... import android.support.v7.app.AppCompatActivity;...
  • android中,在activity里调用自定义view的postInvalidate()和invalidate()方法,自定义view中并没有重新绘制,当然在自定义view本类中调用是可以实现重新绘制的。求大神指导,是我调用方法不对? 还是有其他重新绘制...
  • Android自定义View 自定义组合控件

    千次阅读 2014-11-13 23:09:28
    自定义组合控件: 以三国杀游戏武将为例,包括武将头像,血条,装备区 1.先定义该组合的XML文件布局 1 "1.0" encoding="utf-8"?> 2 "http://schemas.android.com/apk/res/android" 3 android:layout_width...

空空如也

1 2 3 4 5 ... 20
收藏数 53,952
精华内容 21,580
关键字:

自定义view