月度归档:2015年05月

Java的RTTI和反射(一)

什么是RTTI?

RTTI的全称是RunTime Type Information的缩写 ,即可以在程序的运行时发现和使用类型信息。在Java里想要在运行时里获得类型的信息有两种方法,一是传统的RTTI即假定我们在编译时已经知道了所有类型,另一种是反射机制,它允许我们在运行时获得类型信息。

为什么需要RTTI?

面向对象编程中的一个基本目的是,让代码只操控一个对基类的引用,这样在添加新类型也能保证不会影响到原来的代码,且增强可读性。比如你有一个Shape类它里面有一个draw()方法,它有三个派生类Circle,Square,Triangle分别覆写了draw方法。

public class Shapes{
    public static void main(String[] args){
        List<Shape> list = Arrays.asList(new Circle,
        new Square,new Triangle());
        for(Shape shape : list)
            shape.draw();
    }
}

但是这样在实际编程的时候经常会有这样的问题,比如我想调用每个子类特有的方法呢?比如Square和Triangle有rotate这个方法,而这个方法Circle没有,这个时候我们就是需要在运行时获得这个shape对象的具体类型信息,来决定是否可以使用某些特定的方法。

Class对象

根据Java的原理,你应该知道编译Java程序的时候,每个你写的类都会编译产生xxx.class文件.如下图:

class

 

这是Java里一个非常强大的特性,Java不像C++这种静态加载语言(即运行前已经加载了所有类型信息),Java则是在开始运行前并非完全加载的,只有在需要对应的类型时才动态加载的。JVM里有一个叫ClassLoader的系统,它会首先检查要用的这个类是否已经被加载,若未加载则会搜索对应的.class文件进行加载。

Java里有Class类,要获得运行时的类型信息,你需要获得它对应的Class对象的应用.

通常有三种方法来获取对应的Class对象引用:

  • 知道类型的具体名字:

Class.forName(String className),这里的className必须是全限定名(也就是相当于文件的据对路径一样,要包含对应的包名才行)。如果用于forName的这个对象没有被加载,就会被自动加载并初始化

  • 有一个具体对象

object.getClass()即可返回对应的Class

  • 类的字面常量

可以用Class myClass = Test.class来直接创建一个Class引用,注意这里和Class.forName()有一个巨大的不同,这里创建的Class引用并不会自动的初始化该对象。并且这种方式创建还有很多好处,这样会使代码更加简洁(因为用Class.forName()可以会抛出ClassNotFoundException,所以它必须写在try/catch里或者抛出异常),而且更安全,因为这样写编译器在编译就可以对其进行检查。

Class类一些常用的方法介绍

  • 获取类型名字

有getName(),getCanonicalName(),getSimpleName()

  • 创建对应类的对象

Class.newInstance()就可以产生对应的对象,注意这里可能会抛出两种异常,一个是IllegalAcessException和InstantiationException,前者主要是可能因为违反了Java里的安全机制如,该类的构筑函数为private。

  • 检查是否是某一类型

有三种方法,一是使用关键字 instanceOf,如可以用 if (circle instanceOf Shape );二是使用Class.isInstance(Object obj)来检查;三是使用xxx.getClass() == xxx.class 来比较检查。在这里一二两种方法完全等价,但是和第三种有巨大不同,一二是相当于问“你是这个类吗,你是这个类的派生类吗”,而方法三就是只最对具体确切的那一个class。

好了,下次再写反射

Android自定义Activity Animation(一)

在Android里Activity的切换都有默认的动画,不同系统版本不同的ROM的动画可能不同,然而我们可以通过自定义Activity Animation来实现一致的或更炫酷的效果。本篇文章将对(Android 5.0 之后推出了Activity enter and exit transition animation 是后话,这里感兴趣可以去了解一下)

如果你只想要实现简单的替换默认的Activity Animation,你只需要在startActivity(Intent intent)后调用overridePendingTransition (int enterAnim, int exitAnim)调用对应的动画文件就可以实现自定义Activity Animation了。不过下面我将介绍一个稍微复杂一点的Activity Animation是实现。

下面已Ireader的打开书籍的demo作为例子,介绍一下。 自定义的Activity Animation。效果图如下:(gif图,点开大图观看)

ireader_demo

分析:点击书的封面后将打开书籍(此时是打开了另外一个Activity,叫ReadingPageActivity),此时执行我们自定义的Activity动画。这个动画像是直接在该Activity下直接打开的,而不是打开了一个新的窗口,且书籍的打开动画随着书的位置不同有不同放大动画。

