Android Context完全解析,你不知道的Context的各种细节

Context相信所有的Android开发人员基本上每天都在接触,因为它太常见了。但是这并不代表Context没有什么东西好讲的,实际上Context有太多小的细节并不被大家所关注,那么今天我们就来学习一下那些你所不知道的细节。

 

Context类型

我们知道,Android应用都是使用Java语言来编写的,那么大家可以思考一下,一个Android程序和一个Java程序,他们*大的区别在哪里?划分界限又是什么呢?其实简单点分析,Android程序不像Java程序一样,随便创建一个类,写个main()方法就能跑了,而是要有一个完整的Android工程环境,在这个环境下,我们有像Activity、Service、BroadcastReceiver等系统组件,而这些组件并不是像一个普通的Java对象new一下就能创建实例的了,而是要有它们各自的上下文环境,也就是我们这里讨论的Context。可以这样讲,Context是维持Android程序中各组件能够正常工作的一个核心功能类。

 

下面我们来看一下Context的继承结构:

%title插图%num

Context的继承结构还是稍微有点复杂的,可以看到,直系子类有两个,一个是ContextWrapper,一个是ContextImpl。那么从名字上就可以看出,ContextWrapper是上下文功能的封装类,而ContextImpl则是上下文功能的实现类。而ContextWrapper又有三个直接的子类,ContextThemeWrapper、Service和Application。其中,ContextThemeWrapper是一个带主题的封装类,而它有一个直接子类就是Activity。

 

那么在这里我们至少看到了几个所比较熟悉的面孔,Activity、Service、还有Application。由此,其实我们就已经可以得出结论了,Context一共有三种类型,分别是Application、Activity和Service。这三个类虽然分别各种承担着不同的作用,但它们都属于Context的一种,而它们具体Context的功能则是由ContextImpl类去实现的。

 

那么Context到底可以实现哪些功能呢?这个就实在是太多了,弹出Toast、启动Activity、启动Service、发送广播、操作数据库等等等等都需要用到Context。由于Context的具体能力是由ContextImpl类去实现的,因此在*大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。

 

Context数量

那么一个应用程序中到底有多少个Context呢?其实根据上面的Context类型我们就已经可以得出答案了。Context一共有Application、Activity和Service三种类型,因此一个应用程序中Context数量的计算公式就可以这样写:

[plain] view plain copy
  1. Context数量 = Activity数量 + Service数量 + 1

上面的1代表着Application的数量,因为一个应用程序中可以有多个Activity和多个Service,但是只能有一个Application。

 

Application Context的设计

基本上每一个应用程序都会有一个自己的Application,并让它继承自系统的Application类,然后在自己的Application类中去封装一些通用的操作。其实这并不是Google所推荐的一种做法,因为这样我们只是把Application当成了一个通用工具类来使用的,而实际上使用一个简单的单例类也可以实现同样的功能。但是根据我的观察,有太多的项目都是这样使用Application的。当然这种做法也并没有什么副作用,只是说明还是有不少人对于Application理解的还有些欠缺。那么这里我们先来对Application的设计进行分析,讲一些大家所不知道的细节,然后再看一下平时使用Application的问题。

 

首先新建一个MyApplication并让它继承自Application,然后在AndroidManifest.xml文件中对MyApplication进行指定,如下所示:

[html] view plain copy
  1. <application  
  2.     android:name=“.MyApplication”  
  3.     android:allowBackup=“true”  
  4.     android:icon=“@drawable/ic_launcher”  
  5.     android:label=“@string/app_name”  
  6.     android:theme=“@style/AppTheme” >  
  7.     ……
  8. </application>  

指定完成后,当我们的程序启动时Android系统就会创建一个MyApplication的实例,如果这里不指定的话就会默认创建一个Application的实例。

 

前面提到过,现在很多的Application都是被当作通用工具类来使用的,那么既然作为一个通用工具类,我们要怎样才能获取到它的实例呢?如下所示:

[java] view plain copy
  1. public class MainActivity extends Activity {  
  2.     @Override  
  3.     protected void onCreate(Bundle savedInstanceState) {  
  4.         super.onCreate(savedInstanceState);  
  5.         setContentView(R.layout.activity_main);
  6.         MyApplication myApp = (MyApplication) getApplication();
  7.         Log.d(“TAG”, “getApplication is ” + myApp);  
  8.     }
  9. }

可以看到,代码很简单,只需要调用getApplication()方法就能拿到我们自定义的Application的实例了,打印结果如下所示:

 

%title插图%num

那么除了getApplication()方法,其实还有一个getApplicationContext()方法,这两个方法看上去好像有点关联,那么它们的区别是什么呢?我们将代码修改一下:

[java] view plain copy
  1. public class MainActivity extends Activity {  
  2.     @Override  
  3.     protected void onCreate(Bundle savedInstanceState) {  
  4.         super.onCreate(savedInstanceState);  
  5.         setContentView(R.layout.activity_main);
  6.         MyApplication myApp = (MyApplication) getApplication();
  7.         Log.d(“TAG”, “getApplication is ” + myApp);  
  8.         Context appContext = getApplicationContext();
  9.         Log.d(“TAG”, “getApplicationContext is ” + appContext);  
  10.     }
  11. }

同样,我们把getApplicationContext()的结果打印了出来,现在重新运行代码,结果如下图所示:

 

%title插图%num

 

咦?好像打印出的结果是一样的呀,连后面的内存地址都是相同的,看来它们是同一个对象。其实这个结果也很好理解,因为前面已经说过了,Application本身就是一个Context,所以这里获取getApplicationContext()得到的结果就是MyApplication本身的实例。

 

那么有的朋友可能就会问了,既然这两个方法得到的结果都是相同的,那么Android为什么要提供两个功能重复的方法呢?实际上这两个方法在作用域上有比较大的区别。getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在*大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法了,如下所示:

[java] view plain copy
  1. public class MyReceiver extends BroadcastReceiver {  
  2.     @Override  
  3.     public void onReceive(Context context, Intent intent) {  
  4.         MyApplication myApp = (MyApplication) context.getApplicationContext();
  5.         Log.d(“TAG”, “myApp is ” + myApp);  
  6.     }
  7. }

也就是说,getApplicationContext()方法的作用域会更广一些,任何一个Context的实例,只要调用getApplicationContext()方法都可以拿到我们的Application对象。

那么更加细心的朋友会发现,除了这两个方法之外,其实还有一个getBaseContext()方法,这个baseContext又是什么东西呢?我们还是通过打印的方式来验证一下:

 

%title插图%num

 

哦?这次得到的是不同的对象了,getBaseContext()方法得到的是一个ContextImpl对象。这个ContextImpl是不是感觉有点似曾相识?回去看一下Context的继承结构图吧,ContextImpl正是上下文功能的实现类。也就是说像Application、Activity这样的类其实并不会去具体实现Context的功能,而仅仅是做了一层接口封装而已,Context的具体功能都是由ContextImpl类去完成的。那么这样的设计到底是怎么实现的呢?我们还是来看一下源码吧。因为Application、Activity、Service都是直接或间接继承自ContextWrapper的,我们就直接看ContextWrapper的源码,如下所示:

[java] view plain copy
  1. /** 
  2.  * Proxying implementation of Context that simply delegates all of its calls to 
  3.  * another Context.  Can be subclassed to modify behavior without changing 
  4.  * the original Context. 
  5.  */  
  6. public class ContextWrapper extends Context {  
  7.     Context mBase;
  8.     /** 
  9.      * Set the base context for this ContextWrapper.  All calls will then be 
  10.      * delegated to the base context.  Throws 
  11.      * IllegalStateException if a base context has already been set. 
  12.      *  
  13.      * @param base The new base context for this wrapper. 
  14.      */  
  15.     protected void attachBaseContext(Context base) {  
  16.         if (mBase != null) {  
  17.             throw new IllegalStateException(“Base context already set”);  
  18.         }
  19.         mBase = base;
  20.     }
  21.     /** 
  22.      * @return the base context as set by the constructor or setBaseContext 
  23.      */  
  24.     public Context getBaseContext() {  
  25.         return mBase;  
  26.     }
  27.     @Override  
  28.     public AssetManager getAssets() {  
  29.         return mBase.getAssets();  
  30.     }
  31.     @Override  
  32.     public Resources getResources() {  
  33.         return mBase.getResources();  
  34.     }
  35.     @Override  
  36.     public ContentResolver getContentResolver() {  
  37.         return mBase.getContentResolver();  
  38.     }
  39.     @Override  
  40.     public Looper getMainLooper() {  
  41.         return mBase.getMainLooper();  
  42.     }
  43.     @Override  
  44.     public Context getApplicationContext() {  
  45.         return mBase.getApplicationContext();  
  46.     }
  47.     @Override  
  48.     public String getPackageName() {  
  49.         return mBase.getPackageName();  
  50.     }
  51.     @Override  
  52.     public void startActivity(Intent intent) {  
  53.         mBase.startActivity(intent);
  54.     }
  55.     @Override  
  56.     public void sendBroadcast(Intent intent) {  
  57.         mBase.sendBroadcast(intent);
  58.     }
  59.     @Override  
  60.     public Intent registerReceiver(  
  61.         BroadcastReceiver receiver, IntentFilter filter) {
  62.         return mBase.registerReceiver(receiver, filter);  
  63.     }
  64.     @Override  
  65.     public void unregisterReceiver(BroadcastReceiver receiver) {  
  66.         mBase.unregisterReceiver(receiver);
  67.     }
  68.     @Override  
  69.     public ComponentName startService(Intent service) {  
  70.         return mBase.startService(service);  
  71.     }
  72.     @Override  
  73.     public boolean stopService(Intent name) {  
  74.         return mBase.stopService(name);  
  75.     }
  76.     @Override  
  77.     public boolean bindService(Intent service, ServiceConnection conn,  
  78.             int flags) {  
  79.         return mBase.bindService(service, conn, flags);  
  80.     }
  81.     @Override  
  82.     public void unbindService(ServiceConnection conn) {  
  83.         mBase.unbindService(conn);
  84.     }
  85.     @Override  
  86.     public Object getSystemService(String name) {  
  87.         return mBase.getSystemService(name);  
  88.     }
  89.     ……
  90. }

由于ContextWrapper中的方法还是非常多的,我就进行了一些筛选,只贴出来了部分方法。那么上面的这些方法相信大家都是非常熟悉的,getResources()、getPackageName()、getSystemService()等等都是我们经常要用到的方法。那么所有这些方法的实现又是什么样的呢?其实所有ContextWrapper中方法的实现都非常统一,就是调用了mBase对象中对应当前方法名的方法。

 

那么这个mBase对象又是什么呢?我们来看第16行的attachBaseContext()方法,这个方法中传入了一个base参数,并把这个参数赋值给了mBase对象。而attachBaseContext()方法其实是由系统来调用的,它会把ContextImpl对象作为参数传递到attachBaseContext()方法当中,从而赋值给mBase对象,之后ContextWrapper中的所有方法其实都是通过这种委托的机制交由ContextImpl去具体实现的,所以说ContextImpl是上下文功能的实现类是非常准确的。

 

那么另外再看一下我们刚刚打印的getBaseContext()方法,在第26行。这个方法只有一行代码,就是返回了mBase对象而已,而mBase对象其实就是ContextImpl对象,因此刚才的打印结果也得到了印证。

 

使用Application的问题

虽说Application的用法确实非常简单,但是我们平时的开发工作当中也着实存在着不少Application误用的场景,那么今天就来看一看有哪些比较容易犯错的地方是我们应该注意的。

 

Application是Context的其中一种类型,那么是否就意味着,只要是Application的实例,就能随时使用Context的各种方法呢?我们来做个实验试一下就知道了:

[java] view plain copy
  1. public class MyApplication extends Application {  
  2.     public MyApplication() {  
  3.         String packageName = getPackageName();
  4.         Log.d(“TAG”, “package name is ” + packageName);  
  5.     }
  6. }

这是一个非常简单的自定义Application,我们在MyApplication的构造方法当中获取了当前应用程序的包名,并打印出来。获取包名使用了getPackageName()方法,这个方法就是由Context提供的。那么上面的代码能正常运行吗?跑一下就知道了,你将会看到如下所示的结果:

 

%title插图%num

 

应用程序一启动就立刻崩溃了,报的是一个空指针异常。看起来好像挺简单的一段代码,怎么就会成空指针了呢?但是如果你尝试把代码改成下面的写法,就会发现一切正常了:

[java] view plain copy
  1. public class MyApplication extends Application {  
  2.     @Override  
  3.     public void onCreate() {  
  4.         super.onCreate();  
  5.         String packageName = getPackageName();
  6.         Log.d(“TAG”, “package name is ” + packageName);  
  7.     }
  8. }

运行结果如下所示:

 

%title插图%num

 

在构造方法中调用Context的方法就会崩溃,在onCreate()方法中调用Context的方法就一切正常,那么这两个方法之间到底发生了什么事情呢?我们重新回顾一下ContextWrapper类的源码,ContextWrapper中有一个attachBaseContext()方法,这个方法会将传入的一个Context参数赋值给mBase对象,之后mBase对象就有值了。而我们又知道,所有Context的方法都是调用这个mBase对象的同名方法,那么也就是说如果在mBase对象还没赋值的情况下就去调用Context中的任何一个方法时,就会出现空指针异常,上面的代码就是这种情况。Application中方法的执行顺序如下图所示:

 

%title插图%num

 

Application中在onCreate()方法里去初始化各种全局的变量数据是一种比较推荐的做法,但是如果你想把初始化的时间点提前到*致,也可以去重写attachBaseContext()方法,如下所示:

[java] view plain copy
  1. public class MyApplication extends Application {  
  2.     @Override  
  3.     protected void attachBaseContext(Context base) {  
  4.         // 在这里调用Context的方法会崩溃  
  5.         super.attachBaseContext(base);  
  6.         // 在这里可以正常调用Context的方法  
  7.     }
  8. }

以上是我们平时在使用Application时需要注意的一个点,下面再来介绍另外一种非常普遍的Application误用情况。

 

其实Android官方并不太推荐我们使用自定义的Application,基本上只有需要做一些全局初始化的时候可能才需要用到自定义Application,官方文档描述如下:

%title插图%num

但是就我的观察而言,现在自定义Application的使用情况基本上可以达到100%了,也就是我们平时自己写测试demo的时候可能不会使用,正式的项目几乎全部都会使用自定义Application。可是使用归使用,有不少项目对自定义Application的用法并不到位,正如官方文档中所表述的一样,多数项目只是把自定义Application当成了一个通用工具类,而这个功能并不需要借助Application来实现,使用单例可能是一种更加标准的方式。

 

不过自定义Application也并没有什么副作用,它和单例模式二选一都可以实现同样的功能,但是我见过有一些项目,会把自定义Application和单例模式混合到一起使用,这就让人大跌眼镜了。一个非常典型的例子如下所示:

[java] view plain copy
  1. public class MyApplication extends Application {  
  2.     private static MyApplication app;  
  3.     public static MyApplication getInstance() {  
  4.         if (app == null) {  
  5.             app = new MyApplication();  
  6.         }
  7.         return app;  
  8.     }
  9. }

就像单例模式一样,这里提供了一个getInstance()方法,用于获取MyApplication的实例,有了这个实例之后,就可以调用MyApplication中的各种工具方法了。

 

但是这种写法对吗?这种写法是大错特错!因为我们知道Application是属于系统组件,系统组件的实例是要由系统来去创建的,如果这里我们自己去new一个MyApplication的实例,它就只是一个普通的Java对象而已,而不具备任何Context的能力。有很多人向我反馈使用 LitePal 时发生了空指针错误其实都是由于这个原因,因为你提供给LitePal的只是一个普通的Java对象,它无法通过这个对象来进行Context操作。

 

那么如果真的想要提供一个获取MyApplication实例的方法,比较标准的写法又是什么样的呢?其实这里我们只需谨记一点,Application全局只有一个,它本身就已经是单例了,无需再用单例模式去为它做多重实例保护了,代码如下所示:

[java] view plain copy
  1. public class MyApplication extends Application {  
  2.     private static MyApplication app;  
  3.     public static MyApplication getInstance() {  
  4.         return app;  
  5.     }
  6.     @Override  
  7.     public void onCreate() {  
  8.         super.onCreate();  
  9.         app = this;  
  10.     }
  11. }

getInstance()方法可以照常提供,但是里面不要做任何逻辑判断,直接返回app对象就可以了,而app对象又是什么呢?在onCreate()方法中我们将app对象赋值成this,this就是当前Application的实例,那么app也就是当前Application的实例了。

 

4、Context的应用场景

%title插图%num

 

 

start activity :    需要提供一个task  除了activity其他的context都没有task,     如果在intent 的flag 添加NEW_TASK  属性创建一个新的task,其他context也是可以start activity的

show dialog  :    dialog需要依附一个窗口 只有activity自带一个窗口,如果设置dialog为system.alert  使dialog依附系统窗口 ,其他context也是可以show dialog的

layout inflate :   在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,因为只有activity 是继承的ContextThemeWrapper带有theme。

其他context可以通过ContextThemeWrapper mContextThemeWrapper = new ContextThemeWrapper(context, theme);

创建一个带theme的context对象ContextThemeWrapper使用,这样其他context也可创建带theme的layout

 

 

大家注意看到有一些NO上添加了一些数字,其实这些从能力上来说是YES,但是为什么说是NO呢?下面一个一个解释:

数字1:启动Activity在这些类中是可以的,但是需要创建一个新的task。一般情况不推荐。

数字2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。

数字3:在receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视)

注:ContentProvider、BroadcastReceiver之所以在上述表格中,是因为在其内部方法中都有一个context用于使用。

 

 

好了,关于Context的介绍就到这里吧,内容还是比较简单易懂的,希望大家通过这篇文章可以理解Context更多的细节,并且不要去犯使用Context时的一些低级错误。

关于Android Context 你必须知道的一切

1、Context概念

其实一直想写一篇关于Context的文章,但是又怕技术不如而误人子弟,于是参考了些资料,今天准备整理下写出来,如有不足,请指出,参考资料会在醒目地方标明。

