0%

Volley是Google在Google I/O 2013上发布的一个网络框架,主要功能:web接口请求,网络图片异步下载,支持缓存。volley只是定义了缓存以及Request的接口,具体实现可以自己定义,例如lru磁盘缓存,内存缓存,下载图片的ImageRequest. Volley的源代码里包含了一些实现,都在com.android.volley.toolbox包里,包括磁盘缓存、json请求,图片请求。还定义了一个继承自ImageView的NetworkImageView,可以异步载入网络图片。 项目地址: https://android.googlesource.com/platform/frameworks/volley/ 可能需要翻墙。 下面写个小例子,是请求百度图片api的,给各位参考下. 图方便,我把volley的源代码拷到自己项目里了. 百度图片接口地址为:http://image.baidu.com/channel/listjson?pn=42&rn=42&tag1=%E6%98%8E%E6%98%9F&tag2=%E6%98%9F%E9%97%BB%E6%98%9F%E4%BA%8B&ftags=&sorttype=0&ie=utf8&oe=utf-8&fr=channel&app=img.browse.channel.star 各位可以先看一下结构,针对接口的返回,定义一下Model,ImageListResponse.java:

import java.util.ArrayList;
public class ImageListResponse {
    ArrayList data;
    int totalNum;
    public ArrayList getData() {
        return data;
    }

    public void setData(ArrayList data) {
        this.data = data;
    }

    public class BaiduImage{
        String id,abs,desc,tag,date,image_url;

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getAbs() {
            return abs;
        }

        public void setAbs(String abs) {
            this.abs = abs;
        }

        public String getDesc() {
            return desc;
        }

        public void setDesc(String desc) {
            this.desc = desc;
        }

        public String getTag() {
            return tag;
        }

        public void setTag(String tag) {
            this.tag = tag;
        }

        public String getDate() {
            return date;
        }

        public void setDate(String date) {
            this.date = date;
        }

        public String getImage_url() {
            return image_url;
        }

        public void setImage_url(String image_url) {
            this.image_url = image_url;
        }
    }
}

定义一个Request,继承自JsonRequest(就是为了用它实现的Listener,因为Request接口是没有实现deliverResponse方法的),ListRequest.java:

import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Response;
import com.android.volley.toolbox.HttpHeaderParser;
import com.android.volley.toolbox.JsonRequest;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;

import java.io.UnsupportedEncodingException;


public class ListRequest extends JsonRequest {
    public ListRequest(Response.Listener listener,Response.ErrorListener errorListener) {
        super(Method.GET, "http://image.baidu.com/channel/listjson?pn=42&rn=42&tag1=%E6%98%8E%E6%98%9F&tag2=%E6%98%9F%E9%97%BB%E6%98%9F%E4%BA%8B&ftags=&sorttype=0&ie=utf8&oe=utf-8&fr=channel&app=img.browse.channel.star",null,listener, errorListener);
        //用来取消请求的
        setTag(listener);
    }

    @Override
    protected Response parseNetworkResponse(NetworkResponse response) {
        //配合Gson,转换成我们定义的ImageListResponse
        try {
            String json = new String(
                    response.data, HttpHeaderParser.parseCharset(response.headers));
            Gson gson = new Gson();
            return Response.success(
                    gson.fromJson(json, ImageListResponse.class), HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (JsonSyntaxException e) {
            return Response.error(new ParseError(e));
        }
    }
}

Activity里使用:

import android.app.Activity;
import android.os.Bundle;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.Volley;

import java.util.ArrayList;

public class MainActivity extends Activity implements Response.Listener,Response.ErrorListener{

    RequestQueue requestQueue;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        //初始化
        requestQueue = Volley.newRequestQueue(this);
        //添加请求
        requestQueue.add(new ListRequest(this,this));

    }

    @Override
    public void onResponse(ImageListResponse response) {
       ArrayList images = response.data;
        for (ImageListResponse.BaiduImage image:images) {
            String imageUrl=image.getImage_url();
            if(imageUrl!=null)
                System.out.println(imageUrl);
        }
    }

    @Override
    public void onErrorResponse(VolleyError error) {

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //取消请求,参数是tag
        requestQueue.cancelAll(this);
        requestQueue.stop();
    }
}

Volley判断是否需要刷新缓存是使用服务端设置的,会考虑服务端返回header里的Cache-Control的Expires。但是有时候接口并不返回这些东西,这种情况下,volley设置的缓存ttl就是0,也就是相当于没有缓存,每次都会从网络请求,参考com.android.volley.toolbox.HttpHeaderParser. 这个时候,如果我们需要强制缓存,可以继承HttpHeaderParser,重载parseCacheHeaders方法.

package com.android.volley.helper;

import com.android.volley.Cache;
import com.android.volley.NetworkResponse;
import com.android.volley.toolbox.HttpHeaderParser;

/**
 * 自定义的HeaderParser,跟默认的比,可以强制缓存,忽略服务器的设置
 */
public class CustomHttpHeaderParser extends HttpHeaderParser {
    /**
     * Extracts a {@link com.android.volley.Cache.Entry} from a {@link com.android.volley.NetworkResponse}.
     *
     * @param response The network response to parse headers from
     * @param cacheTime 缓存时间,如果设置了这个值,不管服务器返回是否可以缓存,都会缓存,一天为1000*60*60*24
     * @return a cache entry for the given response, or null if the response is not cacheable.
     */
    public static Cache.Entry parseCacheHeaders(NetworkResponse response,long cacheTime) {
        Cache.Entry entry=parseCacheHeaders(response);
        long now = System.currentTimeMillis();
        long softExpire=now+cacheTime;
        entry.softTtl = softExpire;
        entry.ttl = entry.softTtl;
        return entry;
    }
}

然后在Request的parseNetworkResponse方法里用CustomHttpHeaderParser.parseCacheHeaders(NetworkResponse response,long cacheTime)替代HttpHeaderParser.parseCacheHeaders(NetworkResponse response). 关于NetWorkImageView,调用setImageUrl(String url, ImageLoader imageLoader)方法设置图片,setDefaultImageResId(int defaultImage)设置在图片没下载完时显示的默认图片,setErrorImageResId(int errorImage)设置在图片下载失败时显示的图片.NetWorkImageView会自动根据自身的宽高读取图片,降低OOM的概率。 当NetworkImageView在ListView中使用时,ImageLoader会处理View复用的问题,不会重复给一个复用的ImageView设置图片.可以查看ImageContainer,ImageRequest了解相关实现. ImageLoader需要一个ImageCache,用于处理图片缓存,配合开源项目DiskLruCache,我写了个ImageCache的实现,采用二级缓存,一级在内存中,一级在磁盘上.不过,磁盘读取文件还是在ui线程中,文件大了可能会有卡顿,以后再优化吧.

package com.android.volley.helper;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.util.LruCache;
import com.android.volley.toolbox.ImageLoader;
import com.jakewharton.disklrucache.DiskLruCache;
import utils.MD5Utils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * 二级Lru图片缓存,
 */
public class LruImageCache implements ImageLoader.ImageCache {
    LruCache lruCache;
    DiskLruCache diskLruCache;
    final int RAM_CACHE_SIZE = 5 * 1024 * 1024;
    String DISK_CACHE_DIR = "image";
    final long DISK_MAX_SIZE = 20 * 1024 * 1024;