实现这个自定义的Activity animation的原理是这样的,当我们需要打开另外一个Activity的时候,我们把原来的window animation给disable掉,然后这样相当于马上开启了我们需要打开的Activity,然后再开始运行我们的自定义动画。

要实现打开的Activity像是直接在当前Activity直接打开的效果,我们需要把Activity的windowBackground设置为透明(不然Activity会有一个默认的Background,造成Activity在另一个窗口打开的效果)。先自定义透明背景的style:

 <style name="Transparent" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
    </style>

然后在AndroidManifest里把对应的Activity的theme设置为这个透明的style:

  <activity
            android:screenOrientation="portrait"
            android:name=".ReadingPageActivity"
            android:label="@string/title_activity_reading_page"
            android:theme="@style/Transparent" >

然后我们要在startActivity(Intent intent)的后面马上调用方法overridePendingTransition(0,0)来关闭Window的动画。

现在已经完成了基本的准备工作,下一篇将讲解如何实现,图片的放大和翻书动画。

launch_adv

Android Activity的4种LaunchMode

Activity有4种启动模式,分别是Standard,SingleTop,SingleTask,SingleInstance。要了解清楚这四种模式,必须先解释清楚两个概念,分别是Task和Activity Back Stack。Task可以理解成一系列Activity的合集,这个合集是已栈(Stack)的形式来存放Activity的,如图:

task

Task又可以有前台(Foreground)和后台(Background)两种状态,比如当前用户正在交互的app它的task就是在前台的,如果此时用户按下Home键当前的这个task就会转成后台的状态,后台的task里的Activity将停止。如果用户再次点击这个app的图标,这个task将再次返回前台。Activity的Back Stack是这样工作的,用户当前交互的Activity总是在栈顶的,假设当前用户在Activity A里,此时点击了一个按钮打开了Activity B,此时B就会被压到栈顶,栈里的内容将是 A-B,如果用户在B中又开启了Activity C,栈内容将变成A-B-C。如果此时按Back键,Activity C将会被弹栈,B将成为栈顶再次被用户可见。这里唯一需要注意的问题是,这个Stack里的Activity顺序是不能改变(rearrange),这个栈只能进行push和pop的操作。

有了前面的知识,我们就可以开始解释4种Launch Mode了:

  • Standard

这个是Android的默认标准模式,每次打开一个Activity都将新创建一个Activity压入栈中,比如现在栈里的情况是有A-B-C这三个Activity,我们想在C中再打开一个Activity C,此时Android的默认行为将是新建一个Activity并压栈,栈里的内容将变为A-B-C-C。很容易看出,在某些使用情况下种子模式会产生一种很糟糕的用户体验,比如在一个Activity里可以点开搜索功能(SearchActivity),每次重新搜索将会重新打开SearchActivity,如果用户反复搜索10次,在Standard模式下,SearchActivity将会被重复创建10次,压栈10次,用户如果想通过按Back返回到原来的Activity那得按10次才能返回到最初的Activity!

standard_problem

 

  •   SingleTop

这种模式刚好可以解决上述的问题。它是这样工作的,如果当前要打开的Activity刚好已经存在于Back Stack的顶端它将不再重新创建一个Activity而是使用原来的Activity。比如现在栈里的Activity有A-B-C,如果我想在C中再打开一个Activity,此时发现栈顶已经有C了,我们就直接使用这个C而不是重新创建一个Activity压栈了。当然如果栈里是A-B-C,此时要打开B,还是会重新创建B并压栈。

  • SingleTask

SingleTask的Activity永远在task的根部,意思是如果这个Activity不在当前的Task的根部,它会新建立一个Task然后进行压栈。如果这个SingleTask Activity在当前task的根部,在这个Activity之上的所有Activity将进行弹栈,保证这个SingleTask Activity重新到达栈顶。如栈里有A-B-C-D,若Activity A定义为SingleTask模式,此时在D里我们要再打开一个A,栈里B,C,D将会被弹栈。若栈内为B-C-D (task1),在D中需要打开一个A,将会新建一个新的task2,里面压入A。这里还有一种情况,如果当前的task1的栈里有A-B-C (假设E为SingleTask Activity),这个时候需要打开E,且当前的系统里E已经存在了在task2中为 D-E-F,此时系统会把task2中E以上的Activity弹栈,task2切到前台(foreground),task1在后台(background),如果一直按下back键将把task2里的所有Activity清空了,才可以回到task1里。

  •  SingleInstance