Context,相信不管是*天开发Android,还是开发Android的各种老鸟,对于Context的使用一定不陌生~~你在加载资源、启动一个新的Activity、获取系统服务、获取内部文件(夹)路径、创建View操作时等都需要Context的参与,可见Context的常见性。大家可能会问到底什么是Context,Context字面意思上下文,或者叫做场景,也就是用户与操作系统操作的一个过程,比如你打电话,场景包括电话程序对应的界面,以及隐藏在背后的数据;

但是在程序的角度Context又是什么呢?在程序的角度,我们可以有比较权威的答案,Context是个抽象类,我们可以直接通过看其类结构来说明答案:

%title插图%num

可以看到Activity、Service、Application都是Context的子类;

也就是说,Android系统的角度来理解:Context是一个场景,代表与操作系统的交互的一种过程。从程序的角度上来理解:Context是个抽象类,而Activity、Service、Application等都是该类的一个实现。

在仔细看一下上图:Activity、Service、Application都是继承自ContextWrapper,而ContextWrapper内部会包含一个base context,由这个base context去实现了*大多数的方法。

先扯这么多,有能力了会从别的角度去审视Context,加油~

 

2、Context与ApplicationContext

看了标题,千万不要被误解,ApplicationContext并没有这个类,其实更应该叫做:Activity与Application在作为Context时的区别。嗯,的确是这样的,大家在需要Context的时候,如果是在Activity中,大多直接传个this,当在匿名内部类的时候,因为this不能用,需要写XXXActivity.this,很多哥们会偷懒,直接就来个getApplicationContext。那么大家有没有想过,XXXActivity.this和getApplicationContext的区别呢?

XXXActivity和getApplicationContext返回的肯定不是一个对象,一个是当前Activity的实例,一个是项目的Application的实例。既然区别这么明显,那么各自的使用场景肯定不同,乱使用可能会带来一些问题。

下面开始介绍在使用Context时,需要注意的问题。

 

3、引用的保持

大家在编写一些类时,例如工具类,可能会编写成单例的方式,这些工具类大多需要去访问资源,也就说需要Context的参与。

在这样的情况下,就需要注意Context的引用问题。

例如以下的写法:

[java] view plain copy
  1. package com.mooc.shader.roundimageview;  
  2. import android.content.Context;  
  3. public class CustomManager  
  4. {
  5.     private static CustomManager sInstance;  
  6.     private Context mContext;  
  7.     private CustomManager(Context context)  
  8.     {
  9.         this.mContext = context;  
  10.     }
  11.     public static synchronized CustomManager getInstance(Context context)  
  12.     {
  13.         if (sInstance == null)  
  14.         {
  15.             sInstance = new CustomManager(context);  
  16.         }
  17.         return sInstance;  
  18.     }
  19.     //some methods   
  20.     private void someOtherMethodNeedContext()  
  21.     {
  22.     }
  23. }

对于上述的单例,大家应该都不陌生(请别计较getInstance的效率问题),内部保持了一个Context的引用;

这么写是没有问题的,问题在于,这个Context哪来的我们不能确定,很大的可能性,你在某个Activity里面为了方便,直接传了个this;这样问题就来了,我们的这个类中的sInstance是一个static且强引用的,在其内部引用了一个Activity作为Context,也就是说,我们的这个Activity只要我们的项目活着,就没有办法进行内存回收。而我们的Activity的生命周期肯定没这么长,所以造成了内存泄漏。

那么,我们如何才能避免这样的问题呢?

有人会说,我们可以软引用,嗯,软引用,假如被回收了,你不怕NullPointException么。

把上述代码做下修改:

[java] view plain copy
  1. public static synchronized CustomManager getInstance(Context context)  
  2.     {
  3.         if (sInstance == null)  
  4.         {
  5.             sInstance = new CustomManager(context.getApplicationContext());  
  6.         }
  7.         return sInstance;  
  8.     }

这样,我们就解决了内存泄漏的问题,因为我们引用的是一个ApplicationContext,它的生命周期和我们的单例对象一致。

这样的话,可能有人会说,早说嘛,那我们以后都这么用不就行了,很遗憾的说,不行。上面我们已经说过,Context和Application Context的区别是很大的,也就是说,他们的应用场景(你也可以认为是能力)是不同的,并非所有Activity为Context的场景,Application Context都能搞定。

下面就开始介绍各种Context的应用场景。

 

4、Context的应用场景

%title插图%num

 

大家注意看到有一些NO上添加了一些数字,其实这些从能力上来说是YES,但是为什么说是NO呢?下面一个一个解释:

数字1:启动Activity在这些类中是可以的,但是需要创建一个新的task。一般情况不推荐。

数字2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。

数字3:在receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视)

注:ContentProvider、BroadcastReceiver之所以在上述表格中,是因为在其内部方法中都有一个context用于使用。

 

好了,这里我们看下表格,重点看Activity和Application,可以看到,和UI相关的方法基本都不建议或者不可使用Application,并且,前三个操作基本不可能在Application中出现。实际上,只要把握住一点,凡是跟UI相关的,都应该使用Activity做为Context来处理;其他的一些操作,Service,Activity,Application等实例都可以,当然了,注意Context引用的持有,防止内存泄漏。

 

5、总结

好了,到此,Context的分析基本完成了,希望大家在以后的使用过程中,能够稍微考虑下,这里使用Activity合适吗?会不会造成内存泄漏?这里传入Application work吗?

 

Android属性动画:动画流控制

今天的文章里,我将会和大家讨论对动画流的控制。我们可以通过Animator系列的API来控制动画的开始、停止和取消。在 KitKat也就是API level 19中,我们还可以控制动画的暂停和恢复。在本文中,我将会带你体验整个动画流的控制,并且通过一些函数方法来让你能够观察到动画的状态。

动画流介绍

在之前的教程中,我们已经使用过多次Animator.start这个方法。这个方法是用来让动画从*帧开始播放。该方法只是动画流控制方法集中的一个方法而已,完整的方法集合如下所示:

