精华内容
下载资源
问答
  • 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 自定义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系列教程05--自定义View示例分析

    万次阅读 多人点赞 2016-05-31 22:06:40
    之前结合源码分析完了自定义View的三个阶段:measure,layout,draw。 那么,自定义有哪几种常见的方式呢? 直接继承自View 在使用该方式实现自定义View时通常的核心操作都在onDraw( )当中进行。但是,请注意,...

    版权声明

    • 本文原创作者:谷哥的小弟
    • 作者博客地址:http://blog.csdn.net/lfdfhl

    自定义View概述

    之前结合源码分析完了自定义View的三个阶段:measure,layout,draw。
    那么,自定义有哪几种常见的方式呢?

    1. 直接继承自View
      在使用该方式实现自定义View时通常的核心操作都在onDraw( )当中进行。但是,请注意,在分析measure部分源码的时候,我们提到如果直接继承自View在onMeasure( )中要处理view大小为wrap_content的情况,否则这种情况下的大小和match_parent一样。除此以为,还需要注意对于padding的处理。

    2. 继承自系统已有的View
      比如常见的TextView,Button等等。如果采用该方式,我们只需要在系统控件的基础上做出一些调整和扩展即可,而且也不需要去自己支持wrap_content和padding。

    3. 直接继承自ViewGroup
      如果使用该方式实现自定义View,请注意两个问题
      第一点:
      在onMeasure( )实现wrap_content的支持。这点和直接继承自View是一样的。
      第二点:
      在onMeasure( )和onLayout中需要处理自身的padding以及子View的margin

    4. 继承自系统已有的ViewGroup
      比如LinearLayout,RelativeLayout等等。如果采用该方式,那么在3中提到的两个问题就不用再过多考虑了,简便了许多。

    示例1

    瞅瞅第一个例子,效果如下图:

    这里写图片描述

    这里写图片描述
    对于该效果的主要描述如下:

    1. 点击Title部分,展开图片
    2. 再次点击Title,收缩图片
    3. 图片的收缩和展开都渐次进行的,并使用动画切换右侧箭头的方向。

    好了,效果已经看到了,我们来明确和拆解一下这个小功能

    1. 控件由数字,标题,箭头,图片四部分组成
    2. 点击标题逐渐地显示或隐藏图片
    3. 在图片的切换过程中伴随着箭头方向的改变

    弄清楚这些就该动手写代码了。
    先来看这个控件的布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#ffffff"
        android:orientation="vertical">
    
    
        <RelativeLayout
            android:id="@+id/titleRelativeLayout"
            android:padding="30px"
            android:layout_width="match_parent"
            android:layout_height="170px"
            android:clickable="true">
    
            <TextView
                android:id="@+id/numberTextView"
                android:layout_width="70px"
                android:layout_height="70px"
                android:gravity="center"
                android:layout_centerVertical="true"
                android:background="@drawable/circle_textview"
                android:clickable="false"
                android:text="1"
                android:textStyle="bold"
                android:textColor="#EBEFEC"
                android:textSize="35px" />
    
            <TextView
                android:id="@+id/titleTextView"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_toRightOf="@id/numberTextView"
                android:layout_marginLeft="30px"
                android:clickable="false"
                android:textColor="#1d953f"
                android:textSize="46px" />
    
    
            <ImageView
                android:id="@+id/arrowImageView"
                android:layout_width="48px"
                android:layout_height="27px"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:background="@drawable/btn_an_xxh"
                android:clickable="false"
                android:scaleType="fitCenter" />
        </RelativeLayout>
    
        <View
            android:layout_width="match_parent"
            android:layout_height="2px"
            android:layout_below="@id/titleRelativeLayout"
            android:background="#E7E7EF"
            android:clickable="false"
            />
    
        <RelativeLayout
            android:id="@+id/contentRelativeLayout"
            android:visibility="gone"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">
        </RelativeLayout>
    
    </LinearLayout>
    

    请注意,在此将显示图片的容器即contentRelativeLayout设置为gone。
    为什么要这么做呢?因为进入应用后是看不到图片部分的,只有点击后才可见。嗯哼,你大概已经猜到了:图片的隐藏和显示是通过改变容器的visibility实现的。是的!那图片的逐渐显示和隐藏还有箭头的旋转又是怎么做的呢?请看该控件的具体实现。

    package com.stay4it.testcollapseview;
    
    import android.content.Context;
    import android.text.TextUtils;
    import android.util.AttributeSet;
    import android.util.DisplayMetrics;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.WindowManager;
    import android.view.animation.Animation;
    import android.view.animation.Transformation;
    import android.widget.ImageView;
    import android.widget.LinearLayout;
    import android.widget.RelativeLayout;
    import android.widget.TextView;
    /**
     * 原创作者:
     * 谷哥的小弟
     *
     * 博客地址:
     * http://blog.csdn.net/lfdfhl
     */
    public class CollapseView extends LinearLayout {
        private long duration = 350;
        private Context mContext;
        private TextView mNumberTextView;
        private TextView mTitleTextView;
        private RelativeLayout mContentRelativeLayout;
        private RelativeLayout mTitleRelativeLayout;
        private ImageView mArrowImageView;
        int parentWidthMeasureSpec;
        int parentHeightMeasureSpec;
        public CollapseView(Context context) {
            this(context, null);
        }
    
        public CollapseView(Context context, AttributeSet attrs) {
            super(context, attrs);
            mContext=context;
            LayoutInflater.from(mContext).inflate(R.layout.collapse_layout, this);
            initView();
        }
    
    
        private void initView() {
            mNumberTextView=(TextView)findViewById(R.id.numberTextView);
            mTitleTextView =(TextView)findViewById(R.id.titleTextView);
            mTitleRelativeLayout= (RelativeLayout) findViewById(R.id.titleRelativeLayout);
            mContentRelativeLayout=(RelativeLayout)findViewById(R.id.contentRelativeLayout);
            mArrowImageView =(ImageView)findViewById(R.id.arrowImageView);
            mTitleRelativeLayout.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    rotateArrow();
                }
            });
    
            collapse(mContentRelativeLayout);
        }
    
    
        public void setNumber(String number){
            if(!TextUtils.isEmpty(number)){
                mNumberTextView.setText(number);
            }
        }
    
        public void setTitle(String title){
            if(!TextUtils.isEmpty(title)){
                mTitleTextView.setText(title);
            }
        }
    
        public void setContent(int resID){
            View view=LayoutInflater.from(mContext).inflate(resID,null);
            RelativeLayout.LayoutParams layoutParams=
                    new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
            view.setLayoutParams(layoutParams);
            mContentRelativeLayout.addView(view);
        }
    
    
        public void rotateArrow() {
            int degree = 0;
            if (mArrowImageView.getTag() == null || mArrowImageView.getTag().equals(true)) {
                mArrowImageView.setTag(false);
                degree = -180;
                expand(mContentRelativeLayout);
            } else {
                degree = 0;
                mArrowImageView.setTag(true);
                collapse(mContentRelativeLayout);
            }
            mArrowImageView.animate().setDuration(duration).rotation(degree);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            parentWidthMeasureSpec=widthMeasureSpec;
            parentHeightMeasureSpec=heightMeasureSpec;
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            super.onLayout(changed, l, t, r, b);
        }
    
        // 展开
        private void expand(final View view) {
            WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
            DisplayMetrics outMetrics = new DisplayMetrics();
            wm.getDefaultDisplay().getMetrics(outMetrics);
            view.measure(parentWidthMeasureSpec, parentHeightMeasureSpec);
            final int measuredWidth = view.getMeasuredWidth();
            final int measuredHeight = view.getMeasuredHeight();
            view.setVisibility(View.VISIBLE);
    
            Animation animation = new Animation() {
                @Override
                protected void applyTransformation(float interpolatedTime, Transformation t) {
                    if(interpolatedTime == 1){
                        view.getLayoutParams().height =measuredHeight;
                    }else{
                        view.getLayoutParams().height =(int) (measuredHeight * interpolatedTime);
                    }
                    view.requestLayout();
                }
    
    
                @Override
                public boolean willChangeBounds() {
                    return true;
                }
            };
            animation.setDuration(duration);
            view.startAnimation(animation);
        }
    
        // 折叠
        private void collapse(final View view) {
            final int measuredHeight = view.getMeasuredHeight();
            Animation animation = new Animation() {
                @Override
                protected void applyTransformation(float interpolatedTime, Transformation t) {
                    if (interpolatedTime == 1) {
                        view.setVisibility(View.GONE);
                    } else {
                        view.getLayoutParams().height = measuredHeight - (int) (measuredHeight * interpolatedTime);
                        view.requestLayout();
                    }
                }
    
                @Override
                public boolean willChangeBounds() {
                    return true;
                }
            };
            animation.setDuration(duration);
            view.startAnimation(animation);
        }
    }
    
    

    现就该代码中的主要操作做一些分析和介绍。

    1. 控件提供setNumber()方法用于设置最左侧的数字,请参见代码第62-66行。
      比如你有三个女朋友,那么她们的编号分别就是1,2,3
    2. 控件提供setTitle()方法用于设置标题,请参见代码第68-72行
      比如,现女友,前女友,前前女友。
    3. 控件提供setContent()方法用于设置隐藏和显示的内容,请参见代码第74-80行。
      请注意,该方法的参数是一个布局文件的ID。所以,要显示和隐藏的东西只要写在一个布局文件中就行。这样就灵活多了,可以根据实际需求实现不同的布局就行。比如在这个例子中,我在一个布局文件中就只放了一个ImageView,然后将这个布局文件的ID传递给该方法就行。
    4. 实现Title部分Click监听,请参见代码第51-56行
      在监听到Click事件后显示或隐藏content部分
    5. 实现content部分的显示,请参见代码第110-138行
      在这遇到一个难题:
      这个content会占多大的空间呢?
      我猛地这么一问,大家可能有点懵圈。
      如果没有听懂或者回答不上来,我就先举个例子:
      小狗一秒钟跑1米(即小狗的速度为1m/s),请问小狗跑完这段路要多少时间?
      看到这个问题,是不是觉得挺脑残的,是不是有一种想抽我耳光的冲动?
      你他妹的,路程的长短都没有告诉我,我怎么知道小狗要跑多久?!真是日了狗了!

    嗯哼,是的。我们在这里根本不知道这个View(比如此处的content)有多高多宽,我们当然也不知道它要占多大的空间!!那怎么办呢?在这就按照最直接粗暴的方式来——遇到问题,解决问题!找出该View的宽和高!
    前面在分析View的measure阶段时我们知道这些控件的宽和高是由系统测量的,在此之后我们只需要利用getMeasuredWidth()和getMeasuredHeight()就行了。但是这个控件的visibility原本是GONE的,系统在measure阶段根本不会去测量它的宽和高,所以现在需要我们自己去手动测量。代码如下:

    view.measure(parentWidthMeasureSpec, parentHeightMeasureSpec);

    1. 获取到view的宽高后借助于动画实现content的渐次展开,请参见代码第119-137行。
      动画的interpolatedTime在一定时间内(duration)从0变化到1,所以

    measuredHeight * interpolatedTime

    表示了content的高从0到measuredHeight的逐次变化,在这个变化的过程中不断调用

    view.requestLayout();

    刷新界面,这样就达到了预想的效果。

    1. 实现content部分的隐藏,请参见代码第141-161行
      隐藏的过程和之前的逐次显示过程原理是一样的,不再赘述。

    2. 实现箭头的转向,请参见代码第83-95行
      这个比较简单,在此直接用属性动画(ViewPropertyAnimator)让箭头旋转

    示例小结:
    在该demo中主要采用了手动测量View的方式获取View的大小。

    示例2

    瞅瞅第二个例子,效果如下图:

    这里写图片描述
    嗯哼,这个流式布局(FlowLayout)大家可能见过,它常用来做一些标签的显示。比如,我要给我女朋友的照片加上描述,我就可以设置tag为:“贤良淑德”, “女神”, “年轻美貌”, “清纯”, "温柔贤惠"等等。而且在标签的显示过程中,如果这一行没有足够的空间显示下一个标签,那么会先自动换行然后再添加新的标签。
    好了,效果已经看到了,我们来瞅瞅它是怎么做的。

    package com.stay4it.testflowlayout;
    
    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.View;
    import android.view.ViewGroup;
    
    /**
     * 原创作者:
     * 谷哥的小弟
     *
     * 博客地址:
     * http://blog.csdn.net/lfdfhl
     */
    public class MyFlowLayout extends ViewGroup{
        private int  verticalSpacing = 20;
        public MyFlowLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    
            int paddingLeft = getPaddingLeft();
            int paddingRight = getPaddingRight();
            int paddingTop = getPaddingTop();
            int paddingBottom = getPaddingBottom();
    
            int widthUsed = paddingLeft + paddingRight;
            int heightUsed = paddingTop + paddingBottom;
    
            int childMaxHeightOfThisLine = 0;
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                if (child.getVisibility() != GONE) {
                    int childUsedWidth = 0;
                    int childUsedHeight = 0;
                    measureChild(child,widthMeasureSpec,heightMeasureSpec);
                    childUsedWidth += child.getMeasuredWidth();
                    childUsedHeight += child.getMeasuredHeight();
    
                    LayoutParams childLayoutParams = child.getLayoutParams();
    
                    MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childLayoutParams;
    
                    childUsedWidth += marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
                    childUsedHeight += marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;
    
                    if (widthUsed + childUsedWidth < widthSpecSize) {
                        widthUsed += childUsedWidth;
                        if (childUsedHeight > childMaxHeightOfThisLine) {
                            childMaxHeightOfThisLine = childUsedHeight;
                        }
                    } else {
                        heightUsed += childMaxHeightOfThisLine + verticalSpacing;
                        widthUsed = paddingLeft + paddingRight + childUsedWidth;
                        childMaxHeightOfThisLine = childUsedHeight;
                    }
    
                }
    
            }
    
            heightUsed += childMaxHeightOfThisLine;
            setMeasuredDimension(widthSpecSize, heightUsed);
        }
    
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int paddingLeft = getPaddingLeft();
            int paddingRight = getPaddingRight();
            int paddingTop = getPaddingTop();
            int paddingBottom = getPaddingBottom();
    
            int childStartLayoutX = paddingLeft;
            int childStartLayoutY = paddingTop;
    
            int widthUsed = paddingLeft + paddingRight;
    
            int childMaxHeight = 0;
    
            int childCount = getChildCount();
    
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                if (child.getVisibility() != GONE) {
                    int childNeededWidth, childNeedHeight;
                    int left, top, right, bottom;
    
                    int childMeasuredWidth = child.getMeasuredWidth();
                    int childMeasuredHeight = child.getMeasuredHeight();
    
                    LayoutParams childLayoutParams = child.getLayoutParams();
                    MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childLayoutParams;
                    int childLeftMargin = marginLayoutParams.leftMargin;
                    int childTopMargin = marginLayoutParams.topMargin;
                    int childRightMargin = marginLayoutParams.rightMargin;
                    int childBottomMargin = marginLayoutParams.bottomMargin;
                    childNeededWidth = childLeftMargin + childRightMargin + childMeasuredWidth;
                    childNeedHeight = childTopMargin + childBottomMargin + childMeasuredHeight;
    
                    if (widthUsed + childNeededWidth <= r - l) {
                        if (childNeedHeight > childMaxHeight) {
                            childMaxHeight = childNeedHeight;
                        }
                        left = childStartLayoutX + childLeftMargin;
                        top = childStartLayoutY + childTopMargin;
                        right = left + childMeasuredWidth;
                        bottom = top + childMeasuredHeight;
                        widthUsed += childNeededWidth;
                        childStartLayoutX += childNeededWidth;
                    } else {
                        childStartLayoutY += childMaxHeight + verticalSpacing;
                        childStartLayoutX = paddingLeft;
                        widthUsed = paddingLeft + paddingRight;
                        left = childStartLayoutX + childLeftMargin;
                        top = childStartLayoutY + childTopMargin;
                        right = left + childMeasuredWidth;
                        bottom = top + childMeasuredHeight;
                        widthUsed += childNeededWidth;
                        childStartLayoutX += childNeededWidth;
                        childMaxHeight = childNeedHeight;
                    }
                    child.layout(left, top, right, bottom);
                }
            }
        }
    
    }
    
    

    现就该代码中的主要操作做一些分析和介绍。

    1. 控件继承自ViewGroup,请参见代码第15行。
      系统自带的布局比如LinearLayout很难满足标签自动换行的功能,所以继承ViewGroup实现自需的设计和逻辑
    2. 重写onMeasure( ),请参见代码第22-71行。
      2.1 获取View宽和高的mode和size,请参见代码第23-26行。
      此处widthSpecSize表示了View的宽,该值在判断是否需要换行时会用到。
      2.2 计算View在水平方向和垂直方向已经占用的大小,请参见代码第33-34行。
      在源码阶段也分析过这些已经占用的大小主要指的是View的padding值。
      2.3 测量每个子View的宽和高,请参见代码第38-67行。
      这一步操作是关键。在这一步中需要测量出来每个子View的大小从而计算出该控件的高度。
      在对代码做具体分析之前,我们先明白几个问题。
      第一点:
      我们常说测量每个子View的宽和高是为了将每个子View的宽累加起来得到父View的宽,将每个子View的高累加起来得到父View的高。
      在此处,控件的宽就是屏幕的宽,所以我们不用去累加每个子View的宽,但是要利用子View的宽判断换行的时机。
      至于控件的高,还是需要将每个子View的高相累加。
      第二点:
      怎么判断需要换行显示新的tag呢?如果:
      这一行已占用的宽度+即将显示的子View的宽度>该行总宽度
      那么就要考虑换行显示该tag
      第三点:
      如果十个人站成一排,那么这个队伍的高度是由谁决定的呢?当然是这排人里个子最高的人决定的。同样的道理,几个tag摆放在同一行,这一行的高度就是由最高的tag的值决定的;然后将每一行的高度相加就是View的总高了。

    嗯哼,明白了这些,我们再看代码就容易得多了。
    第一步:
    利用measureChild( )测量子View,请参见代码第43行。
    第二步:
    计算子View需要占用的宽和高(childUsedWidth和childUsedHeight),请参见代码第51-52行。
    第三步:
    判断和处理是否需要换行,请参见代码第54-63行。
    第四步:
    利用setMeasuredDimension()设置View的宽和高,请参见代码第70行

    1. 重写onLayout( ),请参见代码第75-133行。
      在onMeasure中已经对每个子View进行了测量,在该阶段需要把每个子View摆放在合适的位置。
      所以核心是确定每个子View的left, top, right, bottom。
      在该过程中,同样需要考虑换行的问题,思路也和measure阶段类似,故不再赘述。

    嗯哼,完成了该自定义控件的代码,该怎么样使用呢?

    mFlowLayout.addView(textView, marginLayoutParams);

    通过该方式就可以将一个tag添加到FlowLayout控件中显示。

    示例小结:
    通过直接继承ViewGroup在其onMeasure( )和onLayout()中分别测量和摆放各子View

    好了,这就是和大家一起分享的两个自定义View控件。


    who is the next one? ——> TouchEvent

    展开全文
  • 自定义View系列教程01--常用工具介绍

    万次阅读 多人点赞 2016-05-05 16:50:38
    自定义View的时候,常常会用到一些Android系统提供的工具。这些工具封装了我们经常会用到的方法,比如拖拽View,计算滑动速度,View的滚动,手势处理等等。如果我们自己去实现这些方法会比较繁琐,而且容易出一些...
  • 自定义View基础 - 最易懂的自定义View原理系列(1)

    万次阅读 多人点赞 2017-02-20 11:27:07
    自定义View原理是Android开发者必须了解的基础; 在了解自定义View之前,你需要有一定的知识储备; 本文将全面解析关于自定义View中的所有知识基础。 目录1. View的分类视图View主要分为两类: 类别 解释 特点 ...
  • 自定义View
  • 不知不觉中,带你一步步深入了解View系列的文章已经写到第四篇了,回顾一下,我们一共学习了...现在前半部分的承诺已经如约兑现了,那么今天我就要来兑现后面部分的承诺,讲一讲自定义View的实现方法,同时这也是带
  • 网上有大量关于自定义View原理的文章,但存在一些问题:内容不全、思路不清晰、无源码分析、简单问题复杂化 等 今天,我将全面总结自定义View原理中的measure过程,我能保证这是市面上的最全面、最清晰、最易懂的 ...
  • 推翻自己和过往,重学自定义View

    万次阅读 多人点赞 2016-06-14 13:49:50
    关于自定义View以前看了很多资料看,从博客园到CSDN,从stackoverflow到EOE论坛,从百草园到三味书屋,搜了一大筐,沮丧的发现这些文章大同小异:只举个简单的例子,很少研究为什么;人云亦云,文章里的内容根本没有...
  • 自定义view是一个综合的技术体系,本说明文档不去分析一个个具体的自定义view的实现,因为自定义view五花八门,是不可能全部分析一遍的。虽然我们不能把自定义view都分析一遍,但是我们能够提取出一种思想,而面对...
  • 自定义View 为什么要自定义View? 主要是Andorid系统内置的View 无法实现我们的 需求,我们需要针对我们的业务需求定制我们想要的 View.自定义View 我们大部分时候只需重写两个函数: onMeasure(),onDraw(). ...
  • 网上有大量关于自定义View原理的文章,但存在一些问题:内容不全、思路不清晰、无源码分析、简单问题复杂化 等 今天,我将全面总结自定义View原理中的Layout过程,我能保证这是市面上的最全面、最清晰、最易懂的 ...
  • Android 自定义View自定义View属性

    千次阅读 2014-10-10 23:50:18
    我们可以自定义View,当然也可以自定义View的属性。下面就从多个方面来介绍自定义View属性的使用。在使用自定义属性之前,我们需要定义属性。一般我们会按下面的步骤来进行:
  • 网上有大量关于自定义View原理的文章,但存在一些问题:内容不全、思路不清晰、无源码分析、简单问题复杂化 等 今天,我将全面总结自定义View原理中的Draw过程,我能保证这是市面上的最全面、最清晰、最易懂的 ...
  • 自定义android进度条,带有进度指示,项目详情:http://blog.csdn.net/xiaanming/article/details/10298163
  • 自定义View以及自定义属性

    千次阅读 2016-12-10 14:33:10
    Android自定义View以及自定义属性
  • 自定义view流式布局

    万次阅读 2020-05-05 21:59:25
    自定义view流式布局 已经封装依赖可以直接粘贴使用 1.导入依赖 implementation 'com.github.LiHangKun:LiuShiBuJu:1' 然后在项目的build.gradle中 allprojects { repositories { google() jcenter() maven { url...
  • 自定义view圆形头像

    万次阅读 2020-05-05 16:07:41
    自定义view圆形头像 我已经将源码打包成依赖 朋友们可以直接导依赖直接使用 1.正常导入: implementation ‘com.github.LiHangKun:wuyuewuone:1’ 2.在最外层项目的build.gradle里面 maven { url ...
  • 自定义view学习(一)---自定义view

    千次阅读 2017-03-30 16:47:24
    一直感觉会自定义view很牛掰,在毕设项目中多处用到自定义控件,但都是在别人的框架上造轮子,一直打算总结一下自定义控件的实现方式,今天就来总结一下吧。 回到主题,自定义View ,需要掌握的几个点是什么呢?  ...
  • 自定义View在我自学Android开发中一直感觉是高手才能掌握的知识,因为情况太多而且界面看起来有很复杂炫酷。但是自定义View同样遵循着某些规则,这篇博客我就从这个规则入手,先实现View,在涉及原理。
  • 自定义view是干嘛的呢? 当我们不满足于Android提供的原生控件和布局时,就应该考虑到自定义view自定义View分为两大块: 自定义控件 和 自定义容器 自定义View必须重写两个构造方法 第一个是一个参数的上下文...
  • Android:手把手教你写一个完整的自定义View

    万次阅读 多人点赞 2017-03-14 10:11:27
    今天,我将手把手教你写一个自定义View,并理清自定义View所有应该的注意点 阅读本文前,请先阅读我写的一系列自定义View文章 自定义View基础 - 最易懂的自定义View原理系列(1) 自定义View Measure过程 - 最...
  • 我有现成的布局xml文件,现在想定义一个组合的自定义view,怎样把这个view的布局指定为一个xml文件
  • 转载请标明出处: ...在上一篇博客《Android自定义View(一、初体验)》中我们体验了自定义控件的基本流程: 继承View,覆盖构造方法 自定义属性 重写onMeasure方法测量宽高 重写onDraw方法
  • Android 自定义View步骤

    2017-09-01 11:02:06
    自定义View的相关文章: Android 实现一个简单的自定义View Android 自定义View步骤 Android 自定义View之Canvas相关方法说明 Android 自定义View实例(验证码) Android 自定义View实例(进度圆环) Android 源码...
  • Android 自定义View (三) 圆环交替 等待效果

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

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 58,611
精华内容 23,444
关键字:

自定义view