这种模式和singleTask基本相同,唯一的不同的地方是定义为singleInstance的Activity单独存在于一个task里,其上不能再压入Activity。举一个例子,现在我有A,B,C三个Activity,其中Activity B是定义为SingleInstance的,如果我在A中启动B(A在task1里),B将新建一个task2并压入其中,如果在Activity B中我们再打开C,此时C将压入task1的栈而不是task2的栈!此时task1的栈中元素将是A-C,task2的栈是B。此时若回退将从C->A->B这个顺序回退.

总结:我们可以把4种LaunchMode分成两类,一类是Activity可以多次实例化的,如Standard和SingleTop,另一类则是Activity都是单例的(Singleton)的,如SingleTask和SingleInstance。通过改变Activity的LaunchMode在一定的场景下可以获得更好的交互体验。

参考:

ps:这个问题以前自己没有注意过,面试的时候没有当上来,写一篇博客纪念一下。

 

Singleton模式

简述:构造的这种类型,它只能有一个对象。

这样做的优点是,有时候重复生成多个对象,但其实只需要用一个就够了。重复生成浪费资源。

实现Singleton有以下几种方法:

  1. 让构造函数设为private,在类里建立一个为public static final的对象
    public class  Sprite {
        public static final Sprite INSTANCE = new Sprite();
    
        private Sprite() {……}
    	
    }
  2.  让构造函数设置为private,让类里这个对象的实例private,设置一个public static的方法可以获取这个对象(这里用的就是static factory模式)
    public class  Sprite {
        private final Sprite INSTANCE = new Sprite();
    
        private Sprite() {……}
    
        public static Sprite getInstance() { return INSTANCE; }
    }

以上的两种方法有局限性,就是他们都是通过设置private来达到Singleton化的,而private的构造函数在一些反射方法里可以被特殊的调用。还有在Serializable(序列化)的时候比较麻烦,每当解析一个序列化的时候这个时候会产生一个新的对象,这样就造成我们之前说的唯一一个实例的初衷,这里的解决方案是复写Serialzable的readResolve方法,来继续维护Singleton。

Effect Java的作者提到了,最好的实现Singleton的方法,应该是使用枚举。

public enum Singleton {
    INSTANCE;
    
    // other metho
    public void execute () {
        // Perform operation here 
    }
}

使用的时候只要Singleton.INSTANCE就好,非常的简介优雅。这个方法是利用了Java确保枚举类型在程序里只初始化一次来实现的。

下面再介绍一下,在Android下一种实现Singleton模式的方法。最简单的是通过继承Application实现,这也是利用Android保证了一个app一个Application的实例的特点实现的。下面代码是把Volley里的RequestQueue单例化:

public class MyApplication extends Application{
    private static RequestQueue mRequestQueue;
    public static synchronized MyApplication getInstance(){
        return mInstance;
    }
     public static RequestQueue getRequestQueue() {
        if (mRequestQueue == null) {
            mRequestQueue = Volley.newRequestQueue(getInstance());
        }
        return mRequestQueue;
    }
}

 

有趣的视差效果(Parallax effect)

Parallax (视差)这个名词是源自希腊文的παράλλαξις(parallaxis),意思是”改变”。从不同的位置观察,越近的物体有着越大的视差,因此视差可以确定物体的距离。——Wikipedia

简单的说就是,距离理我们远的东西我们看起来移动的慢,距离近的东西移动速度看起来更快。效果如图所示:

Parallax

“Parallax”作者Nathaniel Domek 来自维基共享资源

用这个简单的原理就可以实现android应用里常见的欢迎页动画了,效果图如下:(gif录制软件帧率太低,效果不好,点开看动图)
demo1 demo2

先从右图(横向的视差效果)开始解释如何实现吧。这个是一个ViewPager,我们只要自定义它的翻页时候的动画就行,所以我们只要自己实现一个ViewPager.PageTransformer就可以了,我们的视差效果也都是在里面实现的。每一页的布局如下,最上面的是big_title,中间的是icon_img,下面的叫small_title.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
                android:background="@drawable/p1"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="A Large Title"
        android:id="@+id/big_title"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:textSize="45sp"
        android:textColor="#fff"
        android:layout_marginTop="70dp"/>

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/icon_img"
        android:src="@drawable/icon1"
        android:layout_centerInParent="true"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="16dp"
        android:textSize="20sp"
        android:text="   An awesome parallax effect!\nBring more joy to the user"
        android:id="@+id/small_title"
        android:layout_below="@+id/icon_img"
        android:layout_centerInParent="true"
        android:textColor="#fff"
        android:layout_marginTop="32dp"/>
</RelativeLayout>

我们的目标是实现这三个东西已不同的速度向滑动方向移动。

  • 如何移动呢?

用View的TranslationX(view相对于最左的偏移)来实现对View的移动。

  • 不同的速度如何确定?

假设big_title距离我们最近移动速度最快,small_title距离我们最远移动速度最慢。icon_img在中间。

下面来看PageTransformer的代码:

/**
 * Created by Jiaqi Ning on 28/4/2015.
 */
public class ParallaxTransformer implements ViewPager.PageTransformer {
    private boolean isSpeedReverse;
    private int[] resIds;
    private float speedEffect;
    private float distanceEffect;

    public ParallaxTransformer(float speed,float distance,
                               int[] resIds,boolean isSpeedReverse){
        this.isSpeedReverse = isSpeedReverse;
        this.resIds = resIds;
        this.speedEffect = speed;
        this.distanceEffect = distance;
        if (isSpeedReverse)
            this.speedEffect *= -1;
    }
    @Override
    public void transformPage(View page, float position) {

       float moveLength = page.getWidth() * speedEffect;
        for (int i = 0;i < resIds.length;i++){
            View view = page.findViewById(resIds[i]);
            if (view != null){
                view.setTranslationX(moveLength * position);
            }
            moveLength *= distanceEffect;
        }

    }
}

首先构造这个类需要加入Parallax效果的views的id数组(index越小的说明“距离”越近,为什么请看代码)。moveLength相当于移动的速度大小,最终要移动的距离由参数position*moveLength来决定(在ViewPager里position的范围是-1~1,当前页完全沾满屏幕在正中间的时候position返回0,当前页完全在左边不可见时为-1,同理滑动到右边完全不可见时是1)。

这样用就可以啦:

 // Instantiate a ViewPager and a PagerAdapter.
        mPager = (ViewPager) findViewById(R.id.pager);
        mPagerAdapter = new SlidePagerAdapter(getSupportFragmentManager(), MainActivity.TYPE.NORMAL_TYPE);
        mPager.setAdapter(mPagerAdapter);
        int[] resId = {R.id.title_big,R.id.icon_img,R.id.small_title};
        mPager.setPageTransformer(true, new ParallaxTransformer(speed, distance, resId, false));

上面实现的只是横向滑动的Parallax效果,垂直的怎么实现呢?已这个界面为例QQ截图20150505213817图片应该保持和页面的运动方向相反,比如整个页面向上运动,图片应该向下运动。为什么是图片要和页面运动方向相反呢?因为当页面向上滚动的时候,图片也随之向上移动了,如果我们设置translationY让它已相反的方向移动,就造成了一种它在慢速移动的效果。

这里还会有一个问题,因为垂直滚动需要用ScrollView,但是自带的ScrollView并没有提供给我们onScrollChangeListener,所以我们要自己写一个这样的接口来告知我们页面滚动的情况:

/**
 * @author Cyril Mottier
 */
public class NotifyingScrollView extends ScrollView {
    public interface OnScrollChangedListener {
        void onScrollChanged(ScrollView who, int l, int t, int oldl, int oldt);
    }
    private OnScrollChangedListener mOnScrollChangedListener;
    
    public NotifyingScrollView(Context context) {
        super(context);
    }
    public NotifyingScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public NotifyingScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    public void setOnScrollChangedListener(OnScrollChangedListener listener) {
        mOnScrollChangedListener = listener;
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (mOnScrollChangedListener != null) {
            mOnScrollChangedListener.onScrollChanged(this, l, t, oldl, oldt);
        }
    }

}

然后看我们是如何设置图片的translationY的:

  private NotifyingScrollView.OnScrollChangedListener mOnScrollChangedListener = new NotifyingScrollView.OnScrollChangedListener() {
        public void onScrollChanged(ScrollView who, int l, int t, int oldl, int oldt) {
            if (l < mCoverImageHeight){
                final float ratio = (float) Math.min(Math.max(t, 0), mCoverImageHeight) / mCoverImageHeight;
                mCoverImageView.setTranslationY(ratio* mCoverImageHeight * verticalParalllaxSpeed);
            }
        }
    };

OK!结束了! Demo源代码 在github上:https://github.com/qianlvable/ParallaxEffectDemo