    public LruImageCache() {
        this.lruCache = new LruCache(RAM_CACHE_SIZE) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getByteCount();
            }
        };

        File cacheDir = new File(Environment.getExternalStorageDirectory(), DISK_CACHE_DIR);
        if(!cacheDir.exists())
        {
            cacheDir.mkdir();
        }
        try {
            diskLruCache = DiskLruCache.open(cacheDir, 1, 1, DISK_MAX_SIZE);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Bitmap getBitmap(String url) {
        String key=generateKey(url);
        Bitmap bmp = lruCache.get(key);
        if (bmp == null) {
            bmp = getBitmapFromDiskLruCache(key);
            //从磁盘读出后,放入内存
            if(bmp!=null)
            {
                lruCache.put(key,bmp);
            }
        }
        return bmp;
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        String key=generateKey(url);
        lruCache.put(url, bitmap);
        putBitmapToDiskLruCache(key,bitmap);
    }

    private void putBitmapToDiskLruCache(String key, Bitmap bitmap) {
        try {
            DiskLruCache.Editor editor = diskLruCache.edit(key);
            if(editor!=null)
            {
                OutputStream outputStream = editor.newOutputStream(0);
                bitmap.compress(Bitmap.CompressFormat.PNG, 0, outputStream);
                editor.commit();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private Bitmap getBitmapFromDiskLruCache(String key) {
        try {
            DiskLruCache.Snapshot snapshot=diskLruCache.get(key);
            if(snapshot!=null)
            {
                InputStream inputStream = snapshot.getInputStream(0);
                if (inputStream != null) {
                    Bitmap bmp = BitmapFactory.decodeStream(inputStream);
                    inputStream.close();
                    return bmp;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 因为DiskLruCache对key有限制,只能是[a-z0-9_-]{1,64},所以用md5生成key
     * @param url
     * @return
     */
    private String generateKey(String url)
    {
        return MD5Utils.getMD532(url);
    }
}

MD5Utils是生成md5的类.

画UI只是创建自定义视图的一部分。你也需要让你的视图对用户的输入以一种closely resembles the real-world action you’re mimicking的方式作出反应(拟物?).对象应该跟真实的对象表现一样。举个例子, 图片不应该在已存在的物件上马上弹出,在某处重复出现,因为真实世界里的对象不会这样。图片应该从一个地方移动到另一个地方。 用户也感知微妙的行为或者一个界面,对模拟真实世界反应最好。举个例子,当用户fling(快速滑动,松开,listView里有)一个UI控件时,他们应该感觉到摩擦力让滚动慢下来。 本节示范如何使用Android框架的特性给你拉自定义视图加上这些真实世界的行为 处理输入手势 就像其他UI框架,Android支持输入事件模型。用户动作被转换成回调事件,你可以覆盖这些回调事件来定制你的应用对用户的响应。在Android系统中最常见的输入事件是Touch,它会触发onTouchEvent(android.view.MotionEvent)方法. 覆盖这个方法: Handle Input Gestures

   @Override
   public boolean onTouchEvent(MotionEvent event) {
    return super.onTouchEvent(event);
   }

它们自己的Touch事件不见得有用。现代的触控UI控件根据手势(像tapping轻击,pulling拉,pushing推,flinging抛,zooming缩放)来定义反应。为了把原始的Touch事件转换成手势,Android提供了GestureDetector类. 通过传入一个实现GestureDetector.OnGestureListener接口的类实例来构造一个GestureDetector.如果你只想处理几种手势,你可以继承GestureDetector.SimpleOnGestureListener替代实现GestureDetector.OnGestureListener接口。下面的代码创建了一个继承GestureDetector.SimpleOnGestureListener类并且覆盖onDown(MotionEvent)方法的类

class mListener extends GestureDetector.SimpleOnGestureListener {
   @Override
   public boolean onDown(MotionEvent e) {
       return true;
   }
}
mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());

不管你是否使用GestureDetector.SimpleOnGestureListener,你必须实现返回true的onDown()方法,这一步是必需的,因为所有的手势从onDown()开始。如果你返回false,系统就假设你要忽略处理手势,不会执行GestureDetector.OnGestureListener的其他方法。只有当你真要忽略整个手势时,才返回false. 一旦你实现了GestureDetector.OnGestureListener,并且创建了GestureDetector的实例,你就可以在onTouchEvent()方法里用GestureDetector处理接收到的touch事件.

@Override
public boolean onTouchEvent(MotionEvent event) {
   boolean result = mDetector.onTouchEvent(event);
   if (!result) {
       if (event.getAction() == MotionEvent.ACTION_UP) {
           stopScrolling();
           result = true;
       }
   }
   return result;
}

当你传给onTouchEvent()的touch事件不能被识别成手势的一部分时,它返回false, 创建逼真的物理运动 手势是控制触屏设备的强大工具,但他们也可以有悖常理,难以记忆,除非他们本身产生逼真的结果。一个好案例是fling手势,用户手指在屏幕上快速移动然后放开。这个手势是有意义的,如果UI向fling的方向快速滑动,然后慢下来,就像用户拉动飞轮,让它旋转。 然而, 模拟飞轮的效果不是繁琐的。为了让飞轮模型能正常工作,需要一堆数学和物理知识。幸运的是,Android提供了帮助类来模拟这个和其他行为。Scroller类为操控飞轮形式的fling手势打下了基础。 开始一个fling,通过fling的传入速率和x,y最小最大值来调用fling(),速率,你可以使用GestureDetector计算出来的值

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
   mScroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY);
   postInvalidate();
}

注意:尽管velocity是GestureDetector通过精确物理计算而来,但很多开发者觉得用这个值动画太快.通常把x,y速率值除以4~8. 调用fling()方法为fling手势设置了物理模型,然后,你需要通过定期调用 Scroller.computeScrollOffset()更新Scroller。 computeScrollOffset()通过读取当前时间并且使用物理模型计算x,y位置来更新Scroller对象的内部状态。调用getCurrX() 和 getCurrY() 来获取他们的值 大多数视图把Scroller对象的x,y值直接传给scrollTo()方法。PieChart案例有一些不同:它使用滚动y位置来设置图表的转动角。

if (!mScroller.isFinished()) {
    mScroller.computeScrollOffset();
    setPieRotation(mScroller.getCurrY());
}

Scroller类帮你计算滚动位置,但它不能自动将你的view应用这些位置。经常读取坐标,并应用新坐标,让滚动动画更流畅,是你的责任。有两种方法: 1、在调用fling()方法后,调用 postInvalidate(),强制重绘。这项技术要求你在onDraw()计算滚动偏移,并且在每次滚动偏移改变后,调用postInvalidate() 2、给fling期间设置一个ValueAnimator动画,通过调用 addUpdateListener()添加一个监听器来处理动画事件。 PieChart案例使用了第二种方法。这项技术设置稍稍复杂一点,但它紧依动画系统,并且不需要潜在的没必要的让视图失效(强制重绘)。缺点是ValueAnimator不支持API Level 11以前的版本,所以不能在android 3.0以前的版本运行。 注意:虽然ValueAnimator不支持api 11以前版本,但你仍然可以在面向低版本api的应用中使用。你只要在运行时检测当前的api版本,如果低于11,不要调用.

       mScroller = new Scroller(getContext(), null, true);
       mScrollAnimator = ValueAnimator.ofFloat(0,1);
       mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
           @Override
           public void onAnimationUpdate(ValueAnimator valueAnimator) {
               if (!mScroller.isFinished()) {
                   mScroller.computeScrollOffset();
                   setPieRotation(mScroller.getCurrY());
               } else {
                   mScrollAnimator.cancel();
                   onScrollFinished();
               }
           }
       });

让你的过渡更平滑 用户期望现代的UI在两个状态间平滑过渡。UI对象用淡入淡出替代出现和隐藏。动作平滑开始和结束替代突然的开始和结束。在Android3.0引入的Android属性动画框架,让平滑过渡变得容易。 使用动画系统,无论何时一个属性改变,会影响你的视图外观,不要直接改变属性。替代方案是使用ValueAnimator做改变。接下来的案例中,修改当前选中的扇形图会导致整个图表旋转。ValueAnimator在几百毫秒的周期改变旋转,而不是直接设置新的旋转值.

mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 0);
mAutoCenterAnimator.setIntValues(targetAngle);
mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION);
mAutoCenterAnimator.start();

如果你要改变的值是View基类里的属性,做动画更容易。因为视图有内建的ViewPropertyAnimator,为多个属性同时动画做优化:

animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();

以下快捷键对应Intellij IDEA mac版keymap设为Eclipse(mac)时。大部分跟eclipse一样,但有一些不同。部分快捷键做了自定义,会附上配置文件。 复制/剪切/粘贴 command+c/command+x/command+v 指定位置查找: ctrl+h 打开的文件内查找:默认没有,我改成了command+f 因为ctrl+f冲突,移动到下一个关键字:enter 移动光标到括号结尾:ctrl+],与下面的对应,我加了alt+command+]同时选中alt+command+shift+] 移动光标到括号开始:alt+command+[ 与上面的对应,我加了ctrl+],同时选中alt+command+shift+[ 打开源码相关菜单:ctrl+enter,可以生成构造方法,setter,getter,覆盖,委托,equals,hashCode,toString等方法。 代码完成,默认是ctrl+空格,与Spotlight冲突,我改成了ctrl+/ 代码对齐:command+shift+f 优化导入:command+shift+o 貌似只会删除无用的导入,不会添加需要导入的类 导入缺少的类:光标移到报错的代码上,按command+1,选择import class(第一项,可以直接回车),暂时没找到批量导入方法 复制光标所在行到下一行:command+alt+down 删除光标所在行:command+d 查看类层次结构:F4 查找所有调用:command+ctrl+g 展开/收缩方法:command++/- 展开所有:command+shift+= 收缩所有:command+shift+_ 显示当前类的所有方法和变量(类似eclipse里的大纲):command+o 运行:ctrl+alt+r,shift+command+F11 跳到源代码:F3 显示所有实现:command+t 跳到父类的实现(在子类的覆盖方法上):alt+shift+s 查找类:command+shift+t 生成if/if esle/try catch/synchronized等surround代码段:选中代码段,按alt+command+z 智能类型代码完成(在new后面自动加上类型):alt+shift+空格 注释:选中后 command+/ 每行双斜杠,ctrl+shift+/ /*注释/ 在错误处跳转:command+. command+shift+. 可以快速跳转到当前打开文件错误的地方 在方法间跳转:ctrl+alt+up/ctrl+alt+down 配置文件,保存到~/Library/Preferences/IdeaIC13/keymaps/Eclipse _Mac OS X_ copy.xml Eclipse _Mac OS X_ copy.xml:

<?xml version="1.0" encoding="UTF-8"?>
<keymap version="1" name="Eclipse (Mac OS X) copy" parent="Eclipse (Mac OS X)">
  <action id="CodeCompletion">
    <keyboard-shortcut first-keystroke="control SLASH" />
  </action>
  <action id="CollapseAllRegions">
    <keyboard-shortcut first-keystroke="shift meta DIVIDE" />
    <keyboard-shortcut first-keystroke="shift meta MINUS" />
  </action>
  <action id="CollapseRegion">
    <keyboard-shortcut first-keystroke="meta MINUS" />
  </action>
  <action id="EditorCloneCaretAbove">
    <keyboard-shortcut first-keystroke="control meta DOWN" />
  </action>
  <action id="EditorCodeBlockEnd">
    <keyboard-shortcut first-keystroke="control CLOSE_BRACKET" />
    <keyboard-shortcut first-keystroke="shift control P" />
    <keyboard-shortcut first-keystroke="meta alt CLOSE_BRACKET" />
  </action>
  <action id="ExpandAllRegions">
    <keyboard-shortcut first-keystroke="meta MULTIPLY" />
    <keyboard-shortcut first-keystroke="shift meta EQUALS" />
  </action>
  <action id="ExpandRegion">
    <keyboard-shortcut first-keystroke="meta ADD" />
  </action>
  <action id="Find">
    <keyboard-shortcut first-keystroke="meta F" />
  </action>
  <action id="GotoImplementation">
    <keyboard-shortcut first-keystroke="meta T" />
  </action>
  <action id="GotoSuperMethod">
    <keyboard-shortcut first-keystroke="shift alt S" />
  </action>
  <action id="MethodHierarchy.ImplementMethodAction" />
  <action id="SwitchApply">
    <keyboard-shortcut first-keystroke="control alt ENTER" />
  </action>
  <action id="SwitchDown">
    <keyboard-shortcut first-keystroke="control alt DOWN" />
  </action>
  <action id="SwitchLeft">
    <keyboard-shortcut first-keystroke="control alt LEFT" />
  </action>
  <action id="SwitchRight">
    <keyboard-shortcut first-keystroke="control alt RIGHT" />
  </action>
  <action id="SwitchUp">
    <keyboard-shortcut first-keystroke="control alt UP" />
  </action>
</keymap>

1、避免创建不必要的对象。最好能重用对象,而不是每次在需要的时候就创建一个功能相同的新对象。如果对象是不可变的,或者已知不会变,都可以重用。 2、消除过期引用。虽然Java有垃圾回收,但垃圾回收基于引用计数,只有对象没有被其他对象引用时,才能被回收。特别注意在数组以及集合中,他们包含的对象都被数组以及集合类引用,需要手工从数组或集合类对象中删除或设为null,否则只能等到引用他们的数组或集合对象被回收。这是引发内存泄露的原因。还有事件监听器,setListener的时候增加了listener的引用,如果没有显示地清除listener,也可能会导致无法回收listener,建议只保存listener的weak reference。总结一下,一个对象,如果被生命周期长于自己的对象引用,就可能发生内存泄露,原因在于只要引用者不释放,被引用者就无法回收。 3、不要使用finalizer方法。释放回收资源应该在不需要的时候主动执行,别想靠这个方法在gc时回收。finalizer不知道什么时候,也不一定会执行。Q:为什么设计这个方法?

翻自:http://developer.android.com/training/custom-views/custom-drawing.html 对一个自定义视图来说,最重要的是它的外观,根据你的应用需求,自定义绘画可以很简单也可以很复杂。本课覆盖了最普通的一些操作。 覆盖onDraw()方法 绘制一个自定义视图,最重要的一步是覆盖onDraw()方法。onDraw()方法的参数是一个View可以用于绘制自己的Canvas对象,Canvas类定义了绘制文本,线条,位图和其他很多图元。你可以在onDraw()中用这些方法创建自己的UI。 在你调用任何绘画方法前,创建一个Paint对象是很必要的。下一节详细讨论Paint. 创建绘画对象 android.graphics 框架把绘画分成两块。 画什么,由Canvas决定 怎么画,由Paint控制 举例来说,Canvas提供了一个画线的方法,Paint提供了定义线的颜色的方法。Canvas有一个画矩形的方法,Paint定义了是填充那个矩形还是让它留空。简单地说,Canvas定义了你能在屏幕上画出的形状,而Paint定义了颜色,样式,字体和你画的每个形状等等。 所以,在你画任何东西前,你需要创建一个或多个Paint对象,PieChart在init方法里做了这个,在构造方法中被调用.

private void init() {
   mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   mTextPaint.setColor(mTextColor);
   if (mTextHeight == 0) {
       mTextHeight = mTextPaint.getTextSize();
   } else {
       mTextPaint.setTextSize(mTextHeight);
   }

   mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   mPiePaint.setStyle(Paint.Style.FILL);
   mPiePaint.setTextSize(mTextHeight);

   mShadowPaint = new Paint(0);
   mShadowPaint.setColor(0xff101010);
   mShadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));

   ...

先创建对象是一个很重要的优化。View重绘非常频繁。很多绘图对象需要昂贵的初始化。在onDraw()里创建绘图对象意味着性能降低,会让你的UI卡顿。 处理布局事件 为了合适地绘制你的自定义View,你需要知道它的尺寸。复杂的自定义View经常要执行多个布局计算,通过他们在屏幕上上位置,形状,大小。你不应该假定你的View在屏幕上的位置。即使只有一个app使用你的View,这个app也需要处理不同的屏幕尺寸,多样的屏幕密度,各种屏幕宽高比(包括横竖屏模式) View有很多方法用来测量,他们中的大多数不需要覆盖。如果你不需要在它的尺寸上做特别的控制,你只需要覆盖一个方法:onSizeChanged() onSizeChanged() 在你的View第一次被分配一个大小时调用,因为某个原因改变了View的大小时再次被调用。在onSizeChanged()中计算位置,大小,还有其他跟View尺寸相关的值,取代在每次onDraw中重新计算。 在PieChart案例中,onSizeChanged()是计算饼图矩形边界和text Lable以及其他可见对象相对位置的地方。 当你的View被分配一个尺寸时,布局管理器假定这个尺寸包括了所有内边距。当你计算你的View尺寸时,必须处理padding值。下面是pieChart.OnSizeChanged()里的一段:

       // Account for padding
       float xpad = (float)(getPaddingLeft() + getPaddingRight());
       float ypad = (float)(getPaddingTop() + getPaddingBottom());

       // Account for the label
       if (mShowText) xpad += mTextWidth;

       float ww = (float)w - xpad;
       float hh = (float)h - ypad;

       // Figure out how big we can make the pie.
       float diameter = Math.min(ww, hh);

如果你要更好地控制你的View布局参数,实现onMeasure()方法。这个方法的参数是View.MeasureSpec,它告诉你,你的View的父View希望你的View有多大,这个尺寸是一个硬性的最大值还是仅仅是一个建议。作为优化,这些值被保存成封装过的整数,你可以使用View.MeasureSpec的静态方法读取保存在每个整数中的信息。这是一个实现onMeasure()方法的例子,在这个实现中,PieChart尝试让他的区域足够大,以使饼和它的标签一样大。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on our minimum
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

   // Whatever the width ends up being, ask for a height that would let the pie
   // get as big as it can
   int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0);

   setMeasuredDimension(w, h);
}

在这段代码中,有三个重要的事情: 计算时加上了view的padding,跟之前提到的一样,这是view的责任。 辅助方法resolveSizeAndState(),用于最终的宽和高,它通过比较View所需的尺寸规格,返回了合适的View.MeasureSpec值给onMeasure(); onMeasure()没有返回值,作为替代,这个方法通过调用setMeasuredDimension()通知他的结果。调用这个方法是强制的,如果你忽略这个调用,View类会抛出runtion异常。 绘画! 一旦你的类被创建,测量代码被定义,你就可以实现onDraw()方法,每个View实现onDraw()代码不同,但有一些大多数View可以共用的操作。 画线使用drawText(),通过调用setTypeface()指定字体,通过setColor()指定颜色 画原始形状,使用drawRect()矩形, drawOval()椭圆, drawArc()圆弧,不论是改变形状是否填充,轮廓,或者两者,都调用setStyle()。 画更复杂的图形使用Path类。通过添加直线和曲线到一个Path对象来定义一个形状,然后用drawPath()画出这个形状。就像原始形状一样,Path也可以有轮廓,填充,或者两都都有,取决定于 setStyle() 通过创建LinearGradient对象定义一个梯度填充(渐变填充),在需要填充的shape上调用 setShader()来填充。 绘制位置使用drawBitmap(). 下面是PieChart中的代码,混合了文本,直线和形状

protected void onDraw(Canvas canvas) {
   super.onDraw(canvas);

   // Draw the shadow
   canvas.drawOval(
           mShadowBounds,
           mShadowPaint
   );

   // Draw the label text
   canvas.drawText(mData.get(mCurrentItem).mLabel, mTextX, mTextY, mTextPaint);

   // Draw the pie slices
   for (int i = 0; i < mData.size(); ++i) {
       Item it = mData.get(i);
       mPiePaint.setShader(it.mShader);
       canvas.drawArc(mBounds,
               360 - it.mEndAngle,
               it.mEndAngle - it.mStartAngle,
               true, mPiePaint);
   }

   // Draw the pointer
   canvas.drawLine(mTextX, mPointerY, mPointerX, mPointerY, mTextPaint);
   canvas.drawCircle(mPointerX, mPointerY, mPointerSize, mTextPaint);
}

翻自:http://developer.android.com/training/custom-views/create-view.html 设计良好的定制视图与其他精心设计的类相似。它用一种易于使用的接口封装了一些特殊的方法集合,它高效使用内存和CPU等等。除了良好的设计以外,一个定制视图应该: 符合Android标准 提供定制的可在xml布局中使用的风格属性 发送可访问性事件(针对盲人等) 兼容多个Android平台 继承View类 Android框架里所有的视图类继续自View,你的自定义视图也可以继续View,或者你可以继承一个已经存在的View子类节约时间,像Button 为了能让ADT布局编辑器能使用你定制的View,你的View必须提供一个Context和 AttributeSet对象作为参数的构造方法(要在xml中用,就必须有这个构造方法)

class PieChart extends View {
    public PieChart(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

定义自定义属性 给你的UI添加一个内建的View,你可以在xml中通过指定它的属性,控制它的外观行为。写得好的自定义View也可以通过xml添加和设定风格,为了在你的自定义View中启用这个功能,你必须: 使用元素为你的自定义View定制属性 在xml布局中指定属性值 运行时获取属性值 在你的View中应用获取到的属性值 本节讨论如何定义定制属性并且指定他们的值,下节处理在运行时获取并应用这些值 通过添加来定义定制属性,通常把这些资源放在res/values/attrs.xml文件中,下面是一个例子:

<resources>
   <declare-styleable name="PieChart">
       <attr name="showText" format="boolean" />
       <attr name="labelPosition" format="enum">
           <enum name="left" value="0"/>
           <enum name="right" value="1"/>
       </attr>
   </declare-styleable>
</resources>

这些代码声明了两个自定义属性,showText和labelPosition,属于一个叫PieChart的styleable实体。实体的名称,按照惯例,与自定义View类名相同,尽量没有严格要求必须遵循这个惯例,大多数代码编辑器依赖这个命名惯例提供代码提示。 一旦你定义了自定义属性,你可以就像是内建的属性一样在xml布局中使用。唯一的区别是,你的自定义属性属于一个不同的命名空间,他们属于http://schemas.android.com/apk/res/\[包名\]命名空间,而不是系统内置的http://schemas.android.com/apk/res/android。下面是使用属性的方法:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews">
 <com.example.customviews.charting.PieChart
     custom:showText="true"
     custom:labelPosition="left" />
</LinearLayout>

为了避免重复长命名空间URI,例子使用了xmlns指令,该指令声明http://schemas.android.com/apk/res/com.example.customviews命名空间的一个别名,你可以任何你喜欢的别名。 注意布局中的xml标签名,这是自定义View的全名(包括包名),如果你的View类是内部类,你必须使用它的外部类,举例来说,PieChart有一个叫PieView的内部类,如果要用PieView,你必须标签com.example.customviews.charting.PieChart$PieView。 应用自定义属性 当一个View从xml布局中创建时,xml中的所有属性都会从资源bundle里读取,并作为AttributeSet对象传给View的构造方法,尽管可以直接从AttributeSet里读取这些值,但这么做有一些不好的地方: 没有属性的资源引用不能被解析 样式没有应用到View上 作为代替,把AttributeSet传给obtainStyledAttributes(),这个方法返回一个已经解析好并应用样式的TypedArray数组。 为了让你更容易地调用obtainStyledAttributes(),Android资源编译器做了一大堆工作.res目录里的每一个 资源,自动生成的R.java文件都定义了一个属性id数组和属性在该数组中的索引集合。你要使用预编译的静态变量从TypedArray里读取属性。下面是PieChart类如何读取属性:

public PieChart(Context context, AttributeSet attrs) {
   super(context, attrs);
   TypedArray a = context.getTheme().obtainStyledAttributes(
        attrs,
        R.styleable.PieChart,
        0, 0);

   try {
       mShowText = a.getBoolean(R.styleable.PieChart_showText, false);
       mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
   } finally {
       a.recycle();
   }
}

注意TypedArray对象是共享资源,你用完必须回收。 添加属性和事件 属性是控制View的行为和外观的强大工具,当在View初始化时,他们只能被读取。为了提供动态的行为,可以暴露一个属性的getter和setter方法。下面的代码段展示了PieChart如何暴露showText属性

public boolean isShowText() {
   return mShowText;
}

public void setShowText(boolean showText) {
   mShowText = showText;
   invalidate();
   requestLayout();
}

注意setShowText调用了invalidate()和requestLayout(),这些调用是至关重要的,以确保View表现可靠。你必须在改变可能引起View外观变化的属性后,使View无效,系统才能知道它需要被重绘。同样地,如果属性改变会影响尺寸或形状,你需要请求一个新的布局。少了这两行会产生难以预以发现的BUG。 自定义View也支持事件监听器与外界通讯。举例来说,PieChart暴露了一个叫OnCurrentItemChanged的自定义事件,来告诉监听者用户翻转了饼图,关注了一个新饼图。 忘记暴露属性和事件很容易,特别是自定义View只有自己用的时候,多花点时间小心定义你的View接口,减少未来维护成本。一个好的原则是,暴露你的View中所有影响外观和形为的属性。 无障碍设计 你的自定义View应该支持最广泛的用户,包括看不见,不能使用触摸屏的残障人士。为了这部分用户,你应该: 使用android:contentDescription给输入字段做标记 在适当的时候通过发送sendAccessibilityEvent()发送可访问性事件 支持备用控制器,如手柄,轨迹球。

翻自:http://developer.android.com/training/custom-views/index.html 创建自定义视图 Android已经包含了大量的用于展示数据和用户交互的View类,但有时你的应用有内建View类不能满足的特别需要,本课程教你如何创建自己的强健的,可复用的View. 课程: 创建一个View类 创建一个像内建View一样的类,有自定义属性并且支持ADT布局编辑器 自定义绘画(覆盖OnDraw方法) 使用Android图形系统让你的应用与众不同 让View可以交互 用户期望一个View可以流畅响应,并且很自然地使用手势。本节讨论如何使用手势检测,物理和动画,给你的用户一种专业的感觉。 优化View 如果卡,不管你的UI有多漂亮,用户也不会喜欢。学会如何避免一般的性能问题,如何使用硬件加速让你的定制绘画运行更快。

翻自http://developer.android.com/training/multiscreen/adaptui.html UI flow大致是交互方式的意思,本节讲的是根据不同的layout,在代码中判断,并实现不同的交互方式。 应用当前显示的界面依靠布局,但UI flow可以是不同的。举例来说,如果你的应用是双栏的,在左栏的item上点击将会在右栏上显示内容;如果这是单栏的,内容会在自身上显示(或者是开了个不同的Activity) 判断当前布局 由于你每个布局的实现有些不同,要做的第一件事可能就是先判断当前用户浏看到的是哪个布局。举例来说,你可能需要知道用户是在单栏还是双栏模式。你可以通过判断一个view是否存在,是否可见来实现。

public class NewsReaderActivity extends FragmentActivity {
    boolean mIsDualPane;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);

        View articleView = findViewById(R.id.article);
        mIsDualPane = articleView != null && 
                        articleView.getVisibility() == View.VISIBLE;
    }
}

这里是判断acticle栏是否可用,为比用硬编码查询一个指定的layout再灵活(说的应该是为了判断专门加一个没用的layout吧) 另一个例子在对某些组件执行操作时判断该组件是否可用。比如,在新闻阅读器里,有一个按钮用来打开菜单,但这个按钮只有在Android 3.0以上的版本里才有(因为这个功能用到了 API 11里的ActionBar),所以你可以这样为按钮添加事件监听器:

Button catButton = (Button) findViewById(R.id.categorybutton);
OnClickListener listener = /* create your listener here */;
if (catButton != null) {
    catButton.setOnClickListener(listener);
}

根据当前的布局做不同的响应 根据当前显示的布局,有些动作可能会有不同的结果。在新闻阅读器的例子中,双栏模式在左边点击标题时,右边显示内容,而单栏模式,会打开一个新的Activity用于显示内容.

@Override
public void onHeadlineSelected(int index) {
    mArtIndex = index;
    if (mIsDualPane) {
        /* display article on the right pane */
        mArticleFragment.displayArticle(mCurrentCat.getArticle(index));
    } else {
        /* start a separate activity */
        Intent intent = new Intent(this, ArticleActivity.class);
        intent.putExtra("catIndex", mCatIndex);
        intent.putExtra("artIndex", index);
        startActivity(intent);
    }
}

同样的,在双栏模式下,action bar 会被设置成tab导航,而在单栏模式中,应该设置成一个spinner下拉菜单,所以你的代码里应该检查哪种适合:

final String CATEGORIES[] = { "Top Stories", "Politics", "Economy", "Technology" };

public void onCreate(Bundle savedInstanceState) {
    ....
    if (mIsDualPane) {
        /* use tabs for navigation */
        actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_TABS);
        int i;
        for (i = 0; i < CATEGORIES.length; i++) {
            actionBar.addTab(actionBar.newTab().setText(
                CATEGORIES[i]).setTabListener(handler));
        }
        actionBar.setSelectedNavigationItem(selTab);
    }
    else {
        /* use list navigation (spinner) */
        actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_LIST);
        SpinnerAdapter adap = new ArrayAdapter(this, 
                R.layout.headline_item, CATEGORIES);
        actionBar.setListNavigationCallbacks(adap, handler);
    }
}

