首页 > 代码库 > 我的Android 4 学习系列之创建用户基本界面
我的Android 4 学习系列之创建用户基本界面
目录
- 使用视图和布局
- 理解Fragment
- 优化布局
- 创建分辨率无关的用户界面
- 扩展、分组、创建和使用视图
- 使用适配器将数据绑定到视图
使用视图和布局
1. Android UI 几个基本概念
- 视图: 所有可视界面的元素(通常称为控件或者小组件)的基类。所有的UI控件(包括布局类)都是由 View 派生而来的。
- 视图组:视图类的扩展,可以包含多个子视图。通过扩展ViewGroup类,可以创建由多个相互连接的子视图组成复合控件;还可以通过扩展ViewGroup类来提供布局管理器,以帮助在Acitivity里布局控件。
- Fragment:Fragment在Android 3.0(API level 11)中引入,用于UI的各个部分。
这种封装使得Fragment特别适合针对不同的屏幕尺寸优化UI布局以及创建可重用的UI元素。
每个Fragment都包含自己的UI布局,并接受相关的输入事件,但是必须与包含它们的Acitivity紧密绑定在一起,也就是说Fragment必须签入到Activity。
- Activity: 显示给用户的窗口或者屏幕。要显示UI,就需要给Activity分配一个View(通常是一个布局或者Fragment)。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// resource
// setContentView(R.layout.activity_main);
// code
TextView myTextView = new TextView(this);
setContentView(myTextView);
myTextView.setText("Hello, Android!");
}
2. 布局
布局管理器(也叫布局)是对ViewGroup的扩展,它用来在控制子控件在UI中位置的。可以嵌套。构建复杂界面。
最常用的布局类:
- FrameLayout 最简单的布局管理器。默认从左上角开始堆放,可以用gravity属性改变其位置。添加多个子视图,新的子视图堆积在前一个子视图上面,而且每一个新的子视图可能会遮挡住上一个。
- LinearLayout 线性布局管理器。在垂直方向或者水平方向上对齐每一个子视图。在垂直方向布局会有一个布局列,水平方向则有一个布局行。LinearLayout允许为每一个子视图制定一个属性Weight,以控制子视图在可用空间内的大小。
- RelativeLayout 可以定义每一个子视图与其他子视图之间以及屏幕边界之间的相对位置。
- GridLayout Android 4.0(API Level 14)中引入,由极细的线构成的矩形网格,在一系列的行和列中布局。可以简化布局,消除嵌套,不要自己手动调整XML
--------------------------------Resource--------------------------------------
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Enter Text Bellow"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Text Goes Here!"/>
</LinearLayout>
android:layout_width="match_parent":用来扩展视图,使其填满父视图、Fragment和Activity内的可用空间。
android:layout_height="match_parent":设定包含它显示内容所需的最小尺寸(比如:显示换行文字字符串所需高度)。
-------------------------------Code-------------------------------------------
LinearLayout ll = new LinearLayout(this);
ll.setOrientation(LinearLayout.VERTICAL);
TextView myTextView = new TextView(this);
EditText myEditText = new EditText(this);
myTextView.setText("Enter Text Below");
myEditText.setText("Text Goes Here!");
int lHeight = LinearLayout.LayoutParams.MATCH_PARENT;
int lWidth = LinearLayout.LayoutParams.WRAP_CONTENT;
ll.addView(myTextView, new LinearLayout.LayoutParams(lWidth, lHeight));
ll.addView(myEditText, new LinearLayout.LayoutParams(lWidth, lHeight));
setContentView(ll);
3. 使用布局
布局的关键特征是能够适应各种各样的屏幕尺寸、分辨率和屏幕方向。
(1)LinearLayout:
大多数时候,你会用线性布局来构建一些UI元素,然后把这些UI元素嵌套到其他布局中,比如相对布局。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<Button
android:text="@string/cancel_button_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="@string/ok_button_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
<ListView
android:layout_width="match_parent"
android:layout_height="fill_parent"></ListView>
</LinearLayout>
运行效果:
(2)RelativeLayout:
非常灵活的布局方式,允许根据父元素或其他视图的位置定义每个元素在布局中的位置。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/button_bar"
android:layout_alignParentBottom="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="5dp">
<Button
android:text="@string/cancel_button_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="@string/ok_button_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
<ListView
android:id="@+id/listview_bar"
android:layout_alignParentLeft="true"
android:layout_width="match_parent"
android:layout_height="fill_parent"></ListView>
</RelativeLayout>
(3)GridLayout:
所有布局管理器中最为灵活的一种。
GridLayout使用一个随意选择的网格来放置视图。通过使用行和列延伸、Space View和Gravity属性,可以创建复杂的UI。
对于构建需要在两个方向上进行对齐的布局,网格布局特别有用。
出于性能考虑,优先考虑网格布局,而不是嵌套布局。
<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:background="#FF444444"
android:layout_gravity="fill"></ListView>
<LinearLayout
android:id="@+id/button_bar"
android:layout_gravity="fill_horizontal"
android:orientation="horizontal"
android:padding="5dp">
<Button
android:text="@string/cancel_button_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<Button
android:text="@string/ok_button_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
</LinearLayout>
<ListView
android:id="@+id/listview_bar"
android:layout_alignParentLeft="true"
android:layout_width="match_parent"
android:layout_height="fill_parent"></ListView>
</GridLayout>
使用网格布局时,不需要指定宽度和高度。这是因为每个元素默认都会包围其元素,而且layout_gravity属性被用来确定每个元素应该在那个方向上延伸。
4. 优化布局
(1)避免布局冗余:
merge的使用:当包含有merge标签的布局添加到另一布局时,该布局的merge节点会被删除,而该布局的子View会被直接添加到新的父布局中。
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView
android:id="@+id/myImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="http://www.mamicode.com/@drawable/myimage"/>
<TextView
android:id="@+id/myTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hello_world"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"/>
</merge>
merge标签结合 include标签一起使用尤其有用,include标签是用来把一个布局的内容插入到另一个布局中。
image_text_layout.xml ---------
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:id="@+id/myImageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="http://www.mamicode.com/@drawable/myimage"/>
<TextView
android:id="@+id/myTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hello_world"
android:layout_gravity="center_horizontal"
android:gravity="center_horizontal"/>
</merge>
activity_main.xml ---------
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
android:id="@+id/my_image_text_layout"
layout="@layout/image_text_layout"/>
</LinearLayout>
(2)避免使用过多的View
布局的View数不能超过80,想要在复杂的UI填充View数量变少,可以使用ViewStub。
ViewStub 就像延迟填充的include标签 ----- 一个stub代表了在父布局中指定多个子View ---- 但只有在显示的调用inflate()方法或被置为可见的时候,这个stub才会被填充。
<ViewStub
android:id="@+id/my_hello_stub"
android:layout="@layout/image_text_layout"
android:inflatedId="@+id/image_text_layout_hello"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"/>
当分别使用id 和 inflatedId 属性填充View时,一个ID已经被分配给StubView和它将成为的ViewGroup了。
当ViewStub被填充,它就会从视图层中删除掉且被它导入的View根节点所替换。如果要修改View的可见性,必须使用他们根节点的引用(通过inflate调用返回)或者通过findViewById方法找到那个View,该方法使用在相应的ViewStub节点中分配给该View的布局ID。
(3)使用Lint工具来分析布局:LInt工具能够分析布局的性能问题。
Lint可以检测UI布局性能,缺少的翻译,未使用的资源、不一致的数组大小、可访问性和国际化问题、丢失或重复的图像资源,可用性问题和manifest错误。
5. 实践:To-Do List
一个待办事项列表;一个添加新待办事项的文本框。
Resource :
--------- activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/myEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/addItemHint"
android:contentDescription="@string/addItemDescription"/>
<ListView
android:id="@+id/myListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
------- strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">To Do List</string>
<string name="hello_world">Hello world!</string>
<string name="action_settings">Settings</string>
<string name="cancel_button_text">Cancel</string>
<string name="ok_button_text">OK</string>
<string name="addItemHint">New To Do Item</string>
<string name="addItemDescription">New To Do Item</string>
</resources>
Code:
setContentView(R.layout.activity_main);
//Get Reference of UI widget
ListView myListView = (ListView)findViewById(R.id.myListView);
final EditText myEditText = (EditText)findViewById(R.id.myEditText);
final ArrayList<String> todoItems = new ArrayList<String>();
//Create ArrayAdapter to bind to ListView
final ArrayAdapter<String> aa;
aa = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1,
todoItems);
//Bind ArrayAdapter to ListView
myListView.setAdapter(aa);
myEditText.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if(event.getAction() == KeyEvent.ACTION_DOWN){
if(keyCode == KeyEvent.KEYCODE_DPAD_CENTER
|| keyCode == KeyEvent.KEYCODE_ENTER){
todoItems.add(0, myEditText.getText().toString());
aa.notifyDataSetChanged();
myEditText.setText("");
}
}
return false;
}
});
运行效果:
理解Fragment
1. Fragment 介绍
Fragment(碎片)允许将Activity拆分成多个完全独立封装的可重用的组件,每个组件有它自己的生命周期和UI布局。
Fragment最大的优势是你可以为不同大小屏幕的设备创建动态灵活的UI。
要通过Android支持包来使用Fragment,必须要保证Activity是继承自FragmentActivity类:
Public class MyActivity extends FragmentActivity
创建Fragment
Resource -----
<FrameLayout 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"
tools:context="com.example.hello.MySkeletonFragment" >
<!-- TODO: Update blank fragment layout -->
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/hello_blank_fragment" />
</FrameLayout>
Code ------
public class MySkeletonFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater
.inflate(R.layout.fragment_my_skeleton, container, false);
}
}
和Activity 不同,Fragment 不需要在manifest.xml 进行注册。这是因为Fragment只有嵌入到一个Activity时,它才能够存在,它的生命周期依赖于Activity。
Fragment 的生命周期
Fragment 特有的生命周期事件
1.从父Activity中绑定和分离Fragment。
2.创建和销毁Fragment。与Activity不同,Fragment的UI不在Onreate中初始化。
3.创建和销毁用户界面。OnCreateView与OnDestroyView。
使用OnCreateView方法来初始化Fragment:填充UI,获取它所包含的View的引用(绑定到该View的数据),然后创建所需的任何Service和Timer。一旦填充好了View Layout,该View应该从这个处理程序返回:
return Inflater.inflate(R.layout.my_fragment, container, false);
如果Fragment 和 父Activity的UI交互需要一直等到onActivityCreated事件被触发。
Fragment 状态
Fragment的命运与它所属的Activity息息相关。
Fragment Manager 介绍
每一个Activity都包含一个Fragment Manager来管理它所包含的Fragment。通过getFragementManager方法获得FragmentManager。
向Activity中添加Fragment
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/myEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/addItemHint"
android:contentDescription="@string/addItemDescription"/>
<ListView
android:id="@+id/myListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<fragment android:name="com.example.hello.MyListFragment"
android:id="@+id/my_list_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"/>
<fragment android:name="com.example.hello.DetailsFragment"
android:id="@+id/details_fragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="3"/>
</LinearLayout>
一旦一个Fragment填充后,他就变成一个ViewGroup,会在Activity内显示和管理它所包含UI。
使用FragmentTransaction
在运行时,用来在一个Activity内添加、删除和替换Fragment,使得布局变成动态的。
添加、删除和替换Fragment
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.add(R.id.ui_Container, new MyListFragment());
fragmentTransaction.commit();
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.beginTransaction()
.remove(getFragmentManager().findFragmentById(R.id.details_fragment));
fragmentTransaction.commit();
FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.details_fragment, new DetailFragment(selected_index));
fragmentTransaction.commit();
使用FragmentManager 查找Fragment
如果是通过XML布局的方式把Fragment加入到Activity中的,可以使用资源标识符。
getFragmentManager().findFragmentById(R.id.details_fragment);
如果通过FragmentTransaction添加了一个Fragment,应该把容器View的资源标识符指定给想要查找的Fragment;另外,还可以通过使用findFragmentByTag来查找在FragmentTransaction中指定了Tab标识符的Fragment。
getFragmentManager().findFragmentByTag(arg0);
这种方式在没有UI的Fragment的Activity中特别有用。因为这样Fragment并不是Acitvity View Layout的一部分,它没有一个标识符或者一个容器资源标识符,所以它无法使用findFragmentById()方法找到它。
使用Fragment填充动态的Activity布局
FragmentManager fm = getFragmentManager();
DetailsFragment detailsFragment = (DetailsFragment)fm.findFragmentById(R.id.details_container);
if(detailsFragment == null){
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.details_container, new DetailsFragment());
ft.commit();
}
首先检查这个UI是否提供之前的状态已经被提供过了。为了确保用户体验的一致性,当Activity因配置改变而重新启动时,Android会保存Fragment布局和back栈。
因为同样的原因,当运行时配置改变而创建可替代的布局时,最好考虑在所有的布局变化中,包含所有事务所包含的所有的View容器。这样做的坏处就是FragmentManager把Fragment还原到已不在新布局中的容器。
在一个给定方向的布局中删除一个Fragment容器,只需要将Fragment容器的Visibility属性设置为gone即可。
Fragment和Back栈
FragmentManager fm = getFragmentManager();
DetailsFragment detailsFragment = (DetailsFragment)fm.findFragmentById(R.id.details_container);
if(detailsFragment == null){
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.details_container, new DetailsFragment());
Fragment fragment = getFragmentManager().findFragmentById(R.id.my_list_fragment);
ft.remove(fragment);
String tag = null;
ft.addToBackStack(tag);
ft.commit();
}
使FragmentTransaction动起来
想要应用众多默认过度动画中一个,可以对任何FragmentTransaction使用setTransition方法,并传入一个FragmentTransaction.TRASITION_FRAGMENT_*常量:
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
也可以使用setCustomAnimations方法对FragmentTransaction应用自定义动画。
ft.setCustomAnimations(R.animator.slide_in_left, R.animator.slide_in_right);
Fragment和Activity之间的接口
尽管Fragment可以直接使用主Activity的FragmentManager进行通信,但通常考虑最好使用Activity做媒介,这样尽可能的让Fragment独立和松耦合。
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnFragmentInteractionListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnFragmentInteractionListener");
}
}
public interface OnFragmentInteractionListener {
// TODO: Update argument type and name
public void onFragmentInteraction(Uri uri);
}
没有用户界面的Fragment
大部分情况下Fragment是封装了UI的模块化组件,但是也可以构建没有UI的Fragment,该行为可以一直持续到Activity的重新启动。
这特别适合于:定期和UI交互的后台任务;因配置改变而导致的Activity重新启动时,保存状态变得特别重要的场合。
当Activity重新启动时,同一个Fragment的实例会被保留下来,而不是和它的父Activity一起被销毁和重新创建。虽然可以对存在UI的Fragment使用这项技术,但是并不建议这样做,更好的选择是把关联的后台任务和必要的状态移入到新的没有UI的Fragment中,根据需要让两个Fragment交互。
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.ui_container, new MyFirstFragment());
ft.add(new MyNoUIFragment(), My_No_UI_Fragment);
MyNoUIFragment mn = (MyNoUIFragment)fm.findFragmentByTag(My_No_UI_Fragment);
ft.commit();
Android Fragment 类
DialogFragment ---
ListFragment ---
WebViewFragment ---
对To-Do-List 示例使用Fragment
(1)首先,在res/layout文件夹中创建一个新的布局文件new_item_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<EditText xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/myEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/addItemHint"
android:contentDescription="@string/addItemHint" />
(2)为fragment UI创建新的Fragment:NewItemFragment
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.new_item_fragment, container, false);
return view;
}
(3)定义接口监听新事项的添加
public interface OnNewItemAddedListner{
public void onNewItemAdded(String newItem);
}
(4)创建一个变量来保存实现了接口OnNewItemAddedListner的类的引用
private OnNewItemAddedListner onNewItemAddedListner;
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
try{
onNewItemAddedListner = (OnNewItemAddedListner)activity;
} catch(ClassCastException e){
throw new ClassCastException(activity.toString() + "must implement OnNewItemAddedListner");
}
}
(5)在NewItemFragment中添加onClickListner事件。当用户添加新项,不是直接向数组中添加文本,而是把它传递到父Activity的OnNewItemAddedListner.onNewItemAdded实现中。
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.new_item_fragment, container, false);
final EditText myEditText = (EditText)view.findViewById(R.id.myEditText);
myEditText.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if(event.getAction() == KeyEvent.ACTION_DOWN){
if(keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER){
onNewItemAddedListner.onNewItemAdded(myEditText.getText().toString());
myEditText.setText("");
return true;
}
}
return false;
}
});
return view;
}
(6)创建to-do事项列表Fragment
package com.example.todolist;
import android.app.Fragment;
import android.app.ListFragment;
/**
* A simple {@link Fragment} subclass.
*
*/
public class ToDoListFragment extends ListFragment {
}
(7)重新设计MainActivity的XML布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.todolist.MainActivity" >
<fragment android:name="com.example.todolist.NewItemFragment"
android:id="@+id/newItemFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/addItemHint"
android:contentDescription="@string/addItemHint" />
<fragment android:name="com.example.todolist.ToDoListFragment"
android:id="@+id/toDoListFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
(8)修改MainActivity.java
public class MainActivity extends Activity implements NewItemFragment.OnNewItemAddedListner {
private ArrayList<String> todoItems;
private ArrayAdapter<String> aa;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//获取Fragment的引用
FragmentManager fm = getFragmentManager();
ToDoListFragment todoListFragment = (ToDoListFragment)fm.findFragmentById(R.id.toDoListFragment);
todoItems = new ArrayList<String>();
aa = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, todoItems);
todoListFragment.setListAdapter(aa);
}
//前面定义接口的具体实现
@Override
public void onNewItemAdded(String newItem) {
todoItems.add(newItem);
aa.notifyDataSetChanged();
}
}
Android widget 工具箱
- TextView
- EditText
- Chronometer 一个TextView的扩展,实现一个简单的计时器
- ListView 显示一个对象数组的toString值,其中每一个条目都是一个TextView.
- Spinner 一个组合控件,由一个TextView和一个ListView组成。
- Button
- ToggleButton 两种状态的按钮
- ImageButton
- RadioButton
- ViewFlipper 允许将一组View定义为一个水平行的ViewGroup,其中任意时刻只有一个View可见,并且可见View之间切换以动画形式表现出来。
- VideoView
- QuickContactBadge 与联系人相关信息有关,可选择联系人信息
- ViewPager 可以水平滚动的UI,通过点击或者左右拖拽的方式在不同View之间切换。
创建分辨率无关的用户界面
修改现有的视图
修改to-do-list,先看效果图
下面我们一步一步来做:
(1)因为ListView 每一个条目其实是TextView,那么只需要改变每一个条目就能影响ListView。
创建一个新的扩展了TextView类ToDoListItem类。重写onDraw方法。
public class ToDoListItemView extends TextView {
private Paint marginPaint;
private Paint linePaint;
private int pagerColor;
private float margin;
public ToDoListItemView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public ToDoListItemView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ToDoListItemView(Context context) {
super(context);
init();
}
private void init() {
}
@Override
public void onDraw(Canvas canvas){
}
}
(2)创建资源文件:res/values/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="notepad_paper">#EEF8E0A0</color>
<color name="notepad_lines">#FF0000FF</color>
<color name="notepad_margin">#90FF0000</color>
<color name="notepad_text">#AA0000FF</color>
</resources>
(3) 创建dimens.xml文件,为边缘和宽度添加新值
<dimen name="notepad_margin">30dp</dimen>
(4)通过上面的资源文件就可以绘制新的控件,首先创建相关Paint对象的初始化
private Paint marginPaint;
private Paint linePaint;
private int pagerColor;
private float margin;
private void init() {
Resources myResources = getResources();
//创建onDraw中使用的画刷
marginPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
marginPaint.setColor(myResources.getColor(R.color.notepad_margin));
linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
linePaint.setColor(myResources.getColor(R.color.notepad_lines));
//获得页面背景和边缘宽度
pagerColor = myResources.getColor(R.color.notepad_paper);
margin = myResources.getDimension(R.dimen.notepad_margin);
}
(5)重写onDraw方法
@Override
public void onDraw(Canvas canvas){
//绘制页面的颜色
canvas.drawColor(pagerColor);
//绘制边缘
canvas.drawLine(0, 0, 0, getMeasuredHeight(), linePaint);
canvas.drawLine(0, getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight(), linePaint);
//绘制margin
canvas.drawLine(margin, 0, margin, getMeasuredHeight(), marginPaint);
//移动文本,让它跨越边缘
canvas.save();
canvas.translate(margin, 0);
super.onDraw(canvas);
canvas.restore();
}
(6)创建一个新的资源res/layout/todolist_item.xml资源来指定每一个To-Do List条目
<?xml version="1.0" encoding="utf-8"?>
<com.example.todolist.ToDoListItemView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
android:scrollbars="vertical"
android:textColor="@color/notepad_text"
android:fadingEdge="vertical" />
(7) 最后一步在OnCreate中改变ArrayAdapter传入的参数。使用R.Layout.todolist_item
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//获取Fragment的引用
FragmentManager fm = getFragmentManager();
ToDoListFragment todoListFragment = (ToDoListFragment)fm.findFragmentById(R.id.toDoListFragment);
todoItems = new ArrayList<String>();
aa = new ArrayAdapter<String>(this, R.layout.todolist_item, todoItems);
todoListFragment.setListAdapter(aa);
}
创建复合控件
当创建复合控件时,必须对他包含的视图的布局、外观、交互的定义。复合控件是通过扩展一个ViewGroup(通常是一个布局)来创建的。因此,要创建一个新的复合控件,首先需要选择一个最适合放置子控件的布局类,然后扩展该类。
package com.example.hello;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.LinearLayout;
public class MycompoudView extends LinearLayout {
public MycompoudView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MycompoudView(Context context){
super(context);
}
}
设计复合视图的UI布局的首选方法是使用外部资源。
<?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:orientation="vertical" >
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/clearButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Clear" />
</LinearLayout>
填充布局资源,获得控件的引用,挂钩功能(还未实现)
private EditText editText;
private Button button;
public MycompoudView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//使用布局资源填充视图
String infService = Context.LAYOUT_INFLATER_SERVICE;
LayoutInflater li;
li = (LayoutInflater)getContext().getSystemService(infService);
li.inflate(R.layout.clearable_edit_text, this, true);
//获得对子控件的引用
editText = (EditText)findViewById(R.id.editText);
button = (Button)findViewById(R.id.clearButton);
//挂钩这个功能
hookupButton();
}
使用布局创建简单的复合控件
通过创建一个XML资源来封装要重用的UI模式,以后在创建Activity或Fragment的UI时,可以把他们的布局资源定义使用include标签导入。使用include标签还能够重写所包含布局的根节点的id和layout参数:
<include layout="@layout/clearable_edit_text"
android:id="@+id/add_new_entry_input"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="top" />
创建定制的视图
对View 或者 SurfaceView扩展。
(1)创建新的可视界面
(2)定制可访问性事件
(3)创建一个罗盘视图的Demo
效果图:
制作步骤:
(1)创建新的项目Compass,新增类CompassView扩展自View
public class CompassView extends View {
public CompassView(Context context) {
super(context);
initCompassView();
}
public CompassView(Context context, AttributeSet attrs) {
super(context, attrs);
initCompassView();
}
public CompassView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initCompassView();
}
}
(2)罗盘应该是一个正圆,而且应该占据画布允许的尽可能大的空间。因此重写onMeasure方法来计算最短边的长度,然后使用这个值并通过setMeasuredDimension来设置高度和宽度。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
//罗盘是一个填充尽可能多的空间的圆,通过设置最短的边界,高度和宽度来设置测量的尺寸
int measuredWidth = measure(widthMeasureSpec);
int measureHeight = measure(heightMeasureSpec);
int d = Math.min(measuredWidth, measureHeight);
setMeasuredDimension(d, d);
}
private int measure(int measureSpec) {
int result = 0;
// 对测量说明进行编码
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.UNSPECIFIED){
//如果没有指定边界,则返回默认大小200
result = 200;
}
else {
//由于希望填充可用的空间,所以要返回整个可用的空间的边界
result = specSize;
}
return result;
}
(3)修改main_acitivity.xml,使用新的视图
<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"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.compass.MainActivity" >
<com.example.compass.CompassView
android:id="@+id/compassView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
(4)修改资源res/values/strings.xml 与 res/values/colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Compass</string>
<string name="action_settings">Settings</string>
<string name="cardinal_north">N</string>
<string name="cardinal_east">E</string>
<string name="cardinal_south">S</string>
<string name="cardinal_west">W</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<color name="background_color">#F555</color>
<color name="marker_color">#AFFF</color>
<color name="text_color">#AFFF</color>
</resources>
(5)CompassView类中添加新的属性
private float bearing;
public void setBearing(float _bearing){
bearing = _bearing;
}
public float getBearing(){
return bearing;
}
(6)完成initCompassView方法,准备绘制罗盘所需的Paint等相关对象
private Paint markerPaint;
private Paint textPaint;
private Paint circlePaint;
private String northString;
private String eastString;
private String southString;
private String westString;
private int textHeight;
private void initCompassView() {
setFocusable(true);
Resources r = getResources();
circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
circlePaint.setColor(r.getColor(R.color.background_color));
circlePaint.setStrokeWidth(1);
circlePaint.setStyle(Paint.Style.FILL_AND_STROKE);
northString = r.getString(R.string.cardinal_north);
eastString = r.getString(R.string.cardinal_east);
southString = r.getString(R.string.cardinal_south);
westString = r.getString(R.string.cardinal_west);
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(r.getColor(R.color.text_color));
textHeight = (int)textPaint.measureText("yY");
markerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
markerPaint.setColor(r.getColor(R.color.marker_color));
}
(7)开始绘制,见注解
@Override
protected void onDraw(Canvas canvas){
//找到控制中心,并将最小边的长度作为罗盘的半径存起来
int mMeasuredWidth = getMeasuredWidth();
int mMeasureHeight = getMeasuredHeight();
int cx = mMeasuredWidth / 2;
int cy = mMeasureHeight / 2;
int radius = Math.min(cx, cy);
//使用drawCircle方法画出罗盘字盘的边界,并为其背景着色
canvas.drawCircle(cx, cy, radius, circlePaint);
//这个罗盘是通过旋转它的字盘来显示当前的方向的,所以当前的方向总是指向设备的顶部。
//要实现这一个功能,需要把画布向与当前方向相反的方向旋转
canvas.save();
canvas.rotate(-bearing, cx, cy);
//绘制标记了,把画布旋转一圈,并且每15度画一个标记,每45度画一个方向缩写
int textWidth = (int)textPaint.measureText("W");
int cardinalX = cx - textWidth / 2;
int cardinalY = cy - radius + textHeight;
//每15度绘制一个标记,每45度绘制一个文本
for(int i = 0; i < 24; i++){
//绘制一个标记
canvas.drawLine(cx, cy-radius, cx, cy-radius+10, markerPaint);
canvas.save();
canvas.translate(0, textHeight);
//绘制基本方位
if(i % 6 == 0){
String dirString = "";
switch(i){
case(0):{
dirString = northString;
int arrowY = 2*textHeight;
canvas.drawLine(cx, arrowY, cx-5, 4*textHeight, markerPaint);
canvas.drawLine(cx, arrowY, cx+5, 4*textHeight, markerPaint);
canvas.drawLine(cx-5, 4*textHeight, cx, 6*textHeight, markerPaint);
canvas.drawLine(cx+5, 4*textHeight, cx, 6*textHeight, markerPaint);
break;
}
case(6):dirString = eastString; break;
case(12):dirString = southString; break;
case(18):dirString = westString; break;
}
canvas.drawText(dirString, cardinalX, cardinalY, textPaint);
}
else if(i % 3 == 0){
//每45度绘制文本
String angle = String.valueOf(i*15);
float angleTextWidth = textPaint.measureText(angle);
int angleTextX = (int)(cx-angleTextWidth/2);
int angleTextY = cy-radius+textHeight;
canvas.drawText(angle, angleTextX, angleTextY, textPaint);
}
canvas.restore();
canvas.rotate(15, cx, cy);
}
canvas.restore();
}
(8)添加可访问性支持。罗盘视图以可视方式显示方向,所以为了提高可访问性,当方向变化时,需要广播一个可访问性事件,说明文本发生了变化。
public void setBearing(float _bearing){
bearing = _bearing;
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
}
(9)重写dispatchPopulateAccessibilityEvent,将当前方向用作可访问性事件使用的內容值
@SuppressWarnings("deprecation")
@Override
public boolean dispatchPopulateAccessibilityEvent(final AccessibilityEvent event){
super.dispatchPopulateAccessibilityEvent(event);
if(isShown()){
String bearingStr = String.valueOf(bearing);
if(bearingStr.length() > AccessibilityEvent.MAX_TEXT_LENGTH){
bearingStr = bearingStr.substring(0, AccessibilityEvent.MAX_TEXT_LENGTH);
event.getText().add(bearingStr);
return true;
}
}
return false;
}
Demo到此结束,见开始运行效果。
Adapter简介
Adapter用来把数据绑定到AdapterView扩展类的视图组(如ListView、Gallery)。Adapter负责创建代表所绑定父视图中的底层数据的子视图。
可以创建自己的Adapter类,构建自己的由AdapterView派生的控件。
部分原生的Adapter
- ArrayAdapter 使用数组中每个对象的toString值来填充和创建视图。其他的构造函数允许你使用更复杂的布局,或者通过扩展类把数据绑定到更复杂的布局
- SimpleCursorAdapter 可以把一个布局中的视图和(通常是从Content Provider 查询返回的)游标的特定列绑定到一起。可以指定一个将被填充以显示每个子视图的XML布局,然后把游标中的每一列和那个布局中的特定视图进行绑定。Adappter将每个游标项创建一个新的视图,并将布局填充到视图中,使用游标中对应列的值填充布局中的每一个视图。
1. 定制To-Do List ArrayAdapter
(1) 大多数情况下使用ArrayAdapter,需要对其进行扩展,并重写getView方法来布局视图分配对象属性。
创建一个新的ToDoItem类,定义Task和Date属性,重写toString
package com.paad.todolist;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ToDoItem {
String task;
Date created;
public String getTask() {
return task;
}
public Date getCreated() {
return created;
}
public ToDoItem(String _task) {
this(_task, new Date(java.lang.System.currentTimeMillis()));
}
public ToDoItem(String _task, Date _created) {
task = _task;
created = _created;
}
@Override
public String toString() {
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yy");
String dateString = sdf.format(created);
return "(" + dateString + ") " + task;
}
}
(2)在ToDoListActivity,修改ArrayList和ArrayAdapter变量类型来存储ToDoItem对象,而不是字符串。然后需要修改onCreate更新变量的初始化(initialization)
private ArrayList<ToDoItem> todoItems;
private ToDoItemAdapter aa;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Inflate your view
setContentView(R.layout.main);
// Get references to the Fragments
FragmentManager fm = getFragmentManager();
ToDoListFragment todoListFragment =
(ToDoListFragment)fm.findFragmentById(R.id.TodoListFragment);
// Create the array list of to do items
todoItems = new ArrayList<ToDoItem>();
// Create the array adapter to bind the array to the ListView
int resID = R.layout.todolist_item;
aa = new ToDoItemAdapter(this, resID, todoItems);
// Bind the array adapter to the ListView.
todoListFragment.setListAdapter(aa);
}
(3)更新onNewItemAdded处理程序来支持ToDoItem对象
public void onNewItemAdded(String newItem) {
ToDoItem newTodoItem = new ToDoItem(newItem);
todoItems.add(0, newTodoItem);
aa.notifyDataSetChanged();
}
(4)修改todolist_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">
<TextView
android:id="@+id/rowDate"
android:background="@color/notepad_paper"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:padding="10dp"
android:scrollbars="vertical"
android:fadingEdge="vertical"
android:textColor="#F000"
android:layout_alignParentRight="true"
/>
<com.paad.todolist.ToDoListItemView
android:id="@+id/row"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
android:scrollbars="vertical"
android:fadingEdge="vertical"
android:textColor="@color/notepad_text"
android:layout_toLeftOf="@+id/rowDate"
/>
</RelativeLayout>
(5)ArrayAdapter 的扩展类ToDoItemAdapter,专门用于ToDoItem
package com.paad.todolist;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;
public class ToDoItemAdapter extends ArrayAdapter<ToDoItem> {
int resource;
public ToDoItemAdapter(Context context,
int resource,
List<ToDoItem> items) {
super(context, resource, items);
this.resource = resource;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
LinearLayout todoView;
ToDoItem item = getItem(position);
String taskString = item.getTask();
Date createdDate = item.getCreated();
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yy");
String dateString = sdf.format(createdDate);
if (convertView == null) {
todoView = new LinearLayout(getContext());
String inflater = Context.LAYOUT_INFLATER_SERVICE;
LayoutInflater li;
li = (LayoutInflater)getContext().getSystemService(inflater);
li.inflate(resource, todoView, true);
} else {
todoView = (LinearLayout) convertView;
}
TextView dateView = (TextView)todoView.findViewById(R.id.rowDate);
TextView taskView = (TextView)todoView.findViewById(R.id.row);
dateView.setText(dateString);
taskView.setText(taskString);
return todoView;
}
}
(6)回到ToDoListActivity,使用ToDoItemAdapter来代替ArrayAdapter声明
private ArrayList<ToDoItem> todoItems;
(7)onCreate内,使用新的ToDoItemAdapter来代替ArrayAdapter<ToDoItem>实例化
aa = new ToDoItemAdapter(this, resID, todoItems);
到此完成。
2. SimpleCursorAdapter 的使用跟Content Provider,游标和游标加载器关系密切,这块只能留到后面学习了。
我的Android 4 学习系列之创建用户基本界面