分类目录归档:Android

Interpolator – Android动画插值器

好的动画可以很大程度的提高App交互体验,Android API提供的几种基本类型动画,加上插值器可以做出来大部分比较满意的效果。

一、插值器是什么

简单来讲,插值器就是一个函数,将时间t经过一个函数的变换映射到t’。

二、插值器为什么

自定义Animation我再赘述一下,比较简单的自定义Animation基本上重写下面的方法就够了:

protected void applyTransformation(float interpolatedTime, Transformation t) {

}

interpolatedTime就是动画过程中的t(0<=t<=1),根据当前的时间进行相应的canvas变换,以得到动画的效果。

再看方法的调用:

final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
applyTransformation(interpolatedTime, outTransformation);

所以,传进去的t是经过插值器mInterpolator映射后的t’,影响动画的运动轨迹和速度变化,即: t->t’->s 。

如果我们不给动画设置插值器得到的是什么?答案是匀速,即映射关系 t → t’: t’ = t 。

三、插值器怎么做

实现android.view.animation.Interpolator接口,实现方法:float getInterpolation(float input); 如:

public class LinerInterpolater implements Interpolator {

    @Override
    public float getInterpolation(float input) {
        return input;
    }

}

这便是一个默认的匀速插值,映射其实就是y=x,即t’=t。

四、21分钟精通插值器

t’=f(t) ,f(t)求导便是动画的速度,所以接下来我们可以根据需求进行插值器的定制。

如:

  • 加速(匀变速),这是一个二次插值,f(t) = t * t

image

  • 对应还可以得到减速(匀变速),也是一个二次插值, f(t) = 1 – ( 1 – t ) * ( 1 – t )

image

在一般情况下,映射后的t’也是在0~1范围内;如果我们需要过冲的效果,这个时候就会出现t’大于1的情况,最终回到1位置。(终点不是一定要在1)。

1. 0~1范围映射

只需经过简单的函数变换,就可以得到任意我们想要的变速效果。以上面列举到的二次插值y = x / x*为例:

这种情况下t在0~1上映射到t’也是0~1,可以直接使用。

如果我们想要减速的效果,只需经过一些简单变换。

  1. 先将函数图像上下翻转,y = – x * x
  2. 再将函数图像在x轴上右移一个单位,y = – ( 1 – x ) * ( 1 – x )
  3. 再将行数图像在y轴上上移一个单位,y = 1 – ( 1 – x ) * ( 1 – x )*

即可得到匀减速插值器,t->t’在0~1上映射过去也是0~1。

所以我们可以使用任何一个函数制作需要的插值器,只需要找到一个满意的变速区域(x,x+1),经过一些列函数变换,最终满足x、y均在0~1范围内(再来一遍:y不是一定要在0~1范围)。

2. 过冲的情况

对于t’不是全程都在0~1范围内的情况,比如我们需要平移动画有一个过冲(当然可以通过分段函数实现),抛体运动其实是一个不错的选择,其实就是上面说到的二次函数插值。

我们甚至可以在y=1-(1-x)*(1-x)的基础上继续变换。

1.将函数图像在y轴上放大为原图的m(m>1)倍,可以求出来y=1的时候方程的两个根x1和x2,x2 > x1。

image

y = ( 1 – ( 1 – x ) * ( 1 – x ) ) * m

2.将函数图像在x轴上压缩为原图的1/x2,得到:

image

y = ( 1 – ( 1 – x * x2 ) * ( 1 – x * x2 ) ) * m

如此,便得到了过冲为m-1、峰值为m的插值,且合物理运动规律,比较自然。

我们制作插值的时候,可以尽量模拟物理运动,这样动画看起来会比较自然、舒服。

我们制作插值的时候,可以尽量模拟物理运动,这样动画看起来会比较自然、舒服。

我们制作插值的时候,可以尽量模拟物理运动,这样动画看起来会比较自然、舒服。

重要的事情说三遍。

五、我要干货