在其他Fragment中复用Fragment 在为多屏设计时一个重复模式是,你的一部分UI在某些屏幕配置上实现起来是个面板,但在其他的配置时是一个独立的Activity。举例来说,在新闻阅读器里,大屏的设备中,内容会显示在右侧面板中,而在小屏设备中,内容会显示在独立的activity里。 在这种情况下,你通常可以在多个Activity中复用一个Fragment的子类来避免代码复本(多个Activity中包含一样的逻辑代码)。比如 ArticleFragment在双栏布局中使用:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="400dp"
              android:layout_marginRight="10dp"/>
    <fragment android:id="@+id/article"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.ArticleFragment"
              android:layout_width="fill_parent" />
</LinearLayout>

在小屏设备中,ArticleActivity里复用

ArticleFragment frag = new ArticleFragment();
getSupportFragmentManager().beginTransaction().add(android.R.id.content, frag).commit();

在xml布局里声明fragment也有相同的效果,但在本案例中,ArticleActivity只有这么一个组件,所以xml是没有必要的. 有一点很重要需要铭记的是,当你设计Fragment时,不要跟某个特殊的Activity强耦合(不要调用某个Activity特有的方法,属性等),你可以定义一个接口,抽象所有该Fragment会用到的方法,然后让所有使用该Fragment的Activity实现它(这里不仅指狭义上的Activity自身实现,也可以由Activity来set其他实现者)。 新闻阅读器里的HeadlinesFragment就是这么干的

