界面开发基础

Introduction

Material Design

Material Design 是由 Google 提出的关于如何构建一个 Android 应用的完整的指导方案,该方案不仅仅可以被用于 Android 应用的设计,同样可以被用于 Web 端的设计。目前在 Web 端上已经出现了大量践行 Material Design 的开源的组件库。在开发个 APP 的过程种,Android 提供了多个辅助库来帮助开发者实践这些设计指南。其中最重要的几个库就是:

  • com.android.support:appcompat-v7:23.1.0

  • com.android.support:design :23.1.0

总而言之,如果使用 Android Studio 来开发应用的话,这两个库会被自动作为依赖引入项目中。在开发应用时,一个重要的方面就是应用的颜色模式的选择,而 Material Design 的原则中也阐述了如何来选择颜色。

ShowCase

plaid

Screen & Size

参考资料

DIP(Device Independent Pixels,dp)

概念

解释

屏幕尺寸 Screen Size

即显示屏幕的实际大小,按照屏幕的对角线进行测量。为简单起见,Android 把所有的屏幕大小分为四种尺寸:小,普通,大,超大(分别对应:small, normal, large, and extra large)。应用程序可以为这四种尺寸分别提供不同的自定义屏幕布局-平台将根据屏幕实际尺寸选择对应布局进行渲染,这种选择对于程序侧是透明的。

屏幕长宽比 sAspect ratio

长宽比是屏幕的物理宽度与物理高度的比例关系。应用程序可以通过使用限定的资源来为指定的长宽比提供屏幕布局资源。

屏幕分辨率 Resolution

在屏幕上显示的物理像素总和。需要注意的是:尽管分辨率通常用宽 x 高表示,但分辨率并不意味着具体的屏幕长宽比。在 Andorid 系统中,应用程序不直接使用分辨率。

密度 Density

根据像素分辨率,在屏幕指定物理宽高范围内能显示的像素数量。在同样的宽高区域,低密度的显示屏能显示的像素较少,而高密度的显示屏则能显示更多的像素。屏幕密度非常重要,因为其它条件不变的情况下,一共宽高固定的 UI 组件(比如一个按钮)在在低密度的显示屏上显得很大,而在高密度显示屏上看起来就很小。

密度无关的像素( DIP )

指一个抽象意义上的像素,程序用它来定义界面元素。它作为一个与实际密度无关的单位,帮助程序员构建一个布局方案(界面元素的宽度,高度,位置)。一个与密度无关的像素,在逻辑尺寸上,与一个位于像素密度为 160DPI 的屏幕上的像素是一致的,这也是 Android 平台所假定的默认显示设备。在运行的时候,平台会以目标屏幕的密度作为基准,“透明地”处理所 有需要的 DIP 缩放操作。要把密度无关像素转换为屏幕像素,可以用这样一个简单的公式: pixels = dips * (density / 160)。举个例子,在 DPI 为 240 的屏幕上,1 个 DIP 等于 1.5 个物理像素。我们强烈推荐你用 DIP 来定义你程序的界面布局,因为这样可以保证你的 UI 在各种分辨率的屏幕上都可以正常显示

尺寸获取

#获取基本属性信息
DisplayMetrics metric = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metric);
int width = metric.widthPixels; // 屏幕宽度(像素)
int height = metric.heightPixels; // 屏幕高度(像素)
float density = metric.density; // 屏幕密度(0.75 / 1.0 / 1.5)
int densityDpi = metric.densityDpi; // 屏幕密度DPI(120 / 160 / 240)
// 获取屏幕密度(方法1)
int screenWidth = getWindowManager().getDefaultDisplay().getWidth(); // 屏幕宽(像素,如:480px)
int screenHeight = getWindowManager().getDefaultDisplay().getHeight(); // 屏幕高(像素,如:800p)
Log.e(TAG + " getDefaultDisplay", "screenWidth=" + screenWidth + "; screenHeight=" + screenHeight);
// 获取屏幕密度(方法2)
DisplayMetrics dm = new DisplayMetrics();
dm = getResources().getDisplayMetrics();
float density = dm.density; // 屏幕密度(像素比例:0.75/1.0/1.5/2.0)
int densityDPI = dm.densityDpi; // 屏幕密度(每寸像素:120/160/240/320)
float xdpi = dm.xdpi;
float ydpi = dm.ydpi;
Log.e(TAG + " DisplayMetrics", "xdpi=" + xdpi + "; ydpi=" + ydpi);
Log.e(TAG + " DisplayMetrics", "density=" + density + "; densityDPI=" + densityDPI);
screenWidth = dm.widthPixels; // 屏幕宽(像素,如:480px)
screenHeight = dm.heightPixels; // 屏幕高(像素,如:800px)
Log.e(TAG + " DisplayMetrics(111)", "screenWidth=" + screenWidth + "; screenHeight=" + screenHeight);
// 获取屏幕密度(方法3)
dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
density = dm.density; // 屏幕密度(像素比例:0.75/1.0/1.5/2.0)
densityDPI = dm.densityDpi; // 屏幕密度(每寸像素:120/160/240/320)
xdpi = dm.xdpi;
ydpi = dm.ydpi;
Log.e(TAG + " DisplayMetrics", "xdpi=" + xdpi + "; ydpi=" + ydpi);
Log.e(TAG + " DisplayMetrics", "density=" + density + "; densityDPI=" + densityDPI);
int screenWidthDip = dm.widthPixels; // 屏幕宽(dip,如:320dip)
int screenHeightDip = dm.heightPixels; // 屏幕宽(dip,如:533dip)
Log.e(TAG + " DisplayMetrics(222)", "screenWidthDip=" + screenWidthDip + "; screenHeightDip=" + screenHeightDip);
screenWidth = (int)(dm.widthPixels * density + 0.5f); // 屏幕宽(px,如:480px)
screenHeight = (int)(dm.heightPixels * density + 0.5f); // 屏幕高(px,如:800px)
Log.e(TAG + " DisplayMetrics(222)", "screenWidth=" + screenWidth + "; screenHeight=" + screenHeight);

Components Size(组件尺寸)

StatusBar

Android 中经常需要获取到状态栏的高度来判断屏幕的尺寸,常用的获取 StatusBar 的代码为:

Rect rectgle= new Rect();
Window window= getWindow();
window.getDecorView().getWindowVisibleDisplayFrame(rectgle);
int StatusBarHeight= rectgle.top;
int contentViewTop=
window.findViewById(Window.ID_ANDROID_CONTENT).getTop();
int TitleBarHeight= contentViewTop - StatusBarHeight;
Log.i("*** Jorgesys :: ", "StatusBar Height= " + StatusBarHeight + " , TitleBar Height = " + TitleBarHeight);
而如果在Activity的onCreate函数中需要获取Status的高度,则为如下方法:
public int getStatusBarHeight() {
int result = 0;
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = getResources().getDimensionPixelSize(resourceId);
}
return result;
}

Style & Themes

Theme

  • Hoho Theme

在 4.0 之前 Android 可以说是没有设计可言的,在 4.0 之后推出了 Android Design,从此 Android 在设计上有了很大的改善,而在程序实现上相应的就是 Holo 风格,所以你看到有类似 Theme.Holo.Light、 Theme.Holo.Light.DarkActionBar 就是 4.0 的设计风格,但是为了让 4.0 之前的版本也能有这种风格怎么办呢?这个时候就不得不引用 v7 包了,所以对应的就有 Theme.AppCompat.Light、 Theme.AppCompat.Light.DarkActionBar,如果你的程序最小支持的版本是 4.0,那么可以不用考虑 v7 的兼容,所以在目前来看,我个人建议不用考虑兼容。

  • Material Design Theme

今年的 5.0 版本,Android 推出了 Material Design 的概念,这是在设计上 Android 的又一大突破。对应的程序实现上就有 Theme.Material.Light、 Theme.Material.Light.DarkActionBar 等,但是这种风格只能应用在在 5.0 版本的手机,如果在 5.0 之前应用 Material Design 该怎么办呢?同样的引用 appcompat-v7 包,这个时候的 Theme.AppCompat.Light、 Theme.AppCompat.Light.DarkActionBar 就是想对应兼容的 Material Design 的 Theme。

compile ‘com.android.support:appcompat-v7:21.0.3’ 中的 21 代表 API level 21 推出的兼容包,所以如果你引用的是 21 之前的版本,则默认这些 Theme.AppCompat.Light 是 Holo 风格的,从 21 开始的版本默认是 Material 风格

View Mechanism

参考资料

Layout

LayoutInflater

参考文章

inflate

如果 attachToRoot 是 true 的话,那第一个参数的 layout 文件就会被填充并附加在第二个参数所指定的 ViewGroup 内。方法返回结合后的 View,根元素是第二个参数 ViewGroup。如果是 false 的话,第一个参数所指定的 layout 文件会被填充并作为 View 返回。这个 View 的根元素就是 layout 文件的根元素。不管是 true 还是 false,都需要 ViewGroup 的 LayoutParams 来正确的测量与放置 layout 文件所产生的 View 对象。

attachToRoot 为 True

(1)子元素尺寸依赖于 ViewGroup 假设我们在 XML layout 文件中写了一个 Button 并指定了宽高为 match_parent。

<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/custom_button">
</Button>

现在我们想动态地把这个按钮添加进 Fragment 或 Activity 的 LinearLayout 中。如果这里 LinearLayout 已经是一个成员变量 mLinearLayout 了,我们只需要通过如下代码达成目标:

inflater.inflate(R.layout.custom_button, mLinearLayout, true);

我们指定了用于填充 button 的 layout 资源文件,然后我们告诉 LayoutInflater 我们想把 button 添加到 mLinearLayout 中。这里 Button 的 LayoutParams 种类为 LinearLayout.LayoutParams。

下面的代码也有同样的效果。LayoutInflater 的两个参数的 inflate()方法自动将 attachToRoot 设置为 true。

inflater.inflate(R.layout.custom_button, mLinearLayout);

(2)自定义 View 我们看一个 layout 文件中根元素有标签的例子。标签标识着这个 layout 文件的根 ViewGroup 可以有多种类型。

public class MyCustomView extends LinearLayout {
...
private void init() {
LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.view_with_merge_tag, this);
}
}

这就是一个很好的使用 attachToRoot 的例子。这个例子中 layout 文件没有 ViewGroup 作为根元素,所以我们指定我们自定义的 LinearLayout 作为根元素。如果 layout 文件有一个 FrameLayout 作为根元素而不是,那么 FrameLayout 和它的子元素都可以正常填充,而后都会被添加到 LinearLayout 中,LinearLayout 是根 ViewGroup,包含着 FrameLayout 和其子元素。

attachToRoot 为 False

(1)使用 addView 手动添加

Button button = (Button) inflater.inflate(R.layout.custom_button, mLinearLayout, false);
mLinearLayout.addView(button);

这两行代码与刚才 attachToRoot 为 true 时的一行代码等效。通过传入 false,我们告诉 LayoutInflater 我们不暂时还想将 View 添加到根元素 ViewGroup 中,意思是我们一会儿再添加。在这个例子中,一会儿再添加就是在 inflate()后调用 addView()方 法。 (2)RecyclerView 子元素 每一个 RecyclerView 的子元素都要在 attachToRoot 设置为 false 的情况下填充。这里子 View 在 onCreateViewHolder()中填充。

public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(getActivity());
View view = inflater.inflate(android.R.layout.list_item_recyclerView, parent, false);
return new ViewHolder(view);
}

RecyclerView 负责决定什么时候展示它的子 View,这个不由我们决定。在任何我们不负责将 View 添加进 ViewGroup 的情况下都应该将 attachToRoot 设置为 false。 (3)Fragment 当在 Fragment 的 onCreateView()方法中填充并返回 View 时,要将 attachToRoot 设为 false。如果传入 true,会抛出 IllegalStateException,因为指定的子 View 已经有父 View 了。你需要指定在哪里将 Fragment 的 View 放进 Activity 里,而添加、移除或替换 Fragment 则是 FragmentManager 的事情。

FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.root_viewGroup);
if (fragment == null) {
fragment = new MainFragment();
fragmentManager.beginTransaction().add(R.id.root_viewGroup, fragment).commit();
}

上面代码中 root_viewGroup 就是 Activity 中用于放置 Fragment 的容器,它会作为 inflate()方法中的第二个参数被传入 onCreateView()中。它也是你在 inflate()方法中传入的 ViewGroup。FragmentManager 会将 Fragment 的 View 添加到 ViewGroup 中,你可不想添加两次。

public View onCreateView(LayoutInflater inflater, ViewGroup parentViewGroup, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout, parentViewGroup, false);
return view;
}

具体使用时候要注意:

  • 如果可以传入 ViewGroup 作为根元素,那就传入它。但是避免将 null 作为根 ViewGroup 传入。

  • 自定义 View 时很适合将 attachToRoot 设置为 true,但是不要在 View 已经被添加进 ViewGroup 时传入 true。

Widgets

Attribute

ID

有时候在 Android Studio 里面设置动态的 ID 会报错,可以通过创建资源的方式:

create folder res/values/ids.xmland

<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="refresh" type="id"/>
<item name="settings" type="id"/>
</resources>

in Activity class call like this

ImageView refreshImg = new ImageView(activity);
ImageView settingsImg = new ImageView(activity);
refreshImg.setId(R.id.refresh);
settingsImg .setId(R.id.settings);

Size

常用的控件layout_width、layout_height、width、height这几种。而layout_*这一系列的布局只有用于被layout包裹起来的情况下才会起作用。譬如:
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="300dp"
android:layout_height="80dp"
android:text="Button" >
</Button>

    这个控件如果直接使用 inflate 方法转化成 View 的话它其中设置的 layout_width 与 layout_height 是不会起作用的。而在 Activity 中,有时候直接在 activity_main 中设置单一控件即可,这是因为 Activity 会自动在最外层包裹一个 FrameLayout: