实习学习记录(一)

偷懒一下,打算写一个小系列记录实习期间学习到的知识点总结一下。

View绘制的流程

View和Activity一样其实也有自己的生命周期(这里的生命周期不是指像Activity一样有onPause,onResume这种一样,这里指代的是View的一辈子所要干的事的一种抽象概念)。不过View的生命周期远比Activity的生命周期简单。View的生命周期里主要做了三件事:Attachment/Detachment,遍历(英文是Traversal,View的树形结构遍历,里面包括了onMeasure,onLayout,onDraw等方法的递归遍历调用),保存恢复数据 。

关于Attachment:就像Fragment要依附于Activity一样它又一个onAttach的方法一样。View也有一个叫onAttachedToWindow的方法。在这个方法里面,我们一般进行资源分配,注册监听器。如果你看ListView的源代码你可以发现,google在这里对Adapter注册了监听器用于监听数据变化。对应的View还有onDetachedWindow,在这里一般把分配的资源释放掉,还有把监听器弄掉。

关于遍历:

View在你看到前系统会对它走如下的流程:

Animate->Measure->Layout->Draw

Measure主要是要确定自己到底要占多少位置,多少大小。比如你要知道match_parent和wrap_content的时候你到底需要多大。Layout就是需要确定 child view的显示位置。Draw就是对view进行绘制。

Measure里新手第一次见到的时候会被好多新东西吓到。我来一个个解说,你先是会发现有一个MeasureSpec的数据结构,这个是对View的宽高,还有测量模式的一个结构封装。可以用MeasureSpec.getSize,MeasureSpec.getMode来进行调用。这里的Mode有三种模式:

  • Exactly:比如你确切的给view一个大小如200dp,还有match_parent,LinerLayout里的weight这种都属于Exactly确切模式
  • AT_MOST:这里是Wrap_content的应用。
  • UNSPECIFIED:不确定模式,一般是在ListView和ScrollView这种内容长度不确定的东西。

这里其实我们再自己自定义控件的时候我们没必要考虑每一种模式的计算情况,因为毕竟是自己使用,你可以假定我们永远只用match_parent啊或者是永远wrap_content,所以你主需要写一种模式的测量方法即可。(当然除非你想确保你的控件的普遍适用性,你可以考虑每一种模式)

onLayout

这里是利用前面测量好的数据(getMeasureWidth和getMeasureHeight)来确认view里的child view的放置位置。这里要注意有意思的一点是,View.getWidth和View.getHeight只有在onLayout完之后才可以使用,不然在之前调用返回的都是0.(这种返回0的情况我想很多人在写自己自定义的ViewGroup还有在自己自定义的Adapter的时候都会遇到)那是不是在view显示过一次之前我们都无法获得view的宽高了呢?并不是呢,我们这里还有一个利器叫onPreDrawListener,这个监听器的触发时间将在onLayout之后和onDraw之前。

Draw:这里面用得最多得就是invalidate。和requestLayout类似,调用它以后会触发View树的遍历,就是对它的parent一直进行递归调用直到到了view root。这里注意invalidate是我在UI线程里调用的函数,如果我在非UI线程的时候需要调用postInvalidate。

状态保存:一般是通过Override,onSaveInstanceState和onRestoreInstanceState实现。

clipRect的使用:如果每次调用invalidate的时候都把整个view重绘,将是很耗性能的事情。我们可以通过规定每次需要重绘的区域(一个矩形Rect)让GPU知道每次我只需要对View的这部分显示进行更新就好。如下面这个例子,在这个自定义View包括了文字还有一个按钮,按钮包括一个进度条显示的功能。若果每次只需要更新的是按钮的进度,而重绘整个view就显得有点浪费。可以我们可以选择每次只更新那个按钮的位置。通过设置canvas.clipRect(Rect clipArea)即可。clip

StaticLayout用于在canvas里绘制文字

这个相当于隐藏的API,在canvas里我们绘文字的时候,drawText方法字符串有多长它就画多长,它不会自动换行。用StaticLayout可以解决这个问题。通过建立一个StaticLayout区域,在里面drawText是会自动换行的。TextView就是基于这个实现的。想实现drawText文字换行还有一招,只是有点麻烦思路如下:获取显示区域大小如DisplayWidth,然后测量用画笔画所需字符串将得到的宽度TextWidth(这里是通过调用Paint.getTextBounds(),或Paint。getTextWidths获得,区别是前者是返回int后者是float。后者更精准)如果TextWidth > DisplayWidth我们就对要显示的字符串进行substring然后在下一行的位置进行显示(下一行的位置也是我们自己计算的,用前面得到的TextBounds来获得getHeight)比较麻烦。代码我就不放了,实现效果大概如下:

QQ图片20150726112820

曲线的魔力——初识贝塞尔曲线

为什么我们需要贝塞尔曲线?

设想一下平时曲线对计算机来说是画出来的呢。无非就是要用一个函数y = f(x)来绘制曲线。但是我们能拿到手就用的函数曲线无非是sin,cos这种函数了,若我们想要更复杂的曲线,获取它的方程将会是很难的。

curve
如何产生这个曲线的方程呢?

这个时候,贝塞尔曲线的作用就来了。

什么是贝塞尔曲线?

贝塞尔曲线(英语:Bézier curve)是电脑图形学中相当重要的参数曲线。是用几个很少的控制点就可以产生平滑复杂的曲线的方法。我们经常在开发中用贝塞尔曲线来绘制曲线,或者控制动画的时间变化(擦除函数)。

下面展示一下贝塞尔曲线算法的绘制过程:

先定义三个点

贝塞尔1

 

在第一条线段上任选一个点 D。计算该点到线段起点的距离 AD,与该线段总长 AB 的比例。

贝塞尔2

 

根据上一步得到的比例,从第二条线段上找出对应的点 E,使得 AD:AB = BE:BC

贝塞尔3

 

连接D,E得

贝塞尔4

 

从新的线段 DE 上再次找出相同比例的点 F,使得 DF:DE = AD:AB = BE:BC。

贝塞尔6如果把这个过程反复的做下去将形成一个曲线!

贝塞尔5

 

wiki上的动图效果如下:(好像要点开才看得到)

Bézier_2_big

 

当然控制点还可以有更多来操作出更复杂的曲线。

在Android里的应用

Android里的Path提供了画贝塞尔曲线的API,下面我来做一个简单介绍。

Path包含贝塞尔曲线的API主要包括,quadTo,cubicTo,rCubicTo,rQuadTo。

其中quadTo是有两个控制点的贝赛尔曲线,cubicTo有三个控制点。

例子:

cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)

/**
 * Created by Jiaqi Ning on 5/7/2015.
 */
public class PathView extends View {
    Paint paint;

    public PathView(Context context) {
        super(context);

        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setColor(0xff41be9d);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    //draw the curve
        Path path = new Path();
        path.moveTo(50,50);
        path.cubicTo(50,50,50,300,250,300);
        canvas.drawPath(path,paint);
// draw the control line
        path.reset();
        paint.setColor(Color.GRAY);
        paint.setStrokeWidth(2);
        path.moveTo(50,50);
        path.lineTo(50,300);
        path.lineTo(250,300);
        canvas.drawPath(path,paint);

    }
}

curve_screenshot更强大的用法,考完试再写吧 Orz…

参考:

 

 

新玩具:Android里的DataBinding

为什么我们需要Data Binding

传统的MVC模式如下图所示:

mvc这种模式的初衷是让业务逻辑和View分开,让我们在修改界面而不改变业务逻辑的时候更简单,但是实习操作的时候往往很难完全对View和Controller或Model很好的分离。并且三种组件也增加了一定的复杂度。而当我们有了Data Binding技术我们就可以使用另外一种模式(这种模式常用于Windows软件开发)叫MVVM 全称是Model-View-ViewModel,Model代表的是你的业务逻辑,View是展示的视图,ViewModel是把二者绑定起来的接口。这样说还是有点抽象,在一般Android开发里,如果某个View展示的数据出现了变化,我们需要findViewById找到这个View,并且设置新更新的数值。而在MVVC里,因为Model和View是绑定的,如果model层的数据发送了变化,会自动通知更新view应该如何重新变化展示。听起来是不是非常棒哈。

 如何在Android里使用

  • 准备环境

在整个工程里的build.gradle里添加如下依赖:

  dependencies {
       classpath "com.android.tools.build:gradle:1.3.0-beta1"
       classpath "com.android.databinding:dataBinder:1.0-rc0"
   }

在你需要的使用data binding的module对应的build.gradle里添加

apply plugin: 'com.android.databinding'

ps:这里需要Android Studio是1.3以上,gradle版本在1.3以上,我之前即使是这样设置了还是有问题,我把build tool还有兼容包全部升级到最新版本就木有问题了。

下面我将已一个例子来介绍Android的DataBinding,这个小程序叫MagicNumber,app里显示一个数从1开始,有一个按钮,按一次按钮数字加1,若数字是奇数数字将显示黄色,若是偶数显示蓝色。

 

 

evenodd

先写布局:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="num" type="com.lvable.databindingtest.MagicNumber"/>
    </data>
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="30sp"
            android:text="The magic number"
            android:id="@+id/textView"
            android:layout_marginTop="43dp"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true"/>

        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="67dp"
            android:textSize="60sp"
            android:layout_below="@+id/textView"
            android:layout_centerHorizontal="true"
            android:text="@{num.num}"
            android:textColor="@{num.odd ? @color/light_yellow : @color/light_blue}"
            />

        <Button
            android:text="Add"
            android:id="@+id/btn"
            android:onClick="addCount"
            android:layout_below="@id/textView2"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="20dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </RelativeLayout>

</layout>

注意如下变化:布局文件里的根元素不再是ViewGroup而是一个叫layout的标签,这个标签里包含一个data标签还有一个View,data标签里面包含了要绑定的数据元素类型申明等信息。在本例子里,我们要绑定的数据类型叫MagicNumber,然后在布局里它的名字叫num,type是这个类的完整路径名(若是Java自带的类型直接写就好,比如String,int)。在其他地方若要用到这个绑定数据的值只需要用@ {obj.xxxx}就行。这里要注意一个问题,就是在访问obj的成员变量的时候不一定总是它的全名,我试了一下我的MagicNumber里有一个成员变量叫isOdd,但是要访问它的话我要这样写 @{num.odd}。在这里还可以使用一些语句来实现简单的逻辑操作,比如我在布局文件里写入了奇偶不同颜色不同的逻辑。在textView里

android:textColor="@{num.odd ? @color/light_yellow : @color/light_blue}"

带Observable模式的MagicNumber类

Observable模式就是,当绑定的数据发送变化的时候,这个绑定的View会自己更新显示信息。这里可以使用MagicNumber继承一个叫BaseObservable的类来实现。(也可以使用ObservableField来实现对某一个field的Observe而不用继承)。

package com.lvable.databindingtest;

import android.databinding.BaseObservable;
import android.databinding.Bindable;

import com.lvable.databindingtest.BR;


/**
 * Created by Jiaqi Ning on 21/6/2015.
 */
public class MagicNumber extends BaseObservable {
    private String num;
    private boolean isOdd;
    
    public MagicNumber(int num){
        this.num = Integer.toString(num);
    }
   
    @Bindable
    public String getNum() {
        return num;
    }
   
    @Bindable
    public boolean isOdd() {
        return isOdd;
    }
    
    public void addOne(){
        int c = Integer.parseInt(num);
        num = Integer.toString(++c);
        updateEven();
        notifyPropertyChanged(BR.num);
        notifyPropertyChanged(BR.odd);
    }

    private void updateEven(){
        int c = Integer.parseInt(num);
        isOdd = ((c % 2) != 0) ? true : false;
    }
}

这里要注意,需要有更新通知功能的成员变量的get方法里,添加@Bindable这个注解,然后在起更新数据的地方调用对应的notifyPropertyChanged(BR.xxx)。在本程序里是如果MagicNumber调用了addOne就通知数据更新。

(注:BR 是编译阶段生成的一个类,功能与 R.java 类似,用 @Bindable 标记过 getter 方法会在 BR 中生成一个 entry,当我们通过代码可以看出,当数据发生变化时还是需要手动发出通知。)

好了,终于到了最后真正实现绑定的一步了!

public class MainActivity extends AppCompatActivity {
    MagicNumber magicNumber = new MagicNumber(1);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setNum(magicNumber);
    }

    public void addCount(View view){
        magicNumber.addOne();
    }

}

注意,这里不再使用setContentView(),使用的DataBindingUtil.setContentView()返回一个binding,用它来绑定数据。(这里的set方法是更具之前data里写入的type动态生成的)。

还有一个更好用的特性,若你在布局文件里给了某个View一个ID,在Activity里你想要使用这个View你不再需要些findViewById,你只需要从前面绑定产生的那个binding.xxxx (xxx是view的id名)就可以直接对这个View进行操作了!Awesome!

参考:    Google官方guide

很好的binding demo教程

 

初识Java里RTTI和反射(二)

今天才突然发现,RTTI其实主要是C++里的说法和反射其实表达的意思差不多(反射主要是Java里对RTTI的一种实现),有一个主要的区别是,对RTTI来说,编译器会在编译期打开和检查class文件。但对“反射”来说,class文件在编译期间是不可使用的,而是由运行时环境打开和检查。

动机

一开始觉得动态获取某个类的类型信息好像觉得平时根本用不上,那到底反射他的使用场景是什么呢?我了解的场景有下:

  • 插件化构件的程序就会需要到这个强大的技术了。也就是新加的插件,本来不在原程序里,新的插件是从网络上下载下来,这个类不在原来的程序空间里。
  • 远程方法调用(RMI),它允许一个Java程序将对象分布到多台机器上。例如,你可能正在执行一项需要大量计算的任务,为了提高运算速度,想将计算划分到许多小的计算单元上。
  • 在Android里扩充Framework。如果厂家需要修改Framework,要在里面的某类里添加使用自定义的Jar包,使用该自定义Jar包的方法是用import,但是为保存和原生Framework的兼容性,对原生Framework的最少修改,可以使用ClassLoader动态加载自定义Jar包。

Reflect常用类方法介

  • Method

可以在获取某个Class对象包含的所有方法getMethods(),也可用getMethod(String name,Class<?>… parameterTypes)来获取特定的方法,例子如下:

Class clzz = Class.forName("DummyClass");
Method methods = clzz.getMethods();
for(Metho m : methos){
    m.toString();
}
// get the function name is "display",and the param type is Stirng
Method action = clzz.getMethod("display",String.TYPE);
// call this function
action.invoke("test");
  • getConstruct

getConstructs返回所有构造函数,getConstructor(Class<?>… parameterTypes)返回特定参数的构造函数。

 

考虑过几天敲一下,一个Android插件化构架的例子 : )

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的动画。

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

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:这个问题以前自己没有注意过,面试的时候没有当上来,写一篇博客纪念一下。

 

有趣的视差效果(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

SVG动画——Android 5.0新特性介绍(2)

先放点motivation

(GIF图点开,看动画)

test要实现这个布局很简单,用FrameLayout加一个ImageView并设置一个图片,再在FrameLayout上放一个RelativeLayout,背景用t这个矢量图。布局文件如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:padding="20dp"
                tools:context=".MainActivity">
    <FrameLayout
        android:id="@+id/card_view"
        android:layout_centerInParent="true"
        android:layout_width="260dp"
        android:layout_height="430dp"
        >

        <ImageView
            android:id="@+id/img"
            android:src="@drawable/p1"
            android:scaleType="centerCrop"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
        <RelativeLayout
            android:id="@+id/card_content"
            android:background="@drawable/animate_rect"
            android:layout_width="match_parent"
            android:layout_height="320dp"
            android:padding="15dp">
            <TextView
                android:id="@+id/title"
                android:textColor="#03A9F4"
                android:textSize="25sp"
                android:layout_marginTop="6dp"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Vector card"
                android:layout_alignParentTop="true"
                android:layout_centerHorizontal="true"/>
            <TextView
                android:id="@+id/tv_content"
                android:gravity="center"
                android:layout_marginTop="18dp"
                android:layout_below="@id/title"
                android:text="Lorem ipsum dolor sit amet, Duis aute irure dolor in reprehenderit sunt in culpa quilaborum. in voluptate velit esse cillum dolore eu fugiat nulla ."
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />


        </RelativeLayout>

    </FrameLayout>
</RelativeLayout>

怎么让它动起来呢?其实很简单,只要修改SVG图中的pathData就行,也就是从梯形变成一个长方形。原来的pathData的坐标和要变化的新坐标如图所示,t

 

SVG文件内容:sharp_rect.xml

注意,我们把path加了一个name=”sharp_rect”,这是做动画效果是要指定这部分,所以需要给个独一无二的name作为id.

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="180dp"
        android:height="320dp"
        android:viewportWidth="180"
        android:viewportHeight="400">

    <path
        android:name="sharp_rect"
        android:fillColor="#000000"
        android:pathData="M 180,230 L 0,320 0,0 180,0 z" />
</vector>

下面做animate-vector drawable:

<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/sharp_rect">
    <target
        android:animation="@animator/to_rect"
        android:name="sharp_rect"/>
</animated-vector>

在animated-vector里填入android:drawable=”@drawable/sharp_rect”,这是指定要产生动画的SVG。target里是需要动画的对象,这里我们的对象就是前面那个叫sharp_rect的梯形,所以name里填上它,然后animation填的是对应的动画文件。

动画用的是ObjectAnimator做的,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
                android:duration="330"
                android:interpolator="@android:interpolator/decelerate_cubic"
                android:propertyName="pathData"
                android:valueType="pathType"
                android:valueFrom="M 180,230 L 0,320 0,0 180,0 z"
                android:valueTo="M 180,75 L 0,75 0,0 180,0 z" />

值得注意的是,我们要从梯形变成矩形,就是对pathData进行修改,也就是propertyName要填入pathData,valueType是pathType,valueFrom是原来的路径(也就是梯形的路径),最终变成的效果是valueTo(矩形的路径)。这里有个要注意的地方,如果要进行path的变换,里面的点数必须要一样!

 

Ok了大功告成,最后只要把ImageView的src改成animate_rect就行了,并设置触发函数就行了

 final ImageView play = (ImageView)findViewById(R.id.icon_play);
        play.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Drawable drawable = play.getDrawable();
                if ( drawable instanceof Animatable){
                    ((Animatable) drawable).start();
                }
            }
        });

注意,上面我这种方法不是Android SVG推荐的用法,SVG动画尺寸应该尽可能的小,和简单,因为每次动画都会先把这些path先计算绘成Bitmap,然后上传texture到GPU,如果SVG太大意味着生成更大的Bitmap,占更多内存,消耗更多时间.Google的推荐是把SVG用于图标(icon)和按钮(Button),只有需要的时候才修改Vector的属性(比如alpha,width,height),因为如果SVG不用于动画,android会把这个图生成一个Cache来节省时间,如果SVG动画这个Cache就没有用了。如果要用SVG动画,请确保它“短小精悍”(Short and sweet)。

下面还有一些SVG动画的例子:

(GIF图,点开看)

35ed6362d41a2a3e5a64d3bb8ed16033

Demo源代码: https://github.com/qianlvable/VectorCard

参考:

DevBytes: Android Vector Graphics (youtube视频,需翻墙)

VectorDrawables – Styling Android 系列

Transitioning to Infinity (一个很厉害的法国人,不过他的程序都是用C#写的,本文的BlueTooth的动画来源于这个博客)

SVG图像——Android 5.0新特性介绍(1)

本文将介绍什么是SVG图像,及如何在Android中使用SVG及一些简单的SVG动画。

什么是SVG?

SVG的全称是Scalable Vector Graphics,叫可缩放矢量图形。它和位图(Bitmap)相对,SVG不会像位图一样因为缩放而让图片质量下降。下面这个图片展示了SVG和位图的区别,左边的位图在放大后出现了锯齿,而右边的SVG任然清晰。svg_cp

 

为什么要使用SVG?

  • 节约空间,使用方便

因为SVG可以任意缩放而图像任保持清晰的特点,且在android里SVG是在运行时才进行渲染的,它会根据屏幕的dpi自动缩放到合适的大小,所以我们仅仅用一个SVG文件就可以代替多个不同dpi的drawable了!下面给出一个用SVG的android logo代替多种dpi的drawable的占用空间大小对比.svg_size_cp

  • 用XML就可以写出一些简单的动画。下面会给例子

如何在Android中使用?

  • 把普通SVG图片转成Android可用格式:

有两种方法,手动改一下原始SVG的格式,或者用一个自动转换的工具(Android SVG to VectorDrawable)

下面展示一下,这个图像的SVG文件内容t

<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 180 400" width="180px" height="320" preserveAspectRatio="none">
    <path d="M 180,230 L 0,320 0,0 180,0 z" fill="#000000"/>
</svg>

path是画出这个矢量图所需的笔画路径,这里就是移植到android所需要的关键部分。下面来看转换成android能用SVG图内容,这里我将重点讲解

sharp_rect.xml

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="180dp"
        android:height="320dp"
        android:viewportWidth="180"
        android:viewportHeight="400">

    <path
        android:name="sharp_rect"
        android:fillColor="#000000"
        android:pathData="M 180,230 L 0,320 0,0 180,0 z" />
</vector>

注意到和普通SVG图内容不同地方在与viewBox那变成了viewportWidth,viewportHeight,viewport就相当于画这个SVG的画布大小。width和height是规定这个SVG图像最终的显示大小的,一般用dp表示。第二个不同是有一个普通SVG里的fill到android里要变成fillColor,这里就是SVG图像填充的颜色。第三点不同是,普通SVG的path的数据是d开头的标签,在android里要写成pathData。综上所述,只要把viewBox的大小改成viewport的大小,把填充颜色的fill改成fillColor,把Path中的d,改成pathData就行了。

还有最后一个问题pathData中的那些奇怪的东西是啥?数字是在viewport中的坐标点,M代表move to(把画笔移动到),L是Line(划线),Z是封闭path.(更多参考

我们可以通过上面的变动手动把普通SVG转成android用的VectorDrawable,或者我们可以用这个网站自动完成转换 http://inloop.github.io/svg2android/

 

  • 显示

这个SVG就直接就是Drawable直接用就好:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:padding="20dp"
                tools:context=".MainActivity">
         <ImageView
            android:src="@drawable/sharp_rect"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />        
</RelativeLayout>

 

下一篇文章将介绍一个使用SVG图像动画的例子: )