public class HeadlinesFragment extends ListFragment {
    ...
    OnHeadlineSelectedListener mHeadlineSelectedListener = null;

    /* Activity必须实现该接口*/
    public interface OnHeadlineSelectedListener {
        public void onHeadlineSelected(int index);
    }
    ...

    public void setOnHeadlineSelectedListener(OnHeadlineSelectedListener listener) {
        mHeadlineSelectedListener = listener;
    }
}

然后,当用户选择一个标题时,Fragment会通知Activity指定的监听器(相反的是通知一个写死的Activity)

public class HeadlinesFragment extends ListFragment {
    ...
    @Override
    public void onItemClick(AdapterView parent, 
                            View view, int position, long id) {
        if (null != mHeadlineSelectedListener) {
            mHeadlineSelectedListener.onHeadlineSelected(position);
        }
    }
    ...
}

操作屏幕配置改变 如果你使用独立的Activity来实现界面的某一部分,你要在一些配置变化时(比如屏幕翻转)做出相应反应以保持界面一致性。 在典型的运行Android 3.0及以上版本的7寸平板中,竖屏模式下,新闻阅读器将使用一个独立的Activity显示新闻内容,但在横屏模式下使用双栏。 这意味着,当用户在竖屏模式下浏览文章内容时,是在一个独立的Activity中,你需要检测屏幕方向改变成横屏时作出合适的反应,关闭Activity,回到 main activity,显示两栏布局.