start这个方法顾名思义是用来让动画从开头开始播放的。如果动画设置了一个大于0的播放延迟(startDelay),那么调用该方法后还需要等到延迟的时间过去才回开始播放。
我们有两种停止动画的方法,你可以用end方法抑或cancel方法来停止一个播放着的动画。在两种方式中动画都会终止并且只有再次调用start方法才会重新开始播放。两者的区别则在于停止后动画所在的状态,当你使用cancel方法来停止动画后,动画只是停止了它的时间轴,动画的状态会停在一个中间态(intermediate state)。如果通过end 方法来停止一个动画,那么动画会直接快进到该动画*后一帧并且停止,所有的对象都会保持在动画*终结束后的状态。
在 Kitkat 中增加的没有怎么被大家关注到的新API则是带来了动画可以暂停和恢复的能力。在那之前,一个动画如果被取消并且停留在当前的中间态,此时你用start方法去重启动画,动画只会从一开始重新播放。现在,你则可以调用pause方法来暂停当前播放中的动画,pause也会有和cancel方法一样的功效让动画停留在中间态,但是当你使用resume 方法去恢复这个动画的时候,动画会从这个状态继续播放下去。
现在,让我们来实践下看看效果。我们新建一个 Activity 并且包含一个 私有的动画对象。并且在 onCreate 回调中初始化这个动画。

我们的动画是一个简单的旋转动画,它将把一个图片完整旋转360度五次。被旋转的图片会在 layout 的 XML 文件中定义好,并且给予一个叫some_image的id。这个layout同时也包含了五个按钮分别是:Start、End、Cancel、Pause以及Resume。这五个按钮分别代表了动画的五个调用方法。

这些方法只是简单的调用了相应的动画控制调用。下面会有两张gif图片来演示效果。上边的动画展示了end方法和cancel方法的区别。可以注意到,cancel让图片保持在停止时的中间态,但是end则让动画到了*后的状态。
下边的动画则演示了pause和resume方法。可以注意到,pause 和cancel都暂停了动画的时间轴。但是现在我们可以用resume 调用去恢复动画的时间轴了。

PropertyAnim_EndCancel

PropertyAnim_PauseRestart

动画状态的查询

有些时候,我们需要去查询当前动画的状态,这个需求可以通过下面这些方法来完成。

如果当前动画已经调用了start函数并且还没播放完成也没有被取消掉,那么isStarted方法会返回true。请注意,isStarted 方法*低的 API 需求是 14.同时,就算是在动画播放延迟中,该方法依然会返回 true。这就是这个方法和 isRunning 方法的不同点,isRunning 方法只会在动画确实在播放并且还没停止的时候返回 true。

在 API 19 的时候,isPaused 方法被加入进来。这是由于那时候动画可以被暂停和恢复了,如果 isPaused 返回了 true,那么说明当前动画是在暂停状态下,反之亦然。

为了演示这些观察方法的效果,我们会通过一个包含三个显示动画状态文本框的例子来解释。这三个文本框都会在Activity中做为TextView成员变量存在。

于此同时,我们在onCreate中添加下列三行代码来把这些文本框从layout中获取到。

我们还创建了一个根据当前动画状态来更新这些文本框的方法。

我们在初始化以后每次修改动画流的时候都去调用setStatusTexts方法,比如说,当我没调用cancelAnimation的时候,代码是这样的:

演示的结果我们可以从下面上方的动画中看到。这里我也创建了两个动画,一个演示End和cancel效果,另一个演示pause和 Resume效果。可以注意到,动画的运行状态在你点击了End和cancel之后是相同的,就算视觉上看两者并不一样。这说明了,你没法通过动画的状态来区分出当前动画是通过 End 调用还是cancel调用来停止的。这两者情况下,isStarted和isRunning都会返回false。

PropertyAnim_EndCancelStatus

PropertyAnim_PauseResumeStatus

而下边的动画则显示了pause和resume调用的效果。当我们通过pause调用去暂停动画时,isPaused会返回true。然后通过 resume调用去恢复动画播放后,isPaused也会变成false。可以注意到,图中如果动画是自然结束的,动画的状态并没有改变。当然,动画停止播放后,isStarted和isRunning肯定应该是返回false的。实际上,在动画自然停止后,如果我们再去调用 isStarted和isRunning他们的的确确会返回false。而动画中的例子我们并没有在动画自然停止后去更新状态,所以我们并不知道当前的状态,自然那几个文本框也就没有更新。为了能够更新到*新的状态,我们可以在动画中添加AnimatorListener和 AnimatorPauseListener这两个监听,这两个监听具体的使用方法会是下一讲的主要内容。

从Android动画到贝塞尔曲线

基础知识:

动画通过连续播放一系列画面,给视觉造成连续变化的图画。很通俗的一种解释。也很好理解。那么我们先来一个案例看看。

动画案例:百度贴吧小熊奔跑

效果:

%title插图%num

topic.gif

代码:

    <?xml version="1.0" encoding="utf-8"?>
    <animation-list
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot="false">
        <item android:drawable="@drawable/gif_loading1" android:duration="50"></item>
        <item android:drawable="@drawable/gif_loading2" android:duration="50"></item>
        <item android:drawable="@drawable/gif_loading3" android:duration="50"></item>
        <item android:drawable="@drawable/gif_loading4" android:duration="50"></item>
        <item android:drawable="@drawable/gif_loading5" android:duration="50"></item>
        <item android:drawable="@drawable/gif_loading6" android:duration="50"></item>
        <item android:drawable="@drawable/gif_loading7" android:duration="50"></item>
    </animation-list>
        AnimationDrawable iv_topicAnimation;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_topic);
            ImageView iv_topic = (ImageView) findViewById(R.id.iv_topic);
            iv_topic.setBackgroundResource(R.drawable.anim_topic);
            iv_topicAnimation = (AnimationDrawable) iv_topic.getBackground();
        }

        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            super.onWindowFocusChanged(hasFocus);
            iv_topicAnimation.start();
        }

        public static void startActiivty(Context context) {
            context.startActivity(new Intent(context, TopicActivity.class));
        }

It’s important to note that the start()
method called on the AnimationDrawable cannot be called during the onCreate()
method of your Activity, because the AnimationDrawable is not yet fully attached to the window. If you want to play the animation immediately, without requiring interaction, then you might want to call it from the onWindowFocusChanged() method in your Activity, which will get called when Android brings your window into focus.

可以看到,实现小熊奔跑的效果是非常简单的。你需要理解的是

1.小熊奔跑的效果是有一张张图片交替播放,而动起来的。
2.动画通过连续播放一系列画面,给视觉造成连续变化的图画。

%title插图%num

Paste_Image.png

好了,前面只是一个热身,下面才是真正的介绍Android动画了。

Android动画:

  • 逐帧动画(frame-by-frame animation)
  • 补间动画(tweened animation)
  • 属性动画(property animation)

对于逐帧动画和补间动画这里就不打算具体深入,但是你必须要知道的

  • 补间动画,只是改变View的显示效果而已,并不会真正的改变View的属性
  • 补间动画,只作用于View上面

写了个demo来解释一下上面的意思:

%title插图%num

tween.gif
     bt_tween.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    TranslateAnimation animation = new TranslateAnimation(0, 0, 0, 300);
                    animation.setDuration(1000);
                    animation.setFillAfter(true);
                    tv_tween_hello.startAnimation(animation);
                }
            });

            tv_tween_hello.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(context,"hello animation",Toast.LENGTH_SHORT).show();
                }
            });

可以看到其实hello Animation平移之后点击没用了,而点击之前的位置的时候,还是有效的。这也正是我刚才说的

补间动画:只是改变View的显示效果而已,并不会真正的改变View的属性

看到这里,你也知道如果要用补间动画来做一些交互的动画是很蛋疼的。一般做法是:

  • 预先在动画结束的地方设置一个不可见的View,然后动画结束之后,让其显示。这样就可以处理事件。
  • 利用https://github.com/JakeWharton/NineOldAndroids
属性动画

在介绍属性动画前,我们还是先来看一个demo。

%title插图%num

property.gif
        protected Person person;
        protected TextView tv_property_info;
        protected Button bt_property;


        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_property);
            person = new Person();
            person.setName("张三");
            bt_property = (Button) findViewById(R.id.bt_property);
            tv_property_info = (TextView) findViewById(R.id.tv_property_info);
            bt_property.setOnClickListener(this);
        }

        @Override
        public void onClick(View v) {
            ObjectAnimator objectAnimator = ObjectAnimator.ofInt(person, "age", 1, 100);
            objectAnimator.addUpdateListener(this);
            objectAnimator.setDuration(5000);
            objectAnimator.start();
        }

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            int values = (int) animation.getAnimatedValue();
            person.setAge(values);
            tv_property_info.setText(person.toString());
        }

上面代码模拟了一个Person对象,从0-100岁之间的变化,可能你有点看不懂。我这是在干嘛,但是如果你还记的刚才我说的。属性动画,可以对任何对象属性进行修改,而补间动画,只作用于View上面。我们的demo就是对Person这个对象中属性age不断进行修改。现在,我们假设一个TextView要进行平移或则缩放,或则旋转。只要将对应的属性进行修改,然后重绘。不就可以达到动画的效果了吗?试试吧!

%title插图%num
    protected TextView tv_property_info;
        protected Button bt_property;
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_property);
            bt_property = (Button) findViewById(R.id.bt_property);
            bt_property.setText("View的改变");
            tv_property_info = (TextView) findViewById(R.id.tv_property_info);
            bt_property.setOnClickListener(this);
        }

        @Override
        public void onClick(View v) {
            ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tv_property_info, "TranslationY", 0, 300);
            objectAnimator.addUpdateListener(this);
            objectAnimator.setDuration(500);
            objectAnimator.start();
        }

我们来总结一下.当我们调用

 ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tv_property_info, "TranslationY", 0, 300);
 objectAnimator.setDuration(500);

的时候,将初始值和结束值给ObjectAnimator,并且告诉它所需的时长,然后它的内部使用一种时间循环的机制来计算值与值之间的过渡。然后将变化的值返回给我们。然后我们获取这个间隙的值,重绘界面就给视觉造成连续变化的图画。那么内部是怎么计算这个值的呢?

TypeEvaluator
    /**
     * This evaluator can be used to perform type interpolation between <code>float</code> values.
     */
    public class FloatEvaluator implements TypeEvaluator<Number> {

        /**
         * This function returns the result of linearly interpolating the start and end values, with
         * <code>fraction</code> representing the proportion between the start and end values. The
         * calculation is a simple parametric calculation: <code>result = x0 + t * (v1 - v0)</code>,
         * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
         * and <code>t</code> is <code>fraction</code>.
         *
         * @param fraction   The fraction from the starting to the ending values
         * @param startValue The start value; should be of type <code>float</code> or
         *                   <code>Float</code>
         * @param endValue   The end value; should be of type <code>float</code> or <code>Float</code>
         * @return A linear interpolation between the start and end values, given the
         *         <code>fraction</code> parameter.
         */
        public Float evaluate(float fraction, Number startValue, Number endValue) {
            float startFloat = startValue.floatValue();
            return startFloat + fraction * (endValue.floatValue() - startFloat);
        }
    }

通过查看源码我们知道,系统内置了一个FloatEvaluator,它通过计算告知动画系统如何从初始值过度到结束值。如果,我们想要自己控制这个时间段的运动轨迹的话,就可以自定义TypeEvaluator来返回轨迹点。下面让我们自己定义一个TypeEvaluator吧!

动画案例:饿了么购物车。

先看效果图片,这样有利于思考.

%title插图%num

想要实现这个效果,我们必须先定义一个类。来记录小球的x和y的变量。

        public class Point implements Parcelable {
            public int x;
            public int y;

            public Point() {}

            public Point(int x, int y) {
                this.x = x;
                this.y = y;
            }

            public Point(Point src) {
                this.x = src.x;
                this.y = src.y;
            }

            /**
             * Set the point's x and y coordinates
             */
            public void set(int x, int y) {
                this.x = x;
                this.y = y;
            }
          }

这个类是android自带的一个类。用来记录小球的运动轨迹点,那么现在我们只需要小球的起始点和终点,然后就可以计算出小球的运动轨迹。那么怎么获取开始点和终点呢?终点的话,是一个定值。而开始点是我们根据点击事件获取的。可以根据

     int position[] = new int[2];
     view.getLocationInWindow(position);

来获取x=position[0];y=position[1]; 现在起始点和终点都已经确定好了,那么我们来计算一下小球运动的轨迹吧。从动画中可以看出来。类似于抛物线。这里我想到了用贝塞尔曲线来做。因为我们只需要确定一个控制点,就可以根据公式算出小球的运动轨迹。

%title插图%num

Paste_Image.png

二次方公式
二次方贝兹曲线的路径由给定点P0、P1、P2的函数B(t)追踪:

%title插图%num

TrueType字型就运用了以贝兹样条组成的二次贝兹曲线。

有了计算公式,现在还缺一个控制点。那么这个点,该怎么确定呢?

    int pointX = (startPosition.x + endPosition.x) / 2;
    int pointY = (int) (startPosition.y - convertDpToPixel(100, mContext));
    Point controllPoint = new Point(pointX, pointY);

我这里确定控制点是,取起始点和终点X的中点,y取开始点网上偏移一个距离。这样就会有一个抛物线的角度了。当然你可以通过http://myst729.github.io/bezier-curve/ 微调下你的控制点。好了,现在我们3个点都已经确定好了,现在就需要计算小球的运动轨迹,然后绘制到Window上去就OK了。

     public class BezierEvaluator implements TypeEvaluator<Point> {

            private Point controllPoint;

            public BezierEvaluator(Point controllPoint) {
                this.controllPoint = controllPoint;
            }

            @Override
            public Point evaluate(float t, Point startValue, Point endValue) {
                int x = (int) ((1 - t) * (1 - t) * startValue.x + 2 * t * (1 - t) * controllPoint.x + t * t * endValue.x);
                int y = (int) ((1 - t) * (1 - t) * startValue.y + 2 * t * (1 - t) * controllPoint.y + t * t * endValue.y);
                return new Point(x, y);
            }
        }

上面就是取当小球在时间t的时候,x和y分别是多少,然后去改变View。

      @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            Point point = (Point) animation.getAnimatedValue();
            setX(point.x);
            setY(point.y);
            invalidate();
        }

记得在动画播放完成后记得移除掉这个小球.

    anim.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    ViewGroup viewGroup = (ViewGroup) getParent();
                    viewGroup.removeView(NXHooldeView.this);
                }
            });

那么现在如果叫你实现这种效果,你有思路吗?

268. 缺失数字(JS实现)

268. 缺失数字(JS实现)
1 题目
给定一个包含 0, 1, 2, …, n 中 n 个数的序列,找出 0 … n 中没有出现在序列中的那个数。
示例 1:
输入: [3,0,1]
输出: 2
示例 2:
输入: [9,6,4,2,3,5,7,0,1]
输出: 8
说明:
你的算法应具有线性时间复杂度。你能否仅使用额外常数空间来实现?
链接:https://leetcode-cn.com/problems/missing-number
2 思路
这道题比较简单,我们可以先遍历整个原数列,得到原数列相加和,然后用等差数列的方法快速计算完整的序列和,两者之差就是缺失的数
3代码
/**
 * @param {number[]} nums
 * @return {number}
 */
var missingNumber = function(nums) {
    let sum = 0;
    let max = 0
    for (let num of nums) {
        max = Math.max(max, num)
        sum += num;
    }
    if (max !== nums.length) return max +1;   //排除缺失的是*大数
    return (0 + max) * (nums.length+1) / 2 – sum;
};

android应用程序中获取view的位置

我们重点在获取view的y坐标,你懂的…

依次介绍以下四个方法:

 

1.getLocationInWindow

 

Java代码  收藏代码
  1. int[] position = new int[2];  
  2. textview.getLocationInWindow(position);
  3. System.out.println(“getLocationInWindow:” + position[0] + “,” + position[1]);  

 

这个方法是将view的左上角坐标存入数组中.此坐标是相对当前activity而言.

若是普通activity,则y坐标为可见的状态栏高度+可见的标题栏高度+view左上角到标题栏底部的距离.

可见的意思是:在隐藏了状态栏/标题栏的情况下,它们的高度以0计算.

若是对话框式的activity,则y坐标为可见的标题栏高度+view到标题栏底部的距离.

此时是无视状态栏的有无的.

 

2.getLocationOnScreen

 

Java代码  收藏代码
  1. int[] position = new int[2];  
  2. textview.getLocationOnScreen(position);
  3. System.out.println(“getLocationOnScreen:” + position[0] + “,” + position[1]);  

这个方法跟上面的差不多,也是将view的左上角坐标存入数组中.但此坐标是相对整个屏幕而言.

 

y坐标为view左上角到屏幕顶部的距离.

 

 

3.getGlobalVisibleRect

 

Java代码  收藏代码
  1. Rect viewRect = new Rect();  
  2. textview.getGlobalVisibleRect(viewRect);
  3. System.out.println(viewRect);

这个方法是构建一个Rect用来”套”这个view.此Rect的坐标是相对当前activity而言.

若是普通activity,则Rect的top为可见的状态栏高度+可见的标题栏高度+Rect左上角到标题栏底部的距离.

若是对话框式的activity,则y坐标为Rect的top为可见的标题栏高度+Rect左上角到标题栏底部的距离.

此时是无视状态栏的有无的.

 

4.getLocalVisibleRect

 

Java代码  收藏代码
  1. Rect globeRect = new Rect();  
  2. button.getLocalVisibleRect(globeRect);

这个方法获得的Rect的top和left都是0,也就是说,仅仅能通过这个Rect得到View的宽度和高度….

 

 

注意:

以上方法在OnCreate方法中调用,都会返回0,这是因为View还未加载完毕.

建议在onWindowFocusChanged方法中进行获取,有些情况下onWindowFocusChanged不好用的时候(比如ActivityGroup),可以这样写:

 

Java代码  收藏代码
  1. mTextView.post(new Runnable() {  
  2.     @Override  
  3.     public void run() {  
  4.         Rect viewRect = new Rect();  
  5.         mTextView.getGlobalVisibleRect(viewRect);
  6.         mTreeScrollView.setRect(viewRect);
  7.     }
  8. });

这样在View加载完毕之后会执行获取位置的方法.

274. H 指数(JS实现)

274. H 指数(JS实现)
1 题目
给定一位研究者论文被引用次数的数组(被引用次数是非负整数)。编写一个方法,计算出研究者的 h 指数。
h 指数的定义:h 代表“高引用次数”(high citations),一名科研人员的 h 指数是指他(她)的 (N 篇论文中)总共有 h 篇论文分别被引用了至少 h 次。(其余的 N – h 篇论文每篇被引用次数 不超过 h 次。)
例如:某人的 h 指数是 20,这表示他已发表的论文中,每篇被引用了至少 20 次的论文总共有 20 篇。
示例:
输入:citations = [3,0,6,1,5]
输出:3
解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 3, 0, 6, 1, 5 次。
由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。
链接:https://leetcode-cn.com/problems/h-index
2 思路
这道题我先将因子进行排序,然后用二分法将目标数和其到数组尾的长度进行对比即可,题解里用计数排序,可以进一步降低时间复杂度:
论文的引用次数可能会非常多,这个数值很可能会超过论文的总数 n,因此使用计数排序是非常不合算的(会超出空间限制)。在这道题中,我们可以通过一个不难发现的结论来让计数排序变得有用,即如果一篇文章的引用次数超过论文的总数 n,那么将它的引用次数降低为 n也不会改变 h指数的值。排序后再使用二分即可得到正确答案
3代码
/**
 * @param {number[]} citations
 * @return {number}
 */
var hIndex = function(citations) {
 if (citations.length === 0) return 0;
  citations.sort((a,b) => a – b);    //先将因子排序
  let low = 0;
  let high = citations.length – 1;
  while(low <= high) {     //使用二分法,将每次的结果与长度进行对比
    let mid = Math.floor((low + high) / 2);
    let len = citations.length – mid;
    if (citations[mid] < len) {
      low = mid + 1;
    } else if (citations[mid] > len) {
      if (mid > 0 && citations[mid-1] <= len || mid === 0) {
        return len;
      } else {
        high = mid – 1;
      }
    } else {
      return len;
    }
  }
  return 0;
};

279. 完全平方数(JS实现)

279. 完全平方数(JS实现)
1 题目
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数*少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.
链接:https://leetcode-cn.com/problems/perfect-squares
2 思路
我们先计算可能的平方数,然后广度优先遍历构建一颗树,当找到叶子节点时,返回当前树的深度即可
3代码
/**
 * @param {number} n
 * @return {number}
 */
var numSquares = function(n) {
  const squares = [];
  for (let i=1; i<=Math.sqrt(n); i++) {
    squares.push(i * i);
  }
  let numSet = new Set();
  numSet.add(n);
  let level = 0;
  while (numSet.size > 0) {
      level++;
      let nextSet = new Set();      //每层的节点
      for (let num of numSet) {
        for (let squareNum of squares) {
            if (num === squareNum) {    //找到叶子节点
                return level;
            } else if (num < squareNum) {
                break;
            } else {
                nextSet.add(num – squareNum);
            }
        }
      }
      numSet = nextSet;
  }
};

283. 移动零(JS实现)

283. 移动零(JS实现)
1 题目
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。
链接:https://leetcode-cn.com/problems/move-zeroes
2 思路
这道题我用双指针的方法,p1指针遍历整个数组,p2指针只记录不是0的数,并将其赋值给p1指针
3代码
/**
 * @param {number[]} nums
 * @return {void} Do not return anything, modify nums in-place instead.
 */
var moveZeroes = function(nums) {
    let p1 = p2 = 0;
    let maxIndex = nums.length – 1;
    while(p1 <= maxIndex) {
        if (nums[p2] === 0 && p2 <= maxIndex) {
            p2++;
            continue;
        }
        nums[p1] = p2 > maxIndex ? 0 : nums[p2];
        p1++;
        p2++;
    }
};

284. 顶端迭代器(JS实现)

284. 顶端迭代器(JS实现)
1 题目
给定一个迭代器类的接口,接口包含两个方法: next() 和 hasNext()。设计并实现一个支持 peek() 操作的顶端迭代器 – 其本质就是把原本应由 next() 方法返回的元素 peek() 出来。
示例:
假设迭代器被初始化为列表 [1,2,3]。
调用 next() 返回 1,得到列表中的*个元素。
现在调用 peek() 返回 2,下一个元素。在此之后调用 next() 仍然返回 2。
*后一次调用 next() 返回 3,末尾元素。在此之后调用 hasNext() 应该返回 false。
进阶:你将如何拓展你的设计?使之变得通用化,从而适应所有的类型,而不只是整数型?
链接:https://leetcode-cn.com/problems/peeking-iterator
2 思路
这道题需要支持查看下一个元素,因此我们就多执行一次next操作,然后把结果暂时存下来,需要时返回即可
3代码
/**
 * // This is the Iterator’s API interface.
 * // You should not implement it, or speculate about its implementation.
 * function Iterator() {
 *    @ return {number}
 *    this.next = function() { // return the next number of the iterator
 *       …
 *    };
 *
 *    @return {boolean}
 *    this.hasNext = function() { // return true if it still has numbers
 *       …
 *    };
 * };
 */
/**
 * @param {Iterator} iterator
 */
var PeekingIterator = function(iterator) {
    this.iterator = iterator;
    this.topNum = null;
};
/**
 * @return {number}
 */
PeekingIterator.prototype.peek = function() {
    if (!this.topNum) this.topNum = this.iterator.next();
    return this.topNum;
};
/**
 * @return {number}
 */
PeekingIterator.prototype.next = function() {
    if (this.topNum) {
        let temp = this.topNum;
        this.topNum = null;
        return temp;
    } else {
        return this.iterator.next();
    }
};
/**
 * @return {boolean}
 */
PeekingIterator.prototype.hasNext = function() {
    if (this.topNum) return true;
    return this.iterator.hasNext();
};
/**
 * Your PeekingIterator object will be instantiated and called as such:
 * var obj = new PeekingIterator(arr)
 * var param_1 = obj.peek()
 * var param_2 = obj.next()
 * var param_3 = obj.hasNext()
 */