以上只是带大家复习了一下中学数学,下面给大家提供一些实用的插值器:

1. x^n 插值

加速:y = x^n

减速:y = 1-(1-x)^n

先加速后减速:课后作业,自己练习

求导可知,次数越高,速度的变化速度越快。

2. a^x 指数插值

加速:a^(1-t) 其中a大于0小于1,下同

减速:1-a^t

先加速后减速:课后作业,自己练习。有兴趣的还可以自己变换得到上面的两个函数。

这个插值器的速度变化非常快。减速效果可以得到一种类似于物体在粘性液体中受阻力的运动效果,在终点位置可以非常缓慢逼近,以前做产品的时候,用户的描述是:黄油一般的动画效果……当然动画时间会变的比较长,不然体现不出效果。

减速指数插值终点位置不是1,而是无限逼近1,需要做处理。

这也是要注意的:当t>=1的时候,请直接返回1。不然动画貌似无法停止(懒了,不想查看源码了,改天回来贴源码)。

3、过冲插值或者终点位置回弹的插值

过冲效果的插值最简单可以使用抛体运动的运动方程,上面已述。

另外可以使用简谐运动,即三角函数。下面是一个我写的阻尼运动插值器,模拟的是不断衰减的简谐运动。可以用于一次过冲,也可以模拟在终点位置不断回弹的效果。

import android.view.animation.Interpolator;

/**
 *
 * 
 类描述:阻尼运动插值器
 * 
 * 功能详细描述:
 *
 * @author mengsifan
 * @date [2013-12-27]
 */
public class DampingInterpolator implements Interpolator {

    /** 过冲部分比例,0.0~1.0之间,默认0.5 */
    float mOvershootPercent = 0.5f;
    /** 用于计算阻力的系数,由mOvershootPercent和mRegion求得 */
    float mOvershootModulus;
    /** 回弹次数,最少回弹一次 */
    int mCount = 1;
    /** 整个运动范围,由mCount决定 */
    float mRegion;

    /**
     * <默认构造函数> 默认过冲比例是0.5,回弹次数是1
     */
    public DampingInterpolator() {
        this(1, 0.5f);
    }

    /**
     * @param count
     *            回弹次数
     * @param overshoot
     *            过冲比例,0.0~1.0之间
     */
    public DampingInterpolator(int count, float overshoot) {
        setOverShootCount(count);
        setOverShootPercent(overshoot);
    }

    public void setOverShootCount(int count) {
        mCount = Math.max(1, count);
        mRegion = (float) (Math.PI * 2 * (mCount - 1) + Math.PI / 2 * 3);
        mOvershootModulus = (float) Math.pow(mOvershootPercent, mRegion
                / Math.PI);
    }

    public void setOverShootPercent(float overshoot) {
        mOvershootPercent = Math.max(0, Math.min(1, overshoot));
        /*
         * 当 t * mRegion = Math.PI 的时候,达到第一次过冲的峰值, 则 t = Math.PI / mRegion 。 且此时
         * mOvershootModulus^t = mOvershootPercent , 所以 mOvershootModulus =
         * Math.pow(mOvershootPercent, 1 / t) , 即 mOvershootModulus =
         * Math.pow(mOvershootPercent, mRegion / Math.PI) 。
         */
        mOvershootModulus = (float) Math.pow(mOvershootPercent, mRegion
                / Math.PI);
    }

    public int getOverShootCount() {
        return mCount;
    }

    public float getOverShootPercent() {
        return mOvershootPercent;
    }

    @Override
    public float getInterpolation(float t) {
        if (t <= 0) {
            return 0;
        }
        if (t >= 1) {
            return 1;
        }
        return (float) (1 - Math.pow(mOvershootModulus, t)
                * Math.cos(mRegion * t));
    }
}

Exif在Android上的应用

项目碰到了一个问题:在iOS设备上上传的图片,在Android设备上查看会发现被旋转。经过调查发现原因是iOS上传的照片包含Exif信息,正常的显示应该是在原图片的基础上,根据Exif旋转信息先进行调整再显示即可。