public class ArticleActivity extends FragmentActivity {
    int mCatIndex, mArtIndex;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mCatIndex = getIntent().getExtras().getInt("catIndex", 0);
        mArtIndex = getIntent().getExtras().getInt("artIndex", 0);

        // If should be in two-pane mode, finish to return to main activity
        if (getResources().getBoolean(R.bool.has_two_panes)) {
            finish();
            return;
        }
        ...
}

翻自:http://developer.android.com/training/multiscreen/screendensities.html 本课程向你展示如何通过提供不同的资源和使用与分辨率无关的测量单位(除px以外的)来支持不同的屏幕密度 使用Density-independent Pixels(找不到合适的词翻,总之就是与屏幕分辨率无关的一个单位,就是dip或dp,两者是一样的) 当你设计布局时,应该避免使用绝对像素来定义距离或者尺寸。这是因为不同的屏幕有不同的像素密度,所以一样的像素值在两个不同的设备上可能会有不同的物理大小.因此,在定义大小时,总是使用dp或sp单位. 1个dp相当于160dpi下的1个物理像素,sp也一样,但它可以随着用户在系统中的设置缩放(设置-显示-字体大小),所以你在定义字体大小时,要使用sp,在不要用在定义布局上。不想字段随着设置变,都用dp. 使用dp而不是px指定两个控件间的间隔:

