程序员人生 网站导航

自定义View系列教程05--示例分析

栏目:综合技术时间:2016-06-08 17:12:14

自定义View系列教程01–经常使用工具介绍
自定义View系列教程02–onMeasure源码详实分析
自定义View系列教程03–onLayout源码详实分析
自定义View系列教程04–Draw源码分析及其实践
自定义View系列教程05–示例分析

PS:如果觉得文章太长,那就直接看视频吧


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

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

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

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

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

在此,举两个例子。


瞅瞅第1个例子,效果以下图:

这里写图片描述

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

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

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

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

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

<?xml version="1.0" encoding="utf⑻"?> <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些分析和介绍。

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

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

    view.measure(parentWidthMeasureSpec, parentHeightMeasureSpec);

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

    measuredHeight * interpolatedTime

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

    view.requestLayout();

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

  7. 实现content部份的隐藏,请参见代码第141⑴61行
    隐藏的进程和之前的逐次显示进程原理是1样的,不再赘述。

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

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


瞅瞅第2个例子,效果以下图:

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

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些分析和介绍。

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

    嗯哼,明白了这些,我们再看代码就容易很多了。
    第1步:
    利用measureChild( )丈量子View,请参见代码第43行。
    第2步:
    计算子View需要占用的宽和高(childUsedWidth和childUsedHeight),请参见代码第51⑸2行。
    第3步:
    判断和处理是不是需要换行,请参见代码第54⑹3行。
    第4步:
    利用setMeasuredDimension()设置View的宽和高,请参见代码第70行

  3. 重写onLayout( ),请参见代码第75⑴33行。
    在onMeasure中已对每一个子View进行了丈量,在该阶段需要把每一个子View摆放在适合的位置。
    所以核心是肯定每一个子View的left, top, right, bottom。
    在该进程中,一样需要斟酌换行的问题,思路也和measure阶段类似,故不再赘述。

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

mFlowLayout.addView(textView, marginLayoutParams);

通过该方式就能够将1个tag添加到FlowLayout控件中显示。

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


PS:如果觉得文章太长,那就直接看视频吧


好了,这就是和大家1起分享的两个自定义View控件。
who is the next one? ——> TouchEvent

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