那么,什么是Exif呢?

维基百科:

可交换图像文件格式常被简称为Exif(Exchangeable image file format),是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。

具体可查阅:Exif

Exif一个针对图片文件的跨平台标准,包含了一些列图片的信息,插入到JPEG等文件头部。Exif信息中包含Orientation (rotation)这一项,即图片的方向信息,上面提到的问题产生根源就在于这个属性。

不同的端或者系统对Exif的支持情况

文章开头提到的问题产生的原因就是,iOS上传了包含旋转信息的图片到服务端,而Android端从服务端下载下来图片后,如果没根据Exif中的方向信息进行旋转,就会发生旋转问题,也即:在Android上头像是被旋转的,反而恰恰是因为Android 客户端在显示的时候没有根据Exif的方向信息进行旋转校正。

经过一些简单的测试和反馈发现,iOS设备图片浏览器、Mac端Chrome浏览器等均可显示正确方向的图片,也即这些应用根据Exif信息对图片显示进行了处理。

Android端的话,经过确认如果使用BitmapFactory的一系列decode方法、或者使用Bitmap的一系列createBitmap方法,得到的bitmap并没有根据图片的Exif旋转信息进行校正。Android提供的API(ImageView、BitmapFactory、Bitmap等)或者Android系统层并没有处理Exif的方向信息,所以Android客户端直接decode从服务端下载来的图片byte[]数据为bitmap,会发生头像“被旋转”的现象。

Android项目对Exif方向信息进行支持(解决旋转问题)

Android API提供了ExifInterface获取图片信息的Exif方向信息,使用如下:

ExifInterface exifInterface = new ExifInterface(filePath);   
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);

获取方向后可以进行适当的角度转换:

// Constants used for the Orientation Exif tag.
public static final int ORIENTATION_UNDEFINED = 0;
public static final int ORIENTATION_NORMAL = 1;
public static final int ORIENTATION_FLIP_HORIZONTAL = 2;  // left right reversed mirror
public static final int ORIENTATION_ROTATE_180 = 3;
public static final int ORIENTATION_FLIP_VERTICAL = 4;  // upside down mirror
public static final int ORIENTATION_TRANSPOSE = 5;  // flipped about top-left <--> bottom-right axis
public static final int ORIENTATION_ROTATE_90 = 6;  // rotate 90 cw to right it
public static final int ORIENTATION_TRANSVERSE = 7;  // flipped about top-right <--> bottom-left axis
public static final int ORIENTATION_ROTATE_270 = 8;  // rotate 270 to right it

然后再根据转换后的角度将bitmap方向校正了,这一步不赘述。

Matrix matrix = new Matrix();   
matrix.setRotate(degree);   
Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);

这里是存在一个问题的:即图片必须是手机本地已存储的文件,传入文件路径才可以使用ExifInterface。那么问题就来了,怎么直接从网下下载下来的文件流中读取Exif信息?

答案是:非常不幸,API没提供相关接口。

从图片文件流中读取Exif信息

的确是有一些方法。

1. 使用第三方库metadata-extractor

这个库算是非常符合要求,从文件byte[]读取到了以下信息:

04-13 15:50:39.759: D/EXIF LOG(5830): Directory: Jpeg
04-13 15:50:39.759: D/EXIF LOG(5830): > tag: Compression Type = Baseline
04-13 15:50:39.759: D/EXIF LOG(5830): > tag: Data Precision = 8 bits
04-13 15:50:39.759: D/EXIF LOG(5830): > tag: Image Height = 2448 pixels
04-13 15:50:39.759: D/EXIF LOG(5830): > tag: Image Width = 2449 pixels
04-13 15:50:39.759: D/EXIF LOG(5830): > tag: Number of Components = 3
04-13 15:50:39.760: D/EXIF LOG(5830): > tag: Component 1 = Y component: Quantization table 0, Sampling factors 2 horiz/2 vert
04-13 15:50:39.761: D/EXIF LOG(5830): > tag: Component 2 = Cb component: Quantization table 1, Sampling factors 1 horiz/1 vert
04-13 15:50:39.761: D/EXIF LOG(5830): > tag: Component 3 = Cr component: Quantization table 1, Sampling factors 1 horiz/1 vert
04-13 15:50:39.761: D/EXIF LOG(5830): Directory: Jfif
04-13 15:50:39.761: D/EXIF LOG(5830): > tag: Version = 1.1
04-13 15:50:39.761: D/EXIF LOG(5830): > tag: Resolution Units = none
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: X Resolution = 65535 dots
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Y Resolution = 65535 dots
04-13 15:50:39.762: D/EXIF LOG(5830): Directory: Exif SubIFD
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Color Space = sRGB
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Exif Image Width = 2449 pixels
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Exif Image Height = 2448 pixels
04-13 15:50:39.762: D/EXIF LOG(5830): Directory: Exif IFD0
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Orientation = Right side, top (Rotate 90 CW)
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: X Resolution = 2147483647 dots per inch
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Y Resolution = 2147483647 dots per inch
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Resolution Unit = Inch
04-13 15:50:39.762: D/EXIF LOG(5830): Directory: Photoshop
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: IPTC-NAA Record = 0 bytes binary data
04-13 15:50:39.762: D/EXIF LOG(5830): > tag: Caption Digest = -44 29 -116 -39 -113 0 -78 4 -23 -128 9 -104 -20 -8 66 126
04-13 15:50:39.762: D/EXIF LOG(5830): Directory: Iptc

2. 从文件流中仅获取Exif方向信息。

第1种方法需要引入第三方库,并且大多数时候我们只是处理方向信息而已,所以还有一种简化方法,仅用于获取Exif中的方向信息,传送门(源码亦会附在本文最后):Exif.java

通过getOrientation(byte[] jpeg)方法即可轻松获得图片的方向信息,顺手校正一下再进行显示,完美解决。

附Exif.java源码:

public class Exif {
    private static final String TAG = "CameraExif";

    // Returns the degrees in clockwise. Values are 0, 90, 180, or 270.
    public static int getOrientation(byte[] jpeg) {
        if (jpeg == null) {
            return 0;
        }

        int offset = 0;
        int length = 0;

        // ISO/IEC 10918-1:1993(E)
        while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) {
            int marker = jpeg[offset] & 0xFF;

            // Check if the marker is a padding.
            if (marker == 0xFF) {
                continue;
            }
            offset++;

            // Check if the marker is SOI or TEM.
            if (marker == 0xD8 || marker == 0x01) {
                continue;
            }
            // Check if the marker is EOI or SOS.
            if (marker == 0xD9 || marker == 0xDA) {
                break;
            }

            // Get the length and check if it is reasonable.
            length = pack(jpeg, offset, 2, false);
            if (length < 2 || offset + length > jpeg.length) {
                Log.e(TAG, "Invalid length");
                return 0;
            }

            // Break if the marker is EXIF in APP1.
            if (marker == 0xE1 && length >= 8 &&
                    pack(jpeg, offset + 2, 4, false) == 0x45786966 &&
                    pack(jpeg, offset + 6, 2, false) == 0) {
                offset += 8;
                length -= 8;
                break;
            }

            // Skip other markers.
            offset += length;
            length = 0;
        }

        // JEITA CP-3451 Exif Version 2.2
        if (length > 8) {
            // Identify the byte order.
            int tag = pack(jpeg, offset, 4, false);
            if (tag != 0x49492A00 && tag != 0x4D4D002A) {
                Log.e(TAG, "Invalid byte order");
                return 0;
            }
            boolean littleEndian = (tag == 0x49492A00);

            // Get the offset and check if it is reasonable.
            int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
            if (count < 10 || count > length) {
                Log.e(TAG, "Invalid offset");
                return 0;
            }
            offset += count;
            length -= count;

            // Get the count and go through all the elements.
            count = pack(jpeg, offset - 2, 2, littleEndian);
            while (count-- > 0 && length >= 12) {
                // Get the tag and check if it is orientation.
                tag = pack(jpeg, offset, 2, littleEndian);
                if (tag == 0x0112) {
                    // We do not really care about type and count, do we?
                    int orientation = pack(jpeg, offset + 8, 2, littleEndian);
                    switch (orientation) {
                        case 1:
                            return 0;
                        case 3:
                            return 180;
                        case 6:
                            return 90;
                        case 8:
                            return 270;
                    }
                    Log.i(TAG, "Unsupported orientation");
                    return 0;
                }
                offset += 12;
                length -= 12;
            }
        }

        Log.i(TAG, "Orientation not found");
        return 0;
    }

    private static int pack(byte[] bytes, int offset, int length,
            boolean littleEndian) {
        int step = 1;
        if (littleEndian) {
            offset += length - 1;
            step = -1;
        }

        int value = 0;
        while (length-- > 0) {
            value = (value << 8) | (bytes[offset] & 0xFF);
            offset += step;
        }
        return value;
    }
}

我读书少,你不要骗我

2015-01-14 15:04:46 的屏幕截图

2015-01-14 15:06:36 的屏幕截图

这两天不知为何就冒出来这么一条新闻,Google具体宣布了什么我就不查了,但是感觉新闻报道的有点危言耸听和有失准确。以下单纯聊下漏洞问题。

在很久很久以前,Android系统WebView控件的addJavascriptInterface接口爆出漏洞,详情参见:WebView中接口隐患与手机挂马利用 。如果我没记错的话,当时国内应用甚至包括微信、360家产品等倒了一片,主流浏览器好像就只有UC幸免。

问题漏洞是CVE-2012-6636,API 17以下系统均躺枪,修复的方法是——升!级!系!统!!所以呢,一个比较简单的解决办法是,各家良心企业在自己的App中使用WebView控件的时候,如果是API 17以下则不要使用addJavascriptInterface接口。

这还没完,又陆续爆出了:CVE-2013-4710CVE-2014-1939CVE-2014-7224

这几个漏洞意思大概就是,虽然自己App不使用addJavascriptInterface了,但是系统的一些功能用到了addJavascriptInterface接口,大概有三个:searchBoxJavaBridge_ 、accessibility 、 accessibilityTraversal。所以,最保险的解决方案而是重载addJavascriptInterface为空方法,万一调用了这个接口也不会导致问题。

这样就万无一失了吗?太天真了,图样图森破!

查看源码会发现searchBoxJavaBridge_对象并不是通过调用addJavascriptInterface方法添加的,而是直接塞进去了mJavaScriptObjects的map中(调用addJavascriptInterface方法也是塞到这个map中):

2015-01-14 15:28:32 的屏幕截图

/* package */ static final String JS_INTERFACE_NAME = "searchBoxJavaBridge_";

2015-01-14 15:27:43 的屏幕截图

mSearchBox = new SearchBoxImpl(mWebViewCore, mCallbackProxy);
mJavaScriptObjects.put(SearchBoxImpl.JS_INTERFACE_NAME, mSearchBox);

所以,对于API17以下,使用WebView控件的时候,不光要记得重载addJavascriptInterface为空方法,还要主动移除searchBoxJavaBridge_。

如果有同学有疑问,addJavascriptInterface接口不能用了,那怎么做JS与Native的交互?哎,曲线救国一下吧:在WebViewClient.shouldOverrideUrlLoading()里面捕捉一下iFrame跳转吧……具体不再说了,资料都比较好查。

好啦,讲到这里差不多了,顺便贴一个比较简单的解决方案,继承WebView自定义控件,然后项目中一律使用自定义的WebViewEx控件。

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.webkit.WebView;

/**
 * 
 * 解决Android 4.2以下的WebView注入Javascript对象引发的安全漏洞
 * 
 * @author mengsifan
 * @date [2014年10月9日]
 */
public class WebViewEx extends WebView {

    public WebViewEx(Context context) {
        super(context);
        removeSearchBoxJavaBridgeInterface();
    }

    public WebViewEx(Context context, AttributeSet attrs) {
        super(context, attrs);
        removeSearchBoxJavaBridgeInterface();
    }

    public WebViewEx(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        removeSearchBoxJavaBridgeInterface();
    }

    @SuppressWarnings("deprecation")
    @SuppressLint("NewApi")
    public WebViewEx(Context context, AttributeSet attrs, int defStyle,
            boolean privateBrowsing) {
        super(context, attrs, defStyle, privateBrowsing);
        removeSearchBoxJavaBridgeInterface();
    }

    @SuppressLint("NewApi")
    private void removeSearchBoxJavaBridgeInterface() {
        if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT < 17) {
            removeJavascriptInterface("searchBoxJavaBridge_");
        }
    }

    @Override
    public void addJavascriptInterface(Object object, String name) {
        // API 17以下版本暂时不允许使用addJavascriptInterface方法,防止系统调用造成注入漏洞。
        if (Build.VERSION.SDK_INT >= 17) {
            super.addJavascriptInterface(object, name);
        }
    }
}

【Android】AppCompat-v21下ActionBar的Overflow弹出菜单遮挡ActionBar问题解决

v7 support库更新到v21(Lollipop)之后,使用Overflow弹出的菜单(popup menu)会遮挡住ActionBar,一般情况下应该是菜单的顶部与ActionBar的下端平齐的,具体效果可以参考微信。

本身也不是啥大事情,但是强迫症泛起来了老是想解决他。由于v21比较新 ,能查到的资料比较少,着实折腾了不少功夫。废话少说:

<resources>
    <style name="AppBaseTheme" parent="Theme.AppCompat.Light" />

    <style name="AppTheme" parent="AppBaseTheme">
        <item name="actionOverflowMenuStyle">@style/OverflowMenu</item>
    </style>

    <style name="OverflowMenu" parent="Widget.AppCompat.PopupMenu.Overflow">
        <!--兼容Api 21之前的版本 -->
        <item name="overlapAnchor">false</item>

        <!-- Api 21-->
        <item name="android:overlapAnchor">false</item>
    </style>

</resources>

注意红字部分的内容,第二句可能会报错,提示android:overlapAnchor需要Api21,这种情况下可以分拆成两个style,一个放在value文件夹下:

    <style name="OverflowMenu" parent="Widget.AppCompat.PopupMenu.Overflow">
        <!--兼容Api 21之前的版本 -->
        <item name="overlapAnchor">false</item>
    </style>

然后新建一个value-v21文件夹,并新建style.xml,这里定义一个style:

    <style name="OverflowMenu" parent="Widget.AppCompat.PopupMenu.Overflow">
        <!-- Api 21-->
        <item name="android:overlapAnchor">false</item>
    </style>

同时,如果想改变菜单的位置或者宽度,可以按照以下修改:

    <style name="OverflowMenu" parent="Widget.AppCompat.PopupMenu.Overflow">
        <!--兼容Api 21之前的版本 -->
        <item name="overlapAnchor">false</item>
        <!-- Api 21-->
        <item name="android:overlapAnchor">false</item>
        <!-- 修改竖直方向上与屏幕顶端的距离为20dp -->
        <item name="android:">20dp</item>
    </style>

其他的属性自己按需去定义就好了。

【Android】自定义Style或Theme及自定义ActionBar

以前做Launcher的时候是采用3D引擎,对于UI好多界面都是靠代码布局和绘制,基本上很怎么关注过style和theme(xml)的自定义。现在用到了基本上是从头学习和总结了一番,对多数人来说可能比较初级,blog之简单mark下。干货:

一、定义一个Style。

一些多处用到的style,比如定义一个统一风格的Button(其他控件都一样)。

一个布局xml中正常普通的Button:

<Button
    android:layout_width="fill_parent"
    android:layout_height="@dimen/st_button_normal_height"
    android:layout_gravity="center"
    android:background="@drawable/st_button_normal_bg"
    android:textColor="@color/st_button_normal_textcolor"
    android:textSize="@dimen/st_button_normal_textsize"
    android:layout_marginTop="@dimen/activity_vertical_margin"
    android:text="@string/st_create_activity_create" />

提取红色部分项到style.xml成为自定义style:

<style name="NormalButton">
    <item name="android:layout_gravity">center</item>
    <item name="android:background">@drawable/st_button_normal_bg</item>
    <item name="android:textColor">@color/st_button_normal_textcolor</item>
    <item name="android:textSize">@dimen/st_button_normal_textsize</item>
 </style>

在布局xml的Button中使用:

<Button
    style="@style/NormalButton"
    android:layout_width="fill_parent"
    android:layout_height="@dimen/st_button_normal_height"
    android:layout_marginTop="@dimen/activity_vertical_margin"
    android:text="@string/st_create_activity_create" />

style可继承,可继承自己定义的style,也可以继承Android提供的style:

<style name="CustomButton" parent="NormalButton">
    <item name="android:layout_height">wrap_content</item>
 </style>

二、自定义全局的App Theme。

自定义一个theme:

<color name="custom_theme_color">#b0b0ff</color>
<style name="CustomTheme" parent="android:Theme.Light">
    <item name="android:windowBackground">@color/custom_theme_color</item>
    <item name="android:colorBackground">@color/custom_theme_color</item>
</style>

这里继承了系统的Theme.Light主题,并定义了两个属性,还有其他很多属性标签,自己按需定义,继承的parent自己也可以按需选择。

使用自定义的全局theme:

在AndroidManifest.xml的Application标签中添加:

<application android:theme="@style/CustomTheme">

这样便定义了全局的theme,假如个别的activity想使用其他theme怎么办?其实activity标签也支持设定一个theme,只要在Activity标签中添加同样一句代码即可,并且Android提供了很多的Theme可供选择:

<activity android:theme="@android:style/Theme.Dialog">

三、自定义ActionBar的theme。

因为涉及到Android版本支持的问题,这个略麻烦一点,不过官方和第三方库都有解决方案。

对于Android3.0及之上,自定义theme的时候,设置actionBarStyle即可:

res/values/themes.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- the theme applied to the application or activity -->
    <style name="CustomActionBarTheme"
           parent="@android:style/Theme.Holo.Light.DarkActionBar">
        <item name="android:actionBarStyle">@style/MyActionBar</item>
    </style>

    <!-- ActionBar styles -->
    <style name="MyActionBar"
           parent="@android:style/Widget.Holo.Light.ActionBar.Solid.Inverse">
        <item name="android:background">@drawable/actionbar_background</item>
    </style>
</resources>

然后像中那样设置application的theme即可。

对于2.1~3.0(不含)之间的版本可以使用android的support库v7-appcompat进行ActionBar的支持:

res/values/themes.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- the theme applied to the application or activity -->
    <style name="CustomActionBarTheme"
           parent="@style/Theme.AppCompat.Light.DarkActionBar">
        <item name="android:actionBarStyle">@style/MyActionBar</item>

        <!-- Support library compatibility -->
        <item name="actionBarStyle">@style/MyActionBar</item>
    </style>

    <!-- ActionBar styles -->
    <style name="MyActionBar"
           parent="@style/Widget.AppCompat.Light.ActionBar.Solid.Inverse">
        <item name="android:background">@drawable/actionbar_background</item>

        <!-- Support library compatibility -->
        <item name="background">@drawable/actionbar_background</item>
    </style>
</resources>

注意红字部分必须添加,不然自定义不会生效。

另外是如果使用v7-appcompat库进行ActionBar的支持,则自定义theme必须继承自Theme.AppCompat(或继承自这个theme的其他Android提供的Theme),否则会报错。

四、这是没有什么可总结的总结

一~三基本够用了,需要定义什么属性自己按需就好了。