界面开发基础

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)。举个例子,在DPI240的屏幕上,1DIP等于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.LightTheme.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.LightTheme.Material.Light.DarkActionBar等,但是这种风格只能应用在在5.0版本的手机,如果在5.0之前应用Material Design该怎么办呢?同样的引用appcompat-v7包,这个时候的Theme.AppCompat.LightTheme.AppCompat.Light.DarkActionBar就是想对应兼容的Material DesignTheme

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

View Mechanism

参考资料

Layout

LayoutInflater

参考文章

inflate

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

attachToRootTrue

(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>

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

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

我们指定了用于填充buttonlayout资源文件,然后我们告诉LayoutInflater我们想把button添加到mLinearLayout中。这里ButtonLayoutParams种类为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和其子元素。

attachToRootFalse

(1)使用addView手动添加

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

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

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 当在FragmentonCreateView()方法中填充并返回View时,要将attachToRoot设为false。如果传入true,会抛出IllegalStateException,因为指定的子View已经有父View了。你需要指定在哪里将FragmentView放进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()方法中传入的ViewGroupFragmentManager会将FragmentView添加到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、widthheight这几种。而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_widthlayout_height是不会起作用的。而在Activity中,有时候直接在activity_main中设置单一控件即可,这是因为Activity会自动在最外层包裹一个FrameLayout

Activity Framework包裹图

上一页