<Button android:layout_width="wrap_content" 
    android:layout_height="wrap_content" 
    android:text="@string/clickme"
    android:layout_marginTop="20dp" />

定义字体大小时,使用sp

<TextView android:layout_width="match_parent" 
    android:layout_height="wrap_content" 
    android:textSize="20sp" />

提供几套不同尺寸的图片 因为Adroid设备的多样性,你应该为大多数常见的屏幕密度(low, medium, high,extra-high)提供合适的图片资源,对应ldpi,mdpi,hdpi,xhdpi.这会让你的应用在所有的屏幕密度上有比较好的显示效果和性能。 先根据下表的比例创建不同分辨率的图片 xhdpi: 2.0 hdpi: 1.5 mdpi: 1.0 (基准) ldpi: 0.75 这意味着,如果你要在xhdpi的设备上使用200x200的图片,你要给hdpi创建150x150,mdpi创建100x100,ldpi创建75*75的同样图片。 然后,把这些图片放到各自对应目录下,系统会自动根据屏幕密度选择。 MyProject/ res/ drawable-xhdpi/ awesomeimage.png drawable-hdpi/ awesomeimage.png drawable-mdpi/ awesomeimage.png drawable-ldpi/ awesomeimage.png 当你使用 @drawable/awesomeimage引用图片时,系统会选择合适的图片

翻自Android官方文档:http://developer.android.com/training/multiscreen/screensizes.html 本节向你展示用以下方法支持不同的屏幕尺寸: 1、确保你的布局可以调整大小以适应不同屏幕 2、根据屏幕配置提供适当的UI布局 3、确保正确的布局应用到正确的屏幕 4、提供正确缩放的图片(不同的屏幕,图片大小不同) 使用wrap_content和match_parent 为了确保你的布局灵活并且适配不同的屏幕,你应该在宽度和高度上使用wrap_content和match_parent,使用wrap_content时,view会给宽高设置一个适应内容的最小值,当使用match_parent(api level 8前是fill_parent)组件会展开匹配父view的大小。 通过使用wrap_content和match_parent来替代硬编码的size(size写死),你的view只会利用他需要的空间或者展开铺满尽可能多的空间,如下:

<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="match_parent" 
                  android:id="@+id/linearLayout1"  
                  android:gravity="center"
                  android:layout_height="50dp">
        <ImageView android:id="@+id/imageView1" 
                   android:layout_height="wrap_content"
                   android:layout_width="wrap_content"
                   android:src="@drawable/logo"
                   android:paddingRight="30dp"
                   android:layout_gravity="left"
                   android:layout_weight="0" />
        <View android:layout_height="wrap_content" 
              android:id="@+id/view1"
              android:layout_width="wrap_content"
              android:layout_weight="1" />
        <Button android:id="@+id/categorybutton"
                android:background="@drawable/button_bg"
                android:layout_height="match_parent"
                android:layout_weight="0"
                android:layout_width="120dp"
                style="@style/CategoryButtonStyle"/>
    </LinearLayout>

    <fragment android:id="@+id/headlines" 
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

注意例子是如何在组件大小上使用wrap_content和match_parent而非指定大小。这允许布局对不同的屏幕大小和方向适配。 使用RelativeLayout 你可以用嵌套LinearLayout,wrap_content,match_parent构建相当复杂的布局。然而LinearLayout不能精确控制子视图间的相对关系。LinearLayout的子视图只是简单的一个挨着一个。如果你需要子视图在其他视图变化时调整,而不是一条直线,更好的解决方案是使用RelativeLayout,它允许你指定两个控件间的相对关系。下面的实例是一个视图屏幕左对齐,一个右对齐

<?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/label"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Type here:"/>
    <EditText
        android:id="@+id/entry"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/label"/>
    <Button
        android:id="@+id/ok"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/entry"
        android:layout_alignParentRight="true"
        android:layout_marginLeft="10dp"
        android:text="OK" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toLeftOf="@id/ok"
        android:layout_alignTop="@id/ok"
        android:text="Cancel" />
</RelativeLayout>

QVGA屏幕上的效果 大屏幕上的效果: 尽管大小变了,但是空间关系保存在RelativeLayout.LayoutParams中 使用尺寸限定符 可扩展布局或者相对布局能做的只能前面说的那么多。这些布局通过在空间内部拉伸或者依靠其他控件再适配不同的屏幕,他们可能没有为不同的屏幕提供最好的用户体验。因此,你的应用不应该只提供弹性布局,也要提供几个针对不同的屏幕可选的布局。你可以通过配置界定符,它允许你在运行时自动根据当前的设备配置选择合适的资源(比如不同的屏幕尺寸设计不同的布局) 举个例子,很多应用在大屏幕上实现两栏布局模式(一栏是列表,一栏是内容)。平板和电视的屏幕足够大可以同时显示两栏,但手机的屏幕必须分开显示,所以,实现这些布局,你可以通过下面的文件。 res/layout/main.xml,单栏 (默认) 布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

res/layout-large/main.xml, 两栏布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="400dp"
              android:layout_marginRight="10dp"/>
    <fragment android:id="@+id/article"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.ArticleFragment"
              android:layout_width="fill_parent" />
</LinearLayout>

注意目录名中的有large界定符的第二个布局,这个布局会被屏幕分类为larger的设备自动选择,比如屏幕大于7寸的平板。小屏设备会选择另一个视图(没有large界定符的) 使用最小宽度(Smallest-width)界定符 开发者们需要面对的难题之一是3.2以前的Android设备”large”屏幕尺寸,包括Dell Streak,Galaxy Tab和大部分7寸平板。然而,很多应用想要为不同的设备(都是Large)屏幕上显示出不出的效果(像5寸,7寸),虽然他们都是large.简单地说,large分得不够细. 最小宽度界定符允许你设置一个最小宽度(dp为单位)来适配屏幕。例如,典型的7寸平板是600dp,如果你想要你的UI在这种屏幕上有两栏,但是在更小的屏幕上显示一栏,你可以使用前一节中用到的两个layout(一栏,两栏),但两栏的目录不是用large界定符,而是sw600dp。 res/layout/main.xml, 默认一栏布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

res/layout-sw600dp/main.xml,两栏布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="400dp"
              android:layout_marginRight="10dp"/>
    <fragment android:id="@+id/article"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.ArticleFragment"
              android:layout_width="fill_parent" />
</LinearLayout>

所有屏幕宽度大于600dp的设备会选择res/layout-sw600dp/main.xml,而小于600dp的设备会选择res/layout/main.xml Android 3.2以前的版本支持上面的写法,你还是得用large,所以你的应用里,得有layout-large,还要有一个内容与layout-large相同的layout-sw600dp(抛弃这部分用户吧)。 为了减少维护成本,避免复制多个一样的文件,你可以使用别名定义。例子: res/layout/main.xml, 一栏布局 res/layout/main_twopanes.xml, 两栏布局 res/values-large/layout.xml,res/values-sw600dp/layout.xml里引用:

<resources>
    <item name="main" type="layout">@layout/main_twopanes</item>
</resources>

使用方向界定符 一些布局在横竖屏方向都能工作得很好,但大多数还是可以通过调整受益。下面是在新闻阅读器的例子中,不同屏幕尺寸和不同方向的不同表现: 小屏竖屏 单栏,带logo 小屏横屏 单栏,带logo 7寸平板 竖屏 单栏,带action bar 7寸平板 横屏 双栏,宽,带action bar 10寸平板 竖屏 双栏,窄,带action bar 10寸平板 横屏 双栏 宽 带action bar 电视 横屏 双栏 宽 带action bar 每个布局都定义在res/layout目录中,然后针对不同的屏幕配置使用别名。 res/layout/onepane.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

res/layout/onepane_with_bar.xml:

<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="match_parent" 
                  android:id="@+id/linearLayout1"  
                  android:gravity="center"
                  android:layout_height="50dp">
        <ImageView android:id="@+id/imageView1" 
                   android:layout_height="wrap_content"
                   android:layout_width="wrap_content"
                   android:src="@drawable/logo"
                   android:paddingRight="30dp"
                   android:layout_gravity="left"
                   android:layout_weight="0" />
        <View android:layout_height="wrap_content" 
              android:id="@+id/view1"
              android:layout_width="wrap_content"
              android:layout_weight="1" />
        <Button android:id="@+id/categorybutton"
                android:background="@drawable/button_bg"
                android:layout_height="match_parent"
                android:layout_weight="0"
                android:layout_width="120dp"
                style="@style/CategoryButtonStyle"/>
    </LinearLayout>

    <fragment android:id="@+id/headlines" 
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="match_parent" />
</LinearLayout>

res/layout/twopanes.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="400dp"
              android:layout_marginRight="10dp"/>
    <fragment android:id="@+id/article"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.ArticleFragment"
              android:layout_width="fill_parent" />
</LinearLayout>

res/layout/twopanes_narrow.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    <fragment android:id="@+id/headlines"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.HeadlinesFragment"
              android:layout_width="200dp"
              android:layout_marginRight="10dp"/>
    <fragment android:id="@+id/article"
              android:layout_height="fill_parent"
              android:name="com.example.android.newsreader.ArticleFragment"
              android:layout_width="fill_parent" />
</LinearLayout>

现在所有的布局都定义好了,接下来要做的就是针对不同的屏幕配置映射正确的布局,你可以使用布局别名 res/values/layouts.xml:

<resources>
    <item name="main_layout" type="layout">@layout/onepane_with_bar</item>
    <bool name="has_two_panes">false</bool>
</resources>

res/values-sw600dp-land/layouts.xml:

<resources>
    <item name="main_layout" type="layout">@layout/twopanes</item>
    <bool name="has_two_panes">true</bool>
</resources>

res/values-sw600dp-port/layouts.xml:

<resources>
    <item name="main_layout" type="layout">@layout/onepane</item>
    <bool name="has_two_panes">false</bool>
</resources>

res/values-large-land/layouts.xml:

<resources>
    <item name="main_layout" type="layout">@layout/twopanes</item>
    <bool name="has_two_panes">true</bool>
</resources>

res/values-large-port/layouts.xml:

<resources>
    <item name="main_layout" type="layout">@layout/twopanes_narrow</item>
    <bool name="has_two_panes">true</bool>
</resources>

使用nine-patches图片(后缀为.9.png) 支持不同的屏幕尺寸通常意味着你的图片资源也能够适配不同的尺寸,举例来说,一个按钮的背景必须适应按钮可能的宽高变化 如果你在尺寸可以改变的组件上使用简单的图片,你很快会意识到不是那么好看,因为图片在运行时会被拉伸变形。解决方案是使用nine-patches图片.这是一种能够判断哪些区域可以或者不可以拉伸的png格式图片。 因此,当要为可变尺寸的控件设计图片时,总是使用nine-patches图片,你可以把普通的图片转换成nine-patches。 制作Nine-patch图片通常使用sdk/tools/目录下的draw9patch工具,可以通过在左边和顶部边框上画像素来标记哪个区域可以被拉伸,你也可以在右边和底部边框画像素标记哪块区域可以显示内容,结果如下图: 注意边框上的黑色像素,左边和顶部的用于指明图片的哪个部位可以被拉伸,右边和底部的用于指明内容应该放哪。同时,注意.9.png的扩展名,Android会根据这个识别nine-patch图片,可别把9去掉了. 当你把这个图片应用到组件上时(通过设置android:background=”@drawable/button”),Android框架会自动拉伸图片适应按钮的尺寸,看下图: