什么是Fragment
对于Fragment的一些理解
前言
Fragment想必大家不陌生吧,在日常开发中,对于Fragment的使用也很频繁,现在主流的APP中,基本的架构也都是一个主页,然后每个Tab项用Fragment做布局,不同选项做切换,使用起来也方便。但是否对它有足够的认识吗,谷歌推荐用Fragment来代替Activity,但又没有明确说为什么要用Fragment来代替Activity,这里就引发争议了,那到底是要不要用,是否使用Fragment完全替换Activity真的比常规开发模式更好吗?如果要用的话,那需要了解为何要使用Fragment,Fragment是什么,它的生命周期如何,如何使用,通信又是怎样,有什么缺点吗?带着这些问题,我们一一去解读。
目录
- Fragment为何要用
- Fragment是什么
- Fragment生命周期
- Fragment怎么用
- Fragment通信
- Fragment是否很完美
Fragment为何要用
Fragment是Android 3.0 (Honeycomb)被引入的。主要目的是为了给大屏幕(如平板电脑)上更加动态和灵活的UI设计提供支持。由于平板电脑的屏幕比手机的屏幕大很多,因此可用于组合和交换的UI组件的空间更大,利用Fragment实现此类设计的时,就无需管理对视图层次结构的复杂更改。
通过将 Activity 布局分成片段,您可以在运行时修改 Activity 的外观,并在由 Activity 管理的返回栈中保留这些更改。如果仅仅只有Activity布局,那是不够的,不仅在手机上有一套布局,同时在平板上还需要设计一套布局,那样维护起来也麻烦,代码上也有一定的冗余,对于APP包的大小也有一定的压力。Fragment的优势是布局在不同设备上的适配。
比如:
从图中我们可以看到,在平板中,一个Activity A包含了两个Fragment,分别是Fragment A和Fragment B,但在手机中呢,就需要两个Activity,分别是Activity A包含Fragment A和Activity B包含Fragment B。同时每个Fragment都具有自己的一套生命周期回调方法,并各自处理自己的用户输入事件。 因此,在平板中使用一个Activity 就可以了,左侧是列表,右边是内容详情。
除此之外,使用Fragment还有这么几个方面优势:
- 代码复用。特别适用于模块化的开发,因为一个Fragment可以被多个Activity嵌套,有个共同的业务模块就可以复用了,是模块化UI的良好组件。
- Activity用来管理Fragment。Fragment的生命周期是寄托到Activity中,Fragment可以被Attach添加和Detach释放。
- 可控性。Fragment可以像普通对象那样自由的创建和控制,传递参数更加容易和方便,也不用处理系统相关的事情,显示方式、替换、不管是整体还是部分,都可以做到相应的更改。
- Fragments是view controllers,它们包含可测试的,解耦的业务逻辑块,由于Fragments是构建在views之上的,而views很容易实现动画效果,因此Fragments在屏幕切换时具有更好的控制。
Fragment是什么
说了半天的Fragment,也看到这么多次Fragment这个名词出现,那么Fragment到底是什么东东呢?定义又是如何?
Fragment也可以叫为“片段”,但我觉得“片段”中文叫法有点生硬,还是保持叫Fragment比较好,它可以表示Activity中的行为或用户界面部分。我们可以在一个Activity中用多个Fragment组合来构建多窗格的UI,以及在多个Activity中重复使用某个Fragment。它有自己的生命周期,能接受自己的输入,并且可以在 Activity 运行时添加或删除Fragment(有点像在不同 Activity 中重复使用的“子 Activity”)。
简单来说,Fragment其实可以理解为一个具有自己生命周期的控件,只不过这个控件又有点特殊,它有自己的处理输入事件的能力,有自己的生命周期,又必须依赖于Activity,能互相通信和托管。
Fragment生命周期
如图:
这张图是Fragment生命周期和Activity生命周期对比图,可以看到两者还是有很多相似的地方,比如都有onCreate(),onStart(),onPause(),onDestroy()等等,因为Fragment是被托管到Activity中的,所以多了两个onAttach()和onDetach()。这里讲讲与Activity生命周期不一样的方法。
onAttach()
Fragment和Activity建立关联的时候调用,被附加到Activity中去。
onCreate()
系统会在创建Fragment时调用此方法。可以初始化一段资源文件等等。
onCreateView()
系统会在Fragment首次绘制其用户界面时调用此方法。 要想为Fragment绘制 UI,从该方法中返回的 View 必须是Fragment布局的根视图。如果Fragment未提供 UI,您可以返回 null。
onViewCreated()
在Fragment被绘制后,调用此方法,可以初始化控件资源。
onActivityCreated()
当onCreate(),onCreateView(),onViewCreated()方法执行完后调用,也就是Activity被渲染绘制出来后。
onPause()
系统将此方法作为用户离开Fragment的第一个信号(但并不总是意味着此Fragment会被销毁)进行调用。 通常可以在此方法内确认在当前用户会话结束后仍然有效的任何更改(因为用户可能不会返回)。
onDestroyView()
Fragment中的布局被移除时调用。
onDetach()
Fragment和Activity解除关联的时候调用。
但需要注一点是:除了onCreateView,其他的所有方法如果你重写了,必须调用父类对于该方法的实现。
还有一般在启动Fragment的时候,它的生命周期就会执行这几个方法。
Fragment怎么用
前面介绍了半天,不耐烦的人会说,这么多废话,也不见的到底是如何使用,毕竟我们是开发者,需要的使用方式,那么现在就来说说用法如何吧。两种方式:静态用法和动态用法。
静态用法
1、继承Fragment,重写onCreateView决定Fragemnt的布局
2、在Activity中声明此Fragment,就当和普通的View一样
首先是布局文件:fragment1.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00ff00" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is fragment 1"
android:textColor="#000000"
android:textSize="25sp" />
</LinearLayout>
可以看到,这个布局文件非常简单,只有一个LinearLayout,里面加入了一个TextView。我们再新建一个fragment2.xml :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffff00" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is fragment 2"
android:textColor="#000000"
android:textSize="25sp" />
</LinearLayout>
然后新建一个类Fragment1,这个类是继承自Fragment的:
public class Fragment1 extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment1, container, false);
}
}
可以看到,在onCreateView()方法中加载了fragment1.xml的布局。同样fragment2.xml也是一样的做法,新建一个Fragment2类:
public class Fragment2 extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment2, container, false);
}
}
然后打开或新建activity_main.xml作为主Activity的布局文件,在里面加入两个Fragment的引用,使用android:name前缀来引用具体的Fragment:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false" >
<fragment
android:id="@+id/fragment1"
android:name="com.example.fragmentdemo.Fragment1"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/fragment2"
android:name="com.example.fragmentdemo.Fragment2"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
最后新建MainActivity作为程序的主Activity,里面的代码非常简单,都是自动生成的:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
现在我们来运行一次程序,就会看到,一个Activity很融洽地包含了两个Fragment,这两个Fragment平分了整个屏幕,效果图如下:
动态用法
上面仅仅是Fragment简单用法,它真正强大部分是在动态地添加到Activity中,那么动态用法又是如何呢?
还是在静态用法代码的基础上修改,打开activity_main.xml,将其中对Fragment的引用都删除,只保留最外层的LinearLayout,并给它添加一个id,因为我们要动态添加Fragment,不用在XML里添加了,删除后代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false" >
</LinearLayout>
然后打开MainActivity,修改其中的代码如下所示:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Display display = getWindowManager().getDefaultDisplay();
if (display.getWidth() > display.getHeight()) {
Fragment1 fragment1 = new Fragment1();
getFragmentManager().beginTransaction().replace(R.id.main_layout, fragment1).commit();
} else {
Fragment2 fragment2 = new Fragment2();
getFragmentManager().beginTransaction().replace(R.id.main_layout, fragment2).commit();
}
}
}
看到了没,首先,我们要获取屏幕的宽度和高度,然后进行判断,如果屏幕宽度大于高度就添加fragment1,如果高度大于宽度就添加fragment2。动态添加Fragment主要分为4步:
1.获取到FragmentManager,在Activity中可以直接通过getFragmentManager得到。
2.开启一个事务,通过调用beginTransaction方法开启。
3.向容器内加入Fragment,一般使用replace方法实现,需要传入容器的id和Fragment的实例。
4.提交事务,调用commit方法提交。
现在运行一下程序,效果如下图所示:
要想管理 Activity 中的片段,需要使用 FragmentManager。要想获取它,需要 Activity 调用 getFragmentManager()。
使用 FragmentManager 执行的操作包括:
- 通过 findFragmentById()(对于在 Activity 布局中提供 UI 的片段)或 findFragmentByTag()(对于提供或不提供 UI 的片段)获取 Activity 中存在的片段
- 通过 popBackStack()将片段从返回栈中弹出
- 通过 addOnBackStackChangedListener() 注册一个侦听返回栈变化的侦听器
也可以使用 FragmentManager 打开一个 FragmentTransaction,通过它来执行某些事务,如添加和删除片段。
Fragment通信
尽管 Fragment 是作为独立于 Activity的对象实现,并且可在多个 Activity 内使用,但Fragment 的给定实例会直接绑定到包含它的 Activity。具体地说,Fragment 可以通过 getActivity() 访问 Activity实例,并轻松地执行在 Activity 布局中查找视图等任务。如:
View listView = getActivity().findViewById(R.id.list);
同样地,Activity 也可以使用 findFragmentById() 或 findFragmentByTag(),通过从 FragmentManager 获取对 Fragment 的引用来调用Fragment中的方法。例如:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
创建对 Activity 的事件回调
在某些情况下,可能需要通过与 Activity 共享事件。执行此操作的一个好方法是,在Fragment 内定义一个回调接口,并要求宿主 Activity 实现它。 当 Activity 通过该接口收到回调时,可以根据需要与布局中的其他Fragment共享这些信息。
例如,如果一个新闻应用的 Activity 有两个Fragment ,一个用于显示文章列表(Fragment A),另一个用于显示文章(Fragment B)—,那么Fragment A必须在列表项被选定后告知 Activity,以便它告知Fragment B 显示该文章。 在本例中,OnArticleSelectedListener 接口在片段 A 内声明:
public static class FragmentA extends ListFragment {
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
}
然后,该Fragment的宿主 Activity 会实现 OnArticleSelectedListener 接口并替代 onArticleSelected(),将来自Fragment A 的事件通知Fragment B。为确保宿主 Activity 实现此界面,Fragment A 的 onAttach() 回调方法(系统在向 Activity 添加Fragment时调用的方法)会通过转换传递到 onAttach() 中的 Activity 来实例化 OnArticleSelectedListener 的实例:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
}
}
}
如果 Activity 未实现界面,则片段会引发 ClassCastException。实现时,mListener 成员会保留对 Activity 的 OnArticleSelectedListener 实现的引用,以便Fragment A 可以通过调用 OnArticleSelectedListener 界面定义的方法与 Activity 共享事件。例如,如果Fragment A 是 ListFragment 的一个扩展,则用户每次点击列表项时,系统都会调用Fragment中的 onListItemClick(),然后该方法会调用 onArticleSelected() 以与 Activity 共享事件:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
mListener.onArticleSelected(noteUri);
}
}
Fragment是否很完美
因为Fragment是由FragmentManager来管理,每一个Activity有一个FragmentManager,管理着一个Fragment的栈,Activity是系统级别的,由系统来管理ActivityManager,栈也是系统范围的。而Fragment则是每个Activity范围内的,所以在使用Fragment的时候也有几点要注意。
- 同一个Activity中,只能有一个ID或TAG标识的Fragment实例。
这很容易理解,同一个范围内,有标识的实例肯定是要唯一才行(否则还要标识干嘛)这个在布局中经常犯错,在布局中写Fragment最好不要加ID或者TAG,否则很容易出现不允许创建的错误。我的原则是如果放在布局中,就不要加ID和TAG,如果需要ID和TAG就全用代码控制。创建新实例前先到FragmentManager中查找一番,这也正是有标识的意义所在。 - 一个Activity中有一个Fragment池,实例不一定会被销毁,可能会保存在池中。
这个跟第一点差不多。就好比系统会缓存Activity的实例一样,FragmentManager也会缓存Fragment实例,以方便和加速再次显示。 - FragmentManager的作用范围是整个Activity,所以,某一个布局ID,不能重复被Fragment替换。
通常显示Fragment有二种方式,一种是层叠到某个布局上,或者把某个布局上面的Fragment替换掉,但是这个布局不能出现二次,比如布局A中有ID为id的区域,要显示为Fragment,此布局A,只能在一个Activity中显示一个,否则第二个id区域不能被Fragment成功替换。因为虽有二个ID布局的实例,但ID是相同的,对FragmentManager来说是一样的,它会认为只有一个,因为它看的是布局的ID,而不是布局的实例。 - Fragment的生命周期反应Activity的生命周期。
Fragment在显示和退出时会走一遍完整的生命周期。此外,正在显示时,就跟Activity的一样,Activity被onPause,里面的Fragment就onPause,以此类推,由此带来的问题就是,比如你在onStart()里面做了一些事情,那么,当宿主Activity被挡住,又出现时(比如接了个电话),Fragment的onStart也会被高到,所以你要想到,这些生命周期不单单在显示和退出时会走到。 - Fragment的可见性。
这个问题出现在有Fragment栈的时候,也就是说每个Fragment不知道自己是否真的对用户可见。比如现在是Fragment A,又在其上面显示了Fragment B,当B显示后,A并不知道自己上面还有一个,也不知道自己对用户不可见了,同样再有一个C,B也不知。C退出后,B依然不知自己已在栈顶,对用户可见,B退后,A也不知。也就是说Fragment显示或者退出,栈里的其他Fragment无法感知。这点就不如Activity,a被b盖住后,a会走到onStop(),同样c显示后,b也能通过onStop()感知。Fragment可以从FragmentManager监听BackStackState的变化,但它只告诉你Stack变了,不告诉你是多了,还是少,还有你处的位置。有一个解决方案就是,记录页面的Path深度,再跟Fragment所在的Stack深度来比较,如果一致,那么这个Fragment就在栈顶。因为每个页面的Path深度是固定的,而Stack深度是不变化的,所以这个能准确的判断Fragment是否对用户可见,当然,这个仅针对整个页面有效,对于布局中的一个区域是无效的。 - Fragment的事件传递。
对于层叠的Fragment,其实就相当于在一个FrameLayout里面加上一堆的View,所以,如果处于顶层的Fragment没处理点击事件,那么事件就会向下层传递,直到事件被处理。比如有二个Fragment A和B,B在A上面,B只有TextView且没处理事件,那么点击B时,会发现A里的View处理了事件。这个对于Activity也不会发生,因为事件不能跨窗体传播,上面的Activity没处理事件,也不会传给下面的Activity,即使它可见。解决之法,就是让上面的Fragment的根布局吃掉事件,为每个根ViewGroup添加onClick=“true”。 - 与第三方Activity交互。与第三方交互,仍要采用Android的标准startActivityForResult()和onActivityResult()这二个方法来进行。但对于Fragment有些事情需要注意,Fragment也有这二个方法,但是为了能正确的让Fragment收到onActivityResult(),需要:
- 宿主Activity要实现一个空的onActivityResult(),里面调用super.onActivityResult()
- 调用Fragment#startActivityForResult()而不是用Activity的 当然,也可以直接使用Activity的startActivityForResult(),那样的话,就只能在宿主Activity里处理返回的结果了。
android自定义View
1、概述
Android****自定义View / ViewGroup****的步骤大致如下:
- 自定义属性;
- 选择和设置构造方法;
- 重写onMeasure()方法;
- 重写onDraw()方法;
- 重写onLayout()方法;
- 重写其他事件的方法(滑动监听等)。</pre>
2、自定义属性
Android自定义属性主要有定义、使用和获取三个步骤。
2.1、定义自定义属性
我们通常将自定义属性定义在/values/attr.xml文件中(attr.xml文件需要自己创建)。
先来看一段示例代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="rightPadding" format="dimension" />
<declare-styleable name="CustomMenu">
<attr name="rightPadding" />
</declare-styleable>
</resources>
可以看到,我们先是定义了一个属性rightPadding,然后又在CustomMenu中引用了这个属性。下面说明一下:
- 首先,我们可以在declare-stylable标签中直接定义属性而不需要引用外部定义好的属性,但是为了属性的重用,我们可以选择上面的这种方法:先定义,后引用;
- declare-stylable标签只是为了给自定义属性分类。一个项目中可能又多个自定义控件,但只能又一个attr.xml文件,因此我们需要对不同自定义控件中的自定义属性进行分类,这也是为什么declare-stylable标签中的name属性往往定义成自定义控件的名称;
- 所谓的在declare-stylable标签中的引用,就是去掉了外部定义的format属性,如果没有去掉format,则会报错;如果外部定义中没有format而在内部引用中又format,也一样会报错。
常用的format类型:
- string:字符串类型;
- integer:整数类型;
- float:浮点型;
- dimension:尺寸,后面必须跟dp、dip、px、sp等单位;
- Boolean:布尔值;
- reference:引用类型,传入的是某一资源的ID,必须以“@”符号开头;
- color:颜色,必须是“#”符号开头;
- fraction:百分比,必须是“%”符号结尾;
- enum:枚举类型</pre>
下面对format类型说明几点:
- format中可以写多种类型,中间使用“|”符号分割开,表示这几种类型都可以传入这个属性;
- enum类型的定义示例如下代码所示:
<resources>
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
<declare-styleable name="CustomView">
<attr name="orientation" />
</declare-styleable>
</resources>
使用时通过getInt()方法获取到value并判断,根据不同的value进行不同的操作即可。
2.2、使用自定义属性
在XML布局文件中使用自定义的属性时,我们需要先定义一个namespace。Android中默认的namespace是android,因此我们通常可以使用“android:xxx”的格式去设置一个控件的某个属性,android这个namespace的定义是在XML文件的头标签中定义的,通常是这样的:
xmlns:android="http://schemas.android.com/apk/res/android"
我们自定义的属性不在这个命名空间下,因此我们需要添加一个命名空间。
自定义属性的命名空间如下:
xmlns:app="http://schemas.android.com/apk/res-auto"
可以看出来,除了将命名空间的名称从android改成app之外,就是将最后的“res/android”改成了“res-auto”。
注意:自定义namespace的名称可以自己定义,不一定非得是app。
2.3、获取自定义属性
在自定义View / ViewGroup中,我们可以通过TypedArray获取到自定义的属性。示例代码如下:
public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomMenu, defStyleAttr, 0); int indexCount = a.getIndexCount(); for (int i = 0; i < indexCount; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.CustomMenu_rightPadding:
mMenuRightPadding = a.getDimensionPixelSize(attr, 0); break;
}
}
a.recycle();
}
这里需要说明一下:
- 获取自定义属性的代码通常是在三个参数的构造方法中编写的(具体为什么是三个参数的构造方法,下面的章节中会有解释);
- 在获取TypedArray对象时就为其绑定了该自定义View的自定义属性集(CustomMenu),通过getIndexCount()方法获取到自定义属性的数量,通过getIndex()方法获取到某一个属性,最后通过switch语句判断属性并进行相应的操作;
- 在TypedArray使用结束后,需要调用recycle()方法回收它。
3、构造方法
当我们定义一个新的类继承了View或ViewGroup时,系统都会提示我们重写它的构造方法。View / ViewGroup中又四个构造方法可以重写,它们分别有一、二、三、四个参数。四个参数的构造方法我们通常用不到,因此这个章节中我们主要介绍一个参数、两个参数和三个参数的构造方法(这里以CustomMenu控件为例)。
3.1、一个参数的构造方法
构造方法的代码:
public CustomMenu(Context context) { …… }
这个构造方法只有一个参数Context上下文。当我们在JAVA代码中直接通过new关键在创建这个控件时,就会调用这个方法。
3.2、两个参数的构造方法
public CustomMenu(Context context, AttributeSet attrs) { …… }
这个构造方法有两个参数:Context上下文和AttributeSet属性集。当我们需要在自定义控件中获取属性时,就默认调用这个构造方法。AttributeSet对象就是这个控件中定义的所有属性。
我们可以通过AttributeSet对象的getAttributeCount()方法获取属性的个数,通过getAttributeName()方法获取到某条属性的名称,通过getAttributeValue()方法获取到某条属性的值。
注意:不管有没有使用自定义属性,都会默认调用这个构造方法,“使用了自定义属性就会默认调用三个参数的构造方法”的说法是错误的。
3.3、三个参数的构造方法
public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) { …… }
这个构造方法中有三个参数:Context上下文、AttributeSet属性集和defStyleAttr自定义属性的引用。这个构造方法不会默认调用,必须要手动调用,这个构造方法和两个参数的构造方法的唯一区别就是这个构造方法给我们默认传入了一个默认属性集。
defStyleAttr指向的是自定义属性的<declare-styleable>标签中定义的自定义属性集,我们在创建TypedArray对象时需要用到defStyleAttr。
3.4、三个构造方法的整合
一般情况下,我们会将这三个构造方法串联起来,即层层调用,让最终的业务处理都集中在三个参数的构造方法。我们让一参的构造方法引用两参的构造方法,两参的构造方法引用三参的构造方法。示例代码如下:
public CustomMenu(Context context) { this(context, null);
} public CustomMenu(Context context, AttributeSet attrs) { this(context, attrs, 0);
} public CustomMenu(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // 业务代码 }
这样一来,就可以保证无论使用什么方式创建这个控件,最终都会到三个参数的构造方法中处理,减少了重复代码。
4、onMeasure()
onMeasure()方法中主要负责测量,决定控件本身或其子控件所占的宽高。我们可以通过onMeasure()方法提供的参数widthMeasureSpec和heightMeasureSpec来分别获取控件宽度和高度的测量模式和测量值(测量 = 测量模式 + 测量值)。
widthMeasureSpec和heightMeasureSpec虽然只是int类型的值,但它们是通过MeasureSpec类进行了编码处理的,其中封装了测量模式和测量值,因此我们可以分别通过MeasureSpec.getMode(xMeasureSpec)和MeasureSpec. getSize(xMeasureSpec)来获取到控件或其子View的测量模式和测量值。
测量模式分为以下三种情况:
- EXACTLY:当宽高值设置为具体值时使用,如100DIP、match_parent等,此时取出的size是精确的尺寸;
- AT_MOST:当宽高值设置为wrap_content时使用,此时取出的size是控件最大可获得的空间;
- UNSPECIFIED:当没有指定宽高值时使用(很少见)。</pre>
onMeasure()方法中常用的方法:
- getChildCount():获取子View的数量;
- getChildAt(i):获取第i个子控件;
- subView.getLayoutParams().width/height:设置或获取子控件的宽或高;
- measureChild(child, widthMeasureSpec, heightMeasureSpec):测量子View的宽高;
- child.getMeasuredHeight/width():执行完measureChild()方法后就可以通过这种方式获取子View的宽高值;
- getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
- setMeasuredDimension(width, height):重新设置控件的宽高。如果写了这句代码,就需要删除“super. onMeasure(widthMeasureSpec, heightMeasureSpec);”这行代码。
注意:onMeasure()方法可能被调用多次,这是因为控件中的内容或子View可能对分配给自己的空间“不满意”,因此向父空间申请重新分配空间。
5、onDraw()
onDraw()方法负责绘制,即如果我们希望得到的效果在Android原生控件中没有现成的支持,那么我们就需要自己绘制我们的自定义控件的显示效果。
要学习onDraw()方法,我们就需要学习在onDraw()方法中使用最多的两个类:Paint和Canvas。
注意:每次触摸了自定义View/ViewGroup时都会触发onDraw()方法。
5.1、Paint类
Paint画笔对象,这个类中包含了如何绘制几何图形、文字和位图的样式和颜色信息,指定了如何绘制文本和图形。画笔对象右很多设置方法,大体上可以分为两类:一类与图形绘制有关,一类与文本绘制有关。
Paint****类中有如下方法:
1、图形绘制:
- setArgb(int a, int r, int g, int b):设置绘制的颜色,a表示透明度,r、g、b表示颜色值;
- setAlpha(int a):设置绘制的图形的透明度;
- setColor(int color):设置绘制的颜色;
- setAntiAlias(boolean a):设置是否使用抗锯齿功能,抗锯齿功能会消耗较大资源,绘制图形的速度会减慢;
- setDither(boolean b):设置是否使用图像抖动处理,会使图像颜色更加平滑饱满,更加清晰;
- setFileterBitmap(Boolean b):设置是否在动画中滤掉Bitmap的优化,可以加快显示速度;
- setMaskFilter(MaskFilter mf):设置MaskFilter来实现滤镜的效果;
- setColorFilter(ColorFilter cf):设置颜色过滤器,可以在绘制颜色时实现不同颜色的变换效果;
- setPathEffect(PathEffect pe):设置绘制的路径的效果;
- setShader(Shader s):设置Shader绘制各种渐变效果;
- setShadowLayer(float r, int x, int y, int c):在图形下面设置阴影层,r为阴影角度,x和y为阴影在x轴和y轴上的距离,c为阴影的颜色;
- setStyle(Paint.Style s):设置画笔的样式:FILL实心;STROKE空心;FILL_OR_STROKE同时实心与空心;
- setStrokeCap(Paint.Cap c):当设置画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式;
- setStrokeJoin(Paint.Join j):设置绘制时各图形的结合方式;
- setStrokeWidth(float w):当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度;
- setXfermode(Xfermode m):设置图形重叠时的处理方式;
2、文本绘制:
- setTextAlign(Path.Align a):设置绘制的文本的对齐方式;
- setTextScaleX(float s):设置文本在X轴的缩放比例,可以实现文字的拉伸效果;
- setTextSize(float s):设置字号;
- setTextSkewX(float s):设置斜体文字,s是文字倾斜度;
- setTypeFace(TypeFace tf):设置字体风格,包括粗体、斜体等;
- setUnderlineText(boolean b):设置绘制的文本是否带有下划线效果;
- setStrikeThruText(boolean b):设置绘制的文本是否带有删除线效果;
- setFakeBoldText(boolean b):模拟实现粗体文字,如果设置在小字体上效果会非常差;
- setSubpixelText(boolean b):如果设置为true则有助于文本在LCD屏幕上显示效果;
3、其他方法: - getTextBounds(String t, int s, int e, Rect b):将页面中t文本从s下标开始到e下标结束的所有字符所占的区域宽高封装到b这个矩形中;
- clearShadowLayer():清除阴影层;
- measureText(String t, int s, int e):返回t文本中从s下标开始到e下标结束的所有字符所占的宽度;
- reset():重置画笔为默认值。
这里需要就几个方法解释一下:
1、setPathEffect(PathEffect pe):设置绘制的路径的效果:
常见的有以下几种可选方案:
- CornerPathEffect:可以用圆角来代替尖锐的角;
- DathPathEffect:虚线,由短线和点组成;
- DiscretePathEffect:荆棘状的线条;
- PathDashPathEffect:定义一种新的形状并将其作为原始路径的轮廓标记;
- SumPathEffect:在一条路径中顺序添加参数中的效果;
- ComposePathEffect:将两种效果组合起来,先使用第一种效果,在此基础上应用第二种效果。
2、setXfermode(Xfermode m):设置图形重叠时的处理方式:
关于Xfermode的多种效果,我们可以参考下面一张图:
在使用的时候,我们需要通过paint. setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XXX))来设置,XXX是上图中的某种模式对应的常量参数,如DST_OUT。
这16中情况的具体解释如下:
1.PorterDuff.Mode.CLEAR:所绘制不会提交到画布上。
2.PorterDuff.Mode.SRC:显示上层绘制图片
3.PorterDuff.Mode.DST:显示下层绘制图片
4.PorterDuff.Mode.SRC_OVER:正常绘制显示,上下层绘制叠盖。
5.PorterDuff.Mode.DST_OVER:上下层都显示。下层居上显示。
6.PorterDuff.Mode.SRC_IN:取两层绘制交集。显示上层。
7.PorterDuff.Mode.DST_IN:取两层绘制交集。显示下层。
8.PorterDuff.Mode.SRC_OUT:上层绘制非交集部分。
9.PorterDuff.Mode.DST_OUT:取下层绘制非交集部分。
10.PorterDuff.Mode.SRC_ATOP:取下层非交集部分与上层交集部分
11.PorterDuff.Mode.DST_ATOP:取上层非交集部分与下层交集部分
12.PorterDuff.Mode.XOR:异或:去除两图层交集部分
13.PorterDuff.Mode.DARKEN:取两图层全部区域,交集部分颜色加深
14.PorterDuff.Mode.LIGHTEN:取两图层全部,点亮交集部分颜色
15.PorterDuff.Mode.MULTIPLY:取两图层交集部分叠加后颜色
16.PorterDuff.Mode.SCREEN:取两图层全部区域,交集部分变为透明色
5.2、Canvas类
Canvas即画布,其上可以使用Paint画笔对象绘制很多东西。
Canvas****对象中可以绘制:
drawArc():绘制圆弧;
drawBitmap():绘制Bitmap图像;
drawCircle():绘制圆圈;
drawLine():绘制线条;
drawOval():绘制椭圆;
drawPath():绘制Path路径;
drawPicture():绘制Picture图片;
drawRect():绘制矩形;
drawRoundRect():绘制圆角矩形;
drawText():绘制文本;
drawVertices():绘制顶点。
Canvas****对象的其他方法:canvas.save():把当前绘制的图像保存起来,让后续的操作相当于是在一个新图层上绘制;
canvas.restore():把当前画布调整到上一个save()之前的状态;
canvas.translate(dx, dy):把当前画布的原点移到(dx, dy)点,后续操作都以(dx, dy)点作为参照;
canvas.scale(x, y):将当前画布在水平方向上缩放x倍,竖直方向上缩放y倍;
canvas.rotate(angle):将当前画布顺时针旋转angle度。</pre>
6、onLayout()
onLayout()方法负责布局,大多数情况是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。
onLayout(boolean changed, int l, int t, int r, int b)方法有5个参数,其中changed表示这个控件是否有了新的尺寸或位置;l、t、r、b分别表示这个View相对于父布局的左/上/右/下方的位置。
以下是onLayout()方法中常用的方法:
- getChildCount():获取子View的数量;
- getChildAt(i):获取第i个子View
- getWidth/Height():获取onMeasure()中返回的宽度和高度的测量值;
- child.getLayoutParams():获取到子View的LayoutParams对象;
- child.getMeasuredWidth/Height():获取onMeasure()方法中测量的子View的宽度和高度值;
- getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
- child.layout(l, t, r, b):设置子View布局的上下左右边的坐标
7、其他方法
7.1、generateLayoutParams()
generateLayoutParams()方法用在自定义ViewGroup中,用来指明子控件之间的关系,即与当前的ViewGroup对应的LayoutParams。我们只需要在方法中返回一个我们想要使用的LayoutParams类型的对象即可。
在generateLayoutParams()方法中需要传入一个AttributeSet对象作为参数,这个对象是这个ViewGroup的属性集,系统根据这个ViewGroup的属性集来定义子View的布局规则,供子View使用。
例如,在自定义流式布局中,我们只需要关心子控件之间的间隔关系,因此我们需要在generateLayoutParams()方法中返回一个new MarginLayoutParams()即可。
7.2、onTouchEvent()
onTouchEvent()方法用来监测用户手指操作。我们通过方法中MotionEvent参数对象的getAction()方法来实时获取用户的手势,有UP、DOWN和MOVE三个枚举值,分别表示用于手指抬起、按下和滑动的动作。每当用户有操作时,就会回掉onTouchEvent()方法。
7.3、onScrollChanged()
如果我们的自定义View / ViewGroup是继承自ScrollView / HorizontalScrollView等可以滚动的控件,就可以通过重写onScrollChanged()方法来监听控件的滚动事件。
这个方法中有四个参数:l和t分别表示当前滑动到的点在水平和竖直方向上的坐标;oldl和oldt分别表示上次滑动到的点在水平和竖直方向上的坐标。我们可以通过这四个值对滑动进行处理,如添加属性动画等。
7.4、invalidate()
invalidate()方法的作用是请求View树进行重绘,即draw()方法,如果视图的大小发生了变化,还会调用layout()方法。
一般会引起invalidate()****操作的函数如下:
<pre style="margin: 0px 0px 0px 22px; white-space: pre-wrap; overflow-wrap: break-word; font-size: 12px !important; font-family: "Courier New" !important;">1) 直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身;
- 调用setSelection()方法,请求重新draw(),但只会绘制调用者本身;
- 调用setVisibility()方法,会间接调用invalidate()方法,继而绘制该View;
- 调用setEnabled()方法,请求重新draw(),但不会重新绘制任何视图,包括调用者本身。
7.5、postInvalidate()
功能与invalidate()方法相同,只是postInvalidate()方法是异步请求重绘视图。
7.6、requestLayout()
requestLayout()方法只是对View树进行重新布局layout过程(包括measure()过程和layout()过程),不会调用draw()过程,即不会重新绘制任何视图,包括该调用者本身。
7.7、requestFocus()
请求View树的draw()过程,但只会绘制需要重绘的视图,即哪个View或ViewGroup调用了这个方法,就重绘哪个视图。
8、总结
最后,让我们来总览一下自定义View / ViewGroup时调用的各种函数的顺序,如下图所示:
在这些方法中:
- onMeasure()会在初始化之后调用一到多次来测量控件或其中的子控件的宽高;
- onLayout()会在onMeasure()方法之后被调用一次,将控件或其子控件进行布局;
- onDraw()会在onLayout()方法之后调用一次,也会在用户手指触摸屏幕时被调用多次,来绘制控件。