关于Android横竖屏切换及Activity重启周期的总结

 更新时间:2016年9月20日 19:55  点击:2088
本文我在网上总结了几篇关于禁止Android横竖屏,解决切换屏幕时重启Activity的方法,解决Android手机 屏幕横竖屏切换,如何让Android横竖屏切换时不销毁当前activity。

禁止Android横竖屏和解决切换屏幕时重启Activity的方法

1.在AndroidManifest.xml的Activity配置中加入 android:screenOrientation=”landscape”属性(landscape是横向,portrait是纵向)。如:

<activity android:name=".ContactsManagerActivity" android:label="@string/app_name" android:screenorientation="portrait">
    <intent-filter>
        <action android:name="android.intent.action.MAIN">
        <category android:name="android.intent.category.LAUNCHER">
    </category></action></intent-filter>
</activity>

        
2.一般横竖屏切换时,activity要重启,为了避免重启,可以为activity 添加android:configChanges=“orientation”属性,然后在activity中复写onConfigurationChanged()方法,例如:

public void onConfigurationChanged(Configuration newConfig) {
    // TODO Auto-generated method stub
    if (newConfig.orientation==Configuration.ORIENTATION_LANDSCAPE) {
        setContentView(R.layout.imageswitch);
        //横屏
    } else {
        setContentView(R.layout.editcontact);//竖屏
    }
    
    super.onConfigurationChanged(newConfig);
}


这样就可以实现不重启activity,实现横竖屏切换了。



解决Android手机 屏幕横竖屏切换

Android中当屏幕横竖屏切换时,Activity的生命周期是重新加载(说明当前的Activity给销毁了,但又重新执行加载),怎么使屏幕横竖屏切换时,当前的Activity不销毁呢?

1. 在AndroidManifest.xml中为Activity设置configChanges属性,

application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".MainActivity"
android:label="@string/app_name" android:configChanges="orientation|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>


configChanges有如下选项:  1. orientation :屏幕在纵向和横向间旋转,  2. keyboardHidden:键盘显示或隐藏 ,3.fontScale:用户变更了首选的字体大小   4.locale : 用户选择了不同的语言设定,5.  keyboard :键盘类型变更,例如手机从12键盘切换到全键盘   6. touchscreen或navigation:键盘或导航方式变化,

如果缺少了keyboardHidden选项 不能防止Activity的销毁,并且在之后提到的onConfigurationChanged事件中 只能捕获竖屏变横屏的事件 不能捕获横屏变竖屏


2. 在对应的Activity中重写:onConfigurationChanged 方法:

public class MainActivity extends Activity {
    private TextView textView;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Log.i("--Main--", "onCreate");
        textView=(TextView)findViewById(R.id.tv_id);
    }
    
    
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        Log.i("--Main--", "onConfigurationChanged");
        if(newConfig.orientation==Configuration.ORIENTATION_LANDSCAPE){
            textView.setText("当前屏幕为横屏");
        }else{
            textView.setText("当前屏幕为竖屏");
        }
    }
    
}



布局文件就是一个简单的TextView此处不给出,

效果如下:

日志打印:


从日志中可以分析出屏幕横竖屏切换时Activity并没有销毁,当然你也可以运行项目在onCreate方法打个断点,执行发现onCreate方法只是在刚开始进入时执行,屏幕横竖屏切换时,已经不会在执行,因此可在onConfigurationChanged方法中下点文章!

注:如果项目不需要屏幕切换时可以设置为

1. android:screenOrientation="portrait" 始终以竖屏显示
2. android:screenOrientation="landscape" 始终以横屏显示

上面的配置文件设置屏幕横竖屏,下面是代码去控制屏幕横竖屏的:

private OnClickListener onClick=new OnClickListener() {
    @Override
    public void onClick(View v) {
        //设置屏幕为横屏
        if(v==butLandscrpe){
            MainActivity.this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        //设置为置屏幕为竖屏
        }else{
            MainActivity.this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
        }
    }
};
//监听系统设置的更改
@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    String message=newConfig.orientation==Configuration.ORIENTATION_LANDSCAPE ? "屏幕设置为:横屏" : "屏幕设置为:竖屏";
    showToast(message);
}




横竖屏切换时候Activity的生命周期

1、新建一个Activity,并把各个生命周期打印出来
2、运行Activity,得到如下信息
onCreate-->onStart-->onResume-->
3、按crtl+f12切换成横屏时
onSaveInstanceState-->onPause-->onStop-->onDestroy-->onCreate-->onStart-->onRestoreInstanceState-->onResume-->
4、再按crtl+f12切换成竖屏时,发现打印了两次相同的log
onSaveInstanceState-->onPause-->onStop-->onDestroy-->onCreate-->onStart-->onRestoreInstanceState-->onResume-->onSaveInstanceState-->onPause-->onStop-->onDestroy-->onCreate-->onStart-->onRestoreInstanceState-->onResume-->
5、修改AndroidManifest.xml,把该Activity添加 android:configChanges="orientation",执行步骤3
onSaveInstanceState-->onPause-->onStop-->onDestroy-->onCreate-->onStart-->onRestoreInstanceState-->onResume-->
6、再执行步骤4,发现不会再打印相同信息,但多打印了一行onConfigChanged
onSaveInstanceState-->onPause-->onStop-->onDestroy-->onCreate-->onStart-->onRestoreInstanceState-->onResume-->onConfigurationChanged-->
7、把步骤5的android:configChanges="orientation" 改成 android:configChanges="orientation|keyboardHidden",执行步骤3,就只打印onConfigChanged
onConfigurationChanged-->
8、执行步骤4
onConfigurationChanged-->onConfigurationChanged-->
 总结:
1、不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次
2、设置Activity的android:configChanges="orientation"时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次
3、设置Activity的android:configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法
 

总结一下整个Activity的生命周期
补充一点,当前Activity产生事件弹出Toast和AlertDialog的时候Activity的生命周期不会有改变
Activity运行时按下HOME键(跟被完全覆盖是一样的):onSaveInstanceState --> onPause --> onStop      onRestart -->onStart--->onResume
Activity未被完全覆盖只是失去焦点:onPause--->onResume


如何让Android横竖屏切换时不销毁当前activity

我们都知道android 屏幕横向或竖向相互转换的时候会销毁当前的Activity 然后重启它。
为了横竖屏切换时不销毁当前Activity 我来介绍两种方法。

方法一:不允许横竖屏切换:

实现方法: Activity中还有一属性和屏幕方向有关:

android:screenOrientation=["unspecified" | "user" | "behind" | "landscape" | "portrait" | "sensor" | "nosensor"]
在Mainifest.xml的Activity元素中增加这么一个属性:
android:screenOrientation=”portrait”则无论手机如何变动,拥有这个属性的activity都将是竖屏显示。
android:screenOrientation=”landscape”,为横屏显示。


//--------------------------------------------------------------------------------------------

方法二:

首先在Mainifest.xml的Activity元素中加入android:configChanges=”orientation|keyboardHidden”属性:

加入这条属性的含义是,应用程序将会处理屏幕方向和键盘状态(推出或合上)信息的改动。

但对于其他的设备配置信息的改动则会由Android系统来处理(销毁当前Activity,然后重启一个新的Activity实例)

然后 在java代码的activity子类中加入配置信息改动的处理代码:

 

//-----------------------------------------------------------------------------------------
@Override public void onConfigurationChanged(Configuration newConfig)
{
super.onConfigurationChanged(newConfig);
// 检测屏幕的方向:纵向或横向
if (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE)
{
//当前为横屏, 在此处添加额外的处理代码
} else if (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT)
{
//当前为竖屏, 在此处添加额外的处理代码
}
//检测实体键盘的状态:推出或者合上
if (newConfig.hardKeyboardHidden ) == Configuration.HARDKEYBOARDHIDDEN_NO)
{
//实体键盘处于推出状态,在此处添加额外的处理代码
} else if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES)
{
//实体键盘处于合上状态,在此处添加额外的处理代码
}
}


findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。

Android中FindViewById()是一个非常常用的函数,位于android.app.Activity包中。该函数利用我们在XML文件中定义的View的id属性来获取相应的View对象。findViewById()属于API Level 1, 对应的android版本是android1.0, 由此,可以看出,该函数是android早期版本中就有的。顺便说一下, android目前市场上已商用的版本及其对应的API Level如下:

android 1.0API Level 1

android 1.1API Level 2

android 1.5API Level 3

android 1.6API Level 4

android 2.0API Level 5

android 2.0.1 API Level 6

android 2.1API Level 7

android 2.2API Level 8

1、参数错误:findViewById的参数是一个View的ID,如果在XML文件中没定义相应的ID,则程序会

善意的告诉你:XXX cannot be resulved。此时,补充定义就可以了。

2、未指定调用布局:findViewById()的调用与具体的布局有关,默认的是main.xml中的布局,函数前

 没有布局指示。不过,当我们在main.xml描述的布局中,添加其它布局时,利用该函数获

 取所添加布局中的View,则需在调用时,添加布局名,形式如下:

 addLayout.findViewById(), 如果不这么做,程序编译时有时不会报错,但运行时会

 提示遇到异常,并强制关闭应用。


3、命名冲突:这个错误可能不是很常见,不过,要是没有遇到过,猛的来这么一下,还真让人 有点懵。

 呵呵,本人就犯过这样的错误。解释下,这里的命名冲突是指当前工程中定义的类与

 android在Framework中提供的名字相同,这样的话,当前工程文件中,会优先使用本工

 程中的定义。当然,使用findViewById()函数时,发生这种错误必须满足以下几个条件:

 一是:需要在当前工程中利用ID(xml中定义)来查找对应的View对象;

 二是:查找的View类名恰好与本工程中已有的类定义相同

 三是:同名的两个类实例化后产生的对象类型不同,如:一个是View, 一个是Activity。

问题现象:

这样的代码熟悉吗?一个控件比较多的页面一直重复写这样的代码有没有很麻烦?

解决方法:


自定义一个方法:

public <T> T $(int viewID) {
 return (T) findViewById(viewID);
}

然后不管是什么类型的View,直接一个$方法搞定:


Android 反射简化findViewById .

官方例子里的小玩意。。。。。

一个注解:InjectView

 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Use this annotation to mark the fields of your Activity as being injectable.
 * <p>
 * See the {@link Injector} class for more details of how this operates.
 */
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {
    /**
     * The resource id of the View to find and inject.
     */
    public int value();
}

一个通过反射注解 并 实例化的功能类:


import java.lang.annotation.Annotation;
import java.lang.reflect.Field;

import android.app.Activity;

/**
 * Very lightweight form of injection, inspired by RoboGuice, for injecting common ui elements.
 * <p>
 * Usage is very simple. In your Activity, define some fields as follows:
 *
 * <pre class="code">
 * @InjectView(R.id.fetch_button)
 * private Button mFetchButton;
 * @InjectView(R.id.submit_button)
 * private Button mSubmitButton;
 * @InjectView(R.id.main_view)
 * private TextView mTextView;
 * </pre>
 * <p>
 * Then, inside your Activity's onCreate() method, perform the injection like this:
 *
 * <pre class="code">
 * setContentView(R.layout.main_layout);
 * Injector.get(this).inject();
 * </pre>
 * <p>
 * See the {@link #inject()} method for full details of how it works. Note that the fields are
 * fetched and assigned at the time you call {@link #inject()}, consequently you should not do this
 * until after you've called the setContentView() method.
 */
public final class Injector {
    private final Activity mActivity;

    private Injector(Activity activity) {
        mActivity = activity;
    }

    /**
     * Gets an {@link Injector} capable of injecting fields for the given Activity.
     */
    public static Injector get(Activity activity) {
        return new Injector(activity);
    }

    /**
     * Injects all fields that are marked with the {@link InjectView} annotation.
     * <p>
     * For each field marked with the InjectView annotation, a call to
     * {@link Activity#findViewById(int)} will be made, passing in the resource id stored in the
     * value() method of the InjectView annotation as the int parameter, and the result of this call
     * will be assigned to the field.
     *
     * @throws IllegalStateException if injection fails, common causes being that you have used an
     *             invalid id value, or you haven't called setContentView() on your Activity.
     */
    public void inject() {
        for (Field field : mActivity.getClass().getDeclaredFields()) {
            for (Annotation annotation : field.getAnnotations()) {
                if (annotation.annotationType().equals(InjectView.class)) {
                    try {
                        Class<?> fieldType = field.getType();
                        int idValue = InjectView.class.cast(annotation).value();
                        field.setAccessible(true);
                        Object injectedValue = fieldType.cast(mActivity.findViewById(idValue));
                        if (injectedValue == null) {
                            throw new IllegalStateException("findViewById(" + idValue
                                    + ") gave null for " +
                                    field + ", can't inject");
                        }
                        field.set(mActivity, injectedValue);
                        field.setAccessible(false);
                    } catch (IllegalAccessException e) {
                        throw new IllegalStateException(e);
                    }
                }
            }
        }
    }
}

使用时类似:

 01.@InjectView(R.id.gygallery) 
02.private Gallery gallery; 
03.@InjectView(R.id.is_switcher) 
04.private ImageSwitcher imageSwitcher; 
 @InjectView(R.id.gygallery)
 private Gallery gallery;
 @InjectView(R.id.is_switcher)
 private ImageSwitcher imageSwitcher;

Activity>onCreate(){


Injector.get(this).inject();//init views

}

现在大部分人开发Android应用是在Android Studio上进行,不过以前网上很多的开源库,控件等还是以前的基于Eclipse进行开发,很多人不知道怎么导入到自己的基于Android Studio项目中来本文详细介绍一下。

本文我们将详细介绍基于Eclipse的开源库如何导入Android Studio项目中。

前言

    --project //项目目录
    |
    build.gradle //项目的gradle配置文件
    |
    settings.gradle //gradle设置,会保存所有的module
    |
    app //module 目录
    |__build.gradle module的配置
    |
    module2 //module2目录
    |__build.gradle module的配置

同eclipse中的项目一样,gradle/android studio 构建也可以有module,将moudle放到项目目录下面,然后在settings.gradle中增加该module,最简单的方法是用文件夹名。比 如我们上面的结构,build.gradle文件应该如下:

    include ':app', ':module2'

更多关于gralde的知识可以看我以前的文章:

使用gradle构建android项目(续)

使用Gradle构建Android项目

导入Jar文件

这种可能很常见,可以下载到别人搞好的jar包,这样可以直接在自己的主module下创建libs文件夹(我这里这样,只是为了兼容eclipse方式),然后把jar文件放进去,然后在module的build.gradle文件中的dependecies{}添加如下代码:

    compile files('libs/name.jar')

当libs文件夹下面有多个文件时,可以用一句代码包含这些包:

    compile fileTree(dir: 'libs', include: ['*.jar'])

当有文件不需要被包含时,可以这样:

    compile fileTree(dir: 'libs', exclude: ['android-support*.jar'], include: ['*.jar'])

从上面的代码中可以看到我们可以使用通配符, +表示一个字符,*表示0到多个字符。

导入maven中的库

如果开源库作者有将代码放到Maven库中,我们可以在gradle配置中直接引入,类似如下:

    compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.1'

一般我们可以在开源库的github页面上面看有没有这样一个地址,或者到maven库中根据包名搜索有没有,我们前面这个引入的项目分三个部分 group:name:version,我们引入其他的包也有遵守这个规则。

导入gradle构建的开源库

这种情况的比较少用到,因为这张的开源库,作者一般都有放到maven库中,但是偶尔也会用到这里也提一下。

首先下载文件,将我们需要的这个库的module文件夹拷贝到我们的项目的目录下面,然后在setting.gradle文件中增加文件夹名称, 然后在我们需要依赖这个模块的module中的build.gradle文件中加入如下代码:

    compile project(':libmodule')

这样就可以了。

导入基于Eclipse构建的开源库

基于Eclipse构建的项目,和基于Android Studio构建的项目的很大区别是目录结构不同。

我们首先将module文件夹拷 贝到我们的项目目录下面,然后在settings.gradle文件中增加这个module,然后在要使用的module中的build.gradle文 件中引入依赖,这样看的话,似乎和引入基于gradle构建的没什么不同。但是,基于Eclipse构建的项目中,没有build.gradle文件,所 以我们需要自己新建一个放到module下面,下面是一个模版:

    apply plugin: 'android-library'
    repositories {
    mavenCentral()
    }
    android {
    compileSdkVersion 19
    buildToolsVersion "20.0.0"
    defaultConfig {
    minSdkVersion 9
    targetSdkVersion 19
    }
    sourceSets {
    main {
    manifest.srcFile 'AndroidManifest.xml'
    java.srcDirs = ['src']
    resources.srcDirs = ['src']
    aidl.srcDirs = ['src']
    res.srcDirs = ['res']
    assets.srcDirs = ['assets']
    jniLibs.srcDirs = ['libs']
    }
    }
    lintOptions {
    abortOnError false
    }
    }
    dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    }

当然,根据各自的sdk和buildtools版本等等,以及其他,配置会有变化,可以看我之前的文章。

其他

以上就是主要的集中导入场景,自己可以根据自己的实际情况然后改变配置等等。

另外,我们导入的仓库可能不是maven中心仓库,或者可能是我们自己搭建的仓库,我们可以自定义仓库地址的,修改build.gradle文件中的repositories就可以了,例如:

    buildscript {
    repositories {
    jcenter()
    mavenCentral()
    maven {
    url "https://oss.sonatype.org/content/repositories/snapshots"
    }
    }
    }

另外,project层的buildscript在module层也是会生效的,所以不用在每个module都配置。




Android Studio如何导入jar,so,以及第三方的开源库?

问:在Github上的许多活跃项目已经开始采用Android Studio编写了,看来这场趋势已经无法阻挡。所以我也开始从Eclipse阵营转入Studio。刚开始玩Studio各种不适应,希望各位大大能给个详细的教程。。。

回答一:

一般用eclipse生成的项目,导入android studio都会有各种问题,不过要解决也不难,简单几步设置就可以了,说几个比较常用的步骤:

1.导入主项目之后,需要在File -> project strucure的modules选项设置好各种路径,比如你的src、libs、gen、bin、AndroidManifest.xml的路径等等

2.在eclipse里的project,在as上就变成module了,所以要用import module的方式引入库项目,之后可能也要重复步骤1给库项目设置好路径

3.jar文件还是放在libs文件夹,然后需要在project strucure里的libraries选项引入进来

4.库项目和各种jar都导进来了,但是主项目还是报错,这可能是因为主项目和这些库文件的依赖关系没关联好,还是project strucure的主项目里面,选择dependencis,把需要用到的module和library都添加进来

所以关键还是在对project strucure的设置 :)

另一种用Guide构建的项目,好处什么的就不细说了,网上一搜一大把,教程可以看android官网的使用指南,有兴趣可以了解下


回答二:

一般 Android 项目从 Eclipse 导入至 Android Studio(以下简称AS) 后,会出现各种问题,最主要的就是 “gradle” 目前为止还不支持 .so 库文件打包入 apk 。
也就是说,如果你用的第三方库中包含 .so 文件 (一般是 libs\armeabi\ xxx.so ) , 那么只要在代码中出现 “System.loadLibrary( "xxx" ) ; 等代码出现,一定会在这里抛出异常 UnsatisfiedLinkError 。

解决方案如下: 根据我 Google 了2天的结果,一个比较满意且所有出现这个问题的人都可行的方案是:
1) 在硬盘任意位置建立空文件夹 "lib" (注意名字是 "lib" 而不是 "libs" )
2) 把原来的 libs 目录下的 armeabi 文件夹拷贝至 刚建立的 lib 目录下. (armeabi文件夹中的.so文件也会被一并拷入)
3) 将刚建立的 "lib" 目录打包压缩成 .zip 文件, 同时改名为 “armeabi.jar"
4) 将 armeabi.jar 放入原来的 libs 目录下 (和其他第三方jar包放在一起)

经过这4步之后,只需要再重新更新下AS的第三方库,就OK了 .

Volley是Google I/O大会上推出了一个新的网络通信框架,它把AsyncHttpClient和Universal-Image-Loader的优点集于了一身。本文我们重点讲讲Volley的基本用法及使用实例。

1. 什么是Volley

我们平时在开发Android应用的时候不可避免地都需要用到网络技术,而多数情况下应用程序都会使用HTTP协议来发送和接收网络数据。Android 系统中主要提供了两种方式来进行HTTP通信,HttpURLConnection和HttpClient,几乎在任何项目的代码中我们都能看到这两个类的身影,使用率非常高。

不过HttpURLConnection和HttpClient的用法还是稍微有些复杂的,如果不进行适当封装的话,很容易就会写出不少重复代码。于是乎,一些Android网络通信框架也就应运而生,比如说AsyncHttpClient,它把HTTP所有的通信细节全部封装在了内部,我们只需要简单调用几行代码就可以完成通信操作了。再比如Universal-Image-Loader,它使得在界面上显示网络图片的操作变得极度简单,开发者不用关心如何从网络上获取图片,也不用关心开启线程、回收图片资源等细节,Universal-Image-Loader已经把一切都做好了。

Android开发团队也是意识到了有必要将HTTP的通信操作再进行简单化,于是在2013年Google I/O大会上推出了一个新的网络通信框架——Volley。Volley可是说是把AsyncHttpClient和Universal-Image- Loader的优点集于了一身,既可以像AsyncHttpClient一样非常简单地进行HTTP通信,也可以像Universal-Image- Loader一样轻松加载网络上的图片。除了简单易用之外,Volley在性能方面也进行了大幅度的调整,它的设计目标就是非常适合去进行数据量不大,但通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,Volley的表现就会非常糟糕。

下图所示的这些应用都是属于数据量不大,但网络通信频繁的,因此非常适合使用Volley。


2. 下载Volley

介绍了这么多理论的东西,下面我们就准备开始进行实战了,首先需要将Volley的jar包准备好,如果你的电脑上装有Git,可以使用如下命令下载Volley的源码:

git clone https://android.googlesource.com/platform/frameworks/volley

下载完成后将它导入到你的Eclipse工程里,然后再导出一个jar包就可以了。如果你的电脑上没有Git,那么也可以直接使用我导出好的jar包,下载地址是:http://www.kwstu.com/ResourcesView/kwstu_201441183330928 。

新建一个Android项目,将volley.jar文件复制到libs目录下,这样准备工作就算是做好了。

3. StringRequest的用法

前面已经说过,Volley的用法非常简单,那么我们就从最基本的HTTP通信开始学习吧,即发起一条HTTP请求,然后接收HTTP响应。首先需要获取到一个RequestQueue对象,可以调用如下方法获取到:

RequestQueue mQueue = Volley.newRequestQueue(context);

注意这里拿到的RequestQueue是一个请求队列对象,它可以缓存所有的HTTP请求,然后按照一定的算法并发地发出这些请求。 RequestQueue内部的设计就是非常合适高并发的,因此我们不必为每一次HTTP请求都创建一个RequestQueue对象,这是非常浪费资源的,基本上在每一个需要和网络交互的Activity中创建一个RequestQueue对象就足够了。

接下来为了要发出一条HTTP请求,我们还需要创建一个StringRequest对象,如下所示:

StringRequest stringRequest = new StringRequest("http://www.baidu.com",
                        new Response.Listener<String>() {
                            @Override
                            public void onResponse(String response) {
                                Log.d("TAG", response);
                            }
                        }, new Response.ErrorListener() {
                            @Override
                            public void onErrorResponse(VolleyError error) {
                                Log.e("TAG", error.getMessage(), error);
                            }
                        });


可以看到,这里new出了一个StringRequest对象,StringRequest的构造函数需要传入三个参数,第一个参数就是目标服务器的URL 地址,第二个参数是服务器响应成功的回调,第三个参数是服务器响应失败的回调。其中,目标服务器地址我们填写的是百度的首页,然后在响应成功的回调里打印出服务器返回的内容,在响应失败的回调里打印出失败的详细信息。

最后,将这个StringRequest对象添加到RequestQueue里面就可以了,如下所示:

mQueue.add(stringRequest);

另外,由于Volley是要访问网络的,因此不要忘记在你的AndroidManifest.xml中添加如下权限:

好了,就是这么简单,如果你现在运行一下程序,并发出这样一条HTTP请求,就会看到LogCat中会打印出如下图所示的数据。


没错,百度返回给我们的就是这样一长串的HTML代码,虽然我们看起来会有些吃力,但是浏览器却可以轻松地对这段HTML代码进行解析,然后将百度的首页展现出来。

这样的话,一个最基本的HTTP发送与响应的功能就完成了。你会发现根本还没写几行代码就轻易实现了这个功能,主要就是进行了以下三步操作:

1. 创建一个RequestQueue对象。

2. 创建一个StringRequest对象。

3. 将StringRequest对象添加到RequestQueue里面。

不过大家都知道,HTTP的请求类型通常有两种,GET和POST,刚才我们使用的明显是一个GET请求,那么如果想要发出一条POST请求应该怎么做呢?StringRequest中还提供了另外一种四个参数的构造函数,其中第一个参数就是指定请求类型的,我们可以使用如下方式进行指定:

StringRequest stringRequest = new StringRequest(Method.POST, url, listener, errorListener);

可是这只是指定了HTTP请求方式是POST,那么我们要提交给服务器的参数又该怎么设置呢?很遗憾,StringRequest中并没有提供设置POST 参数的方法,但是当发出POST请求的时候,Volley会尝试调用StringRequest的父类——Request中的getParams()方法来获取POST参数,那么解决方法自然也就有了,我们只需要在StringRequest的匿名类中重写getParams()方法,在这里设置POST 参数就可以了,代码如下所示:

StringRequest stringRequest = new StringRequest(Method.POST, url,  listener, errorListener) {
    @Override
    protected Map<String, String> getParams() throws AuthFailureError {
        Map<String, String> map = new HashMap<String, String>();
        map.put("params1", "value1");
        map.put("params2", "value2");
        return map;
    }
};

你可能会说,每次都这样用起来岂不是很累?连个设置POST参数的方法都没有。但是不要忘记,Volley是开源的,只要你愿意,你可以自由地在里面添加和修改任何的方法,轻松就能定制出一个属于你自己的Volley版本。

4. JsonRequest的用法

学完了最基本的StringRequest的用法,我们再来进阶学习一下JsonRequest的用法。类似于 StringRequest,JsonRequest也是继承自Request类的,不过由于JsonRequest是一个抽象类,因此我们无法直接创建它的实例,那么只能从它的子类入手了。JsonRequest有两个直接的子类,JsonObjectRequest和 JsonArrayRequest,从名字上你应该能就看出它们的区别了吧?一个是用于请求一段JSON数据的,一个是用于请求一段JSON数组的。

至于它们的用法也基本上没有什么特殊之处,先new出一个JsonObjectRequest对象,如下所示:

JsonObjectRequest jsonObjectRequest = new JsonObjectRequest("http://m.weather.com.cn/data/101010100.html", null,
        new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                Log.d("TAG", response.toString());
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Log.e("TAG", error.getMessage(), error);
            }
        });

可以看到,这里我们填写的URL地址是http://m.weather.com.cn/data/101010100.html,这是中国天气网提供的一个查询天气信息的接口,响应的数据就是以JSON格式返回的,然后我们在onResponse()方法中将返回的数据打印出来。

最后再将这个JsonObjectRequest对象添加到RequestQueue里就可以了,如下所示:

mQueue.add(jsonObjectRequest);

这样当HTTP通信完成之后,服务器响应的天气信息就会回调到onResponse()方法中,并打印出来。现在运行一下程序,发出这样一条HTTP请求,就会看到LogCat中会打印出如下图所示的数据。


由此可以看出,服务器返回给我们的数据确实是JSON格式的,并且onResponse()方法中携带的参数也正是一个JSONObject对象,之后只需要从JSONObject对象取出我们想要得到的那部分数据就可以了。

你应该发现了吧,JsonObjectRequest的用法和StringRequest的用法基本上是完全一样的,Volley的易用之处也在这里体现出来了,会了一种就可以让你举一反三,因此关于JsonArrayRequest的用法相信已经不需要我再去讲解了吧。


Android Volley网络框架使用


在Android中,网络请求无非就这两种:HttpURLConnection和HttpClient( Apache),我们在使用时一般都会对它们进行一系列的封装,但是这过程不免有些繁琐,所以,Google官方也考虑到了这点,在2013年Google I/O大会上就推出了一个新的网络请求框架——Volley,它将各种网络请求都简单化,并且把AsyncHttpClient和Universal-Image-Loader两大框架的优点集一身,Volley用在数据量不大的网络请求操作时它的性能表现的非常出色,但是Volley如果在进行数据量大的网络操作时(下载文件等),那么Volley将表现的比较糟糕。

Volley有这么几大功能:

    1、普通数据、JSON、图片的异步加载
    2、网络请求优先级处理
    3、自带硬盘缓存(普通数据、图片、JSON),另外我们在加载图片时候通过ImageLoader还可加入LruCache
    4、取消请求
    5、与Activity生命周期联动(Activity退出时同时取消所有的请求)

可见,Volley框架是非常强大的,下面我就一一介绍怎么使用Volley框架。

    Volley框架的原理:它内部是通过一个请求队列(RequestQueue)来维护所有请求,我们新创建一个请求(request)后通过RequestQueue.add()方法将请求添加置请求队列中,然后调用RequestQueue.start()方法执行请求队列中的方法

Volley中包含这么几种类型的请求:

    StringRequest - 返回字符串数据
    JsonObjectRequest - 返回JSONArray数据
    JsonArrayRequest - 返回JSONObject数据
    ImageRequest - 返回Bitmap类型数据

当然使用前我们必须导入Volley.jar包(可以去网上下载),或者通过git下载

git clone https://android.googlesource.com/platform/frameworks/volley

这里给出我上传的jar包下载地址:Volley.jar
创建RequestQueue请求队列

RequestQueue是通过Volley的静态方法newRequestQueue来创建的:

RequestQueue mRequestQueue = Volley.newRequestQueue(getApplicationContext());

一般我们会继承自Application在自定义的MyApplication中创建一个全局的请求队列,用来维护app中的网络请求。


StringRequest

这里主要讲最常用的GET和POST请求方式:
这里我用聚合网上查询手机号码归属地的数据为例子,我们创建一个StringRequest请求,然后给该请求设置一个Tag,用来标记这个请求,取消请求时候我们可以通过这个Tag来取消某个或者所有请求,再把该请求加入请求队列,最后执行请求队列中的请求。

StringRequest的构造方法为:

/**
* @method 请求方式(GET、POST等)
* @url 请求url
* @listener 请求成功回调的接口
* @errorListener 请求失败回调的接口
*/
public StringRequest(int method, String url, Listener<String> listener, ErrorListener errorListener)

GET

一个完整的StringRequest的GET请求如下:

String url = "http://apis.juhe.cn/mobile/get?phone=18270837821&key=9a4329bdf84fa69d193ce601c22b949d";
RequestQueue mRequestQueue = Volley.newRequestQueue(getApplicationContext());//创建一个请求队列
StringRequest request = new StringRequest(Request.Method.POST, url, new Response.Listener<String>() {
    @Override
    public void onResponse(String s) {
        Toast.makeText(getApplicationContext(),s,Toast.LENGTH_SHORT).show();
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError volleyError) {
        Toast.makeText(getApplicationContext(),volleyError.toString(),Toast.LENGTH_SHORT).show();
    }
});
request.setTag("zxy");
mRequestQueue.add(request);
mRequestQueue.start();

POST

一个完整的StringRequest的POST请求如下:

String url = "http://apis.juhe.cn/mobile/get";
RequestQueue mRequestQueue = Volley.newRequestQueue(getApplicationContext());
StringRequest request = new StringRequest(Request.Method.POST, url, new Response.Listener<String>() {
    @Override
    public void onResponse(String s) {
        Toast.makeText(getApplicationContext(),s,Toast.LENGTH_SHORT).show();
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError volleyError) {
        Toast.makeText(getApplicationContext(),volleyError.toString(),Toast.LENGTH_SHORT).show();
    }
}){
    @Override
    protected Map<String, String> getParams() throws AuthFailureError {
        Map<String,String> map  =new HashMap<>();
        map.put("phone","18270837821");
        map.put("key","9a4329bdf84fa69d193ce601c22b949d");
        return map;
    }
};
request.setTag("zxy");
mRequestQueue.add(request);
mRequestQueue.start();

其中,因为是以POST方式请求数据的,所以我们必须实现StringRequest的getParams()方法,该方法返回的是Map<String, String>类型的集合,也就是用<key,value>的形式把数据通过POST传入服务器


JsonObjectRequest

JsonObjectRequest构造方法为:

/**
* @method 请求方式(GET、POST等)
* @url 请求url
* @jsonRequest 请求传入的json数据
* @listener 请求成功回调的接口
* @errorListener 请求失败回调的接口
*/
public JsonObjectRequest(int method, String url, JSONObject jsonRequest, Listener<JSONObject> listener, ErrorListener errorListener)

GET

一个完整的JsonObjectRequest的GET请求如下:
因为用的是GET请求方式,参数是在url中传入,所以JSONObject对象传入null

String url = "http://apis.juhe.cn/mobile/get?phone=18270837821&key=9a4329bdf84fa69d193ce601c22b949d";
    RequestQueue mRequestQueue = Volley.newRequestQueue(getApplicationContext());
    JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, url, null, new Response.Listener<JSONObject>() {
        @Override
        public void onResponse(JSONObject jsonObject) {
            Toast.makeText(getApplicationContext(),jsonObject.toString(),Toast.LENGTH_SHORT).show();
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError volleyError) {
            Toast.makeText(getApplicationContext(),volleyError.toString(),Toast.LENGTH_SHORT).show();
        }
    });

    request.setTag("zxy");
    mRequestQueue.add(request);
    mRequestQueue.start();

POST

一个完整的JsonObjectRequest的POST请求如下:

String url = "http://apis.juhe.cn/mobile/get";
    RequestQueue mRequestQueue = Volley.newRequestQueue(getApplicationContext());
    JSONObject jsonObject = new JSONObject();
    try {
        jsonObject.put("phone", "18270837821");
        jsonObject.put("key", "9a4329bdf84fa69d193ce601c22b949d");
    } catch (JSONException e) {
        e.printStackTrace();
    }
    JsonObjectRequest request = new JsonObjectRequest(Request.Method.POST, url, jsonObject, new Response.Listener<JSONObject>() {
        @Override
        public void onResponse(JSONObject jsonObject) {
            Toast.makeText(getApplicationContext(),jsonObject.toString(),Toast.LENGTH_SHORT).show();
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError volleyError) {
            Toast.makeText(getApplicationContext(),volleyError.toString(),Toast.LENGTH_SHORT).show();
        }
    });
    request.setTag("zxy");
    mRequestQueue.add(request);
    mRequestQueue.start();

用以上StringRequest和JSONObjectRequest请求我们都获取到了数据,如图
01.png


虽然这两种方式都可以返回我们请求的数据,但是JSONObjectRequest请求在处理json对象的返回结果时候效率更高,所以确定返回结果是json类型时候可以使用JSONObjectRequest


ImageRequest

ImageRequest的构造方法为:

/**
* @url 请求url
* @listener 请求成功回调的接口
* @maxWidth 图片最大的宽度(如果超过则Volley会对图片进行压缩,如果为0则不压缩)
* @maxHeight 图片最大的高度
* @decodeConfig 图片的配置
* @errorListener 请求失败回调的接口
*/
public ImageRequest(String url, Listener<Bitmap> listener, int maxWidth, int maxHeight, Config decodeConfig, ErrorListener errorListener)

普通的加载方式(ImageView显示)

一个标准的普通请求网络图片的方法:

String url="http://img.my.csdn.net/uploads/201507/21/1437459520_6685.jpg";
    RequestQueue mRequestQueue = Volley.newRequestQueue(getApplicationContext());
    ImageRequest request = new ImageRequest(url, new Response.Listener<Bitmap>() {
        @Override
        public void onResponse(Bitmap bitmap) {
            mImageView.setImageBitmap(bitmap);
        }
    }, 0, 0, Bitmap.Config.ARGB_8888, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError volleyError) {
            Toast.makeText(getApplicationContext(),volleyError.toString(),Toast.LENGTH_SHORT).show();
        }
    });
    request.setTag("zxy");
    mRequestQueue.add(request);
    mRequestQueue.start();
}

这种方式只适合在图片数量不多的情况下使用,否则则需要考虑使用下面两种,同样,因为Volley内部本身就有硬盘缓存机制,在没网的情况下则会加载缓存中的图片


带LruCache缓存的加载方式(ImageView+ImageLoader+ImageCache)

我们上一种方法就是使用Volley中的ImageRequest请求的加载网络图片,这种方法没有LruCache和显示效果不好,下面我们来使用Volley中的ImageLoader+ImageCache的加载方式,这种方式可以在网络差的情况下和加载出错的情况下给出一个默认的提示图片,而且使用了LruCache缓存可以避免OOM。
首先我们创建一个ImageLoader对象,ImageLoader构造方法

public ImageLoader(RequestQueue queue, ImageLoader.ImageCache imageCache)

需要传入请求队列对象和ImageCache对象,我们再来看看ImageCache对象

public interface ImageCache{...}

发现它是ImageLoader内部的一个接口,所以我们得实现这个接口然后传入,于是我们创建一个BimapCache实现ImageCache接口:

public class BitmapCache implements ImageLoader.ImageCache {
    private LruCache<String, Bitmap> mBitmapLruCache;

    public BitmapCache() {
        int maxCache = (int) Runtime.getRuntime().maxMemory();
        int cacheSize = maxCache / 8;
        mBitmapLruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getByteCount();
            }
        };
    }

    @Override
    public Bitmap getBitmap(String key) {
        return mBitmapLruCache.get(key);
    }

    @Override
    public void putBitmap(String key, Bitmap bitmap) {
        mBitmapLruCache.put(key, bitmap);
    }
}

其实这就是创建一个图片内存缓存对象。

之后我们再使用ImageLoader.get()方法加载网络图片,我们来看看get()方法的参数:

/**
* @requestUrl - 请求url
* @listener - ImageLoader.ImageListener的监听对象
* @maxWidth - 图片的最大高度,如果超过则会压缩,为0则不压缩
* @maxHeight
*/
public ImageLoader.ImageContainer get(String requestUrl, ImageLoader.ImageListener listener, int maxWidth, int maxHeight)

第一个参数好办,那么第二个参数我们可以通过ImageLoader.getImageListener()来得到,我们来看看它的参数:

/**
* @view - 表示将图片设置到哪个控件对象上
* @defaultImageResId- 默认时显示的图片的资源id
* @errorImageResId- 加载出错时显示的图片的资源id
*/
public static ImageLoader.ImageListener getImageListener(final ImageView view, final int defaultImageResId, final int errorImageResId)

好了一切都好办了,一个带LruCache缓存的加载图片的标准请求:

String url="http://img.my.csdn.net/uploads/201507/21/1437459520_6685.jpg";
        RequestQueue mRequestQueue = Volley.newRequestQueue(getApplicationContext());
        ImageLoader mImageLoader = new ImageLoader(mRequestQueue,new BitmapCache());
        ImageLoader.ImageListener imageListener = ImageLoader.getImageListener(mImageView, R.mipmap.ic_launcher, R.mipmap.ic_launcher);
        mImageLoader.get(url,imageListener);

效果图:

01.gif
【注意】:mImageLoader.get(url,imageListener)加载图片的原理是什么呢?其实从源码中很容易看出,它首先会得到url对应的key,然后判断硬盘缓存中是否含有该key的图片,如果有则取出,没有则通过网络请求重新加载图片,所以使用这种双缓存的方式加载网络图片,可以有效的防止OOM

使用NetworkImageView+ImageLoader+ImageCache

Volley框架中对图片的请求做的特别好,其中还为我们提供了一个专门用于显示图片的控件:NetworkImageView,该控件继承自ImageView,除了拥有ImageView控件的功能之外,还多了三个方法:

public void setImageUrl(String url, ImageLoader imageLoader) {
        this.mUrl = url;
        this.mImageLoader = imageLoader;
        this.loadImageIfNecessary(false);
    }

public void setDefaultImageResId(int defaultImage) {
        this.mDefaultImageId = defaultImage;
    }

public void setErrorImageResId(int errorImage) {
        this.mErrorImageId = errorImage;
    }

    setDefaultImageResId - 设置该控件默认时显示的图片
    setErrorImageResId - 设置加载网络图片失败时显示的图片
    setImageUrl - 从网络上加载图片

使用NetworkImageView需要在layout中替换掉ImageView:

<com.android.volley.toolbox.NetworkImageView
        android:id="@+id/net_img"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />


同样这里也需要使用ImageLoader和ImageCache,一个标准使用NetworkImageView的例子为:

String url="http://img.my.csdn.net/uploads/201507/21/1437459520_6685.jpg";
RequestQueue mRequestQueue = Volley.newRequestQueue(getApplicationContext());
ImageLoader mImageLoader = new ImageLoader(mRequestQueue,new BitmapCache());
mNetworkImageView.setDefaultImageResId(R.mipmap.ic_launcher);//设置默认显示的图片
mNetworkImageView.setErrorImageResId(R.mipmap.ic_launcher);//设置加载出错时显示的图片
mNetworkImageView.setImageUrl(url,mImageLoader);

效果为:
01.gif

可以看到用NetworkImageView和第二种方式效果是一样的,它同样会先检查硬盘缓存中有没有该图片,如果没有,再通过网络加载得到该图片,如果有则直接设置。既然和第二种一样,那为什么还推出NetworkImageView这个控件呢?答案就是NetworkImageView这个控件在你的Activity退出时候会自动取消网络请求,即完全不需要我们担心网络请求生命周期的问题。

【注意】:在上述两种使用了LruCache缓存加载图片的方法,当图片量较大时推荐使用,否则当你只有几张图片则使用第一种比较好,因为你使用LruCache时需要为它分配一定的内存空间,而图片量不大时候也使用LruCache缓存,那这块空间则一直是作为缓存图片用的,占着这块内存空间,相反反而会得不偿失。

Volley与Activity生命周期联动与取消请求

其实就是在Activity退出时候或销毁时候,取消对应的网络请求,避免网络请求在后台浪费资源,所以,我们一般在onStop()方法中通过之前设置的Tag取消网络请求:

@Override
protected void onStop() {
    super.onStop();
    mRequestQueue.cancelAll("zxy");
}


RequestQueue.cancelAll()方法会过滤出Tag为“zxy”的所有请求都一并取消。
或者通过Request.cancel()取消请求,把当前Activity的请求放入一个List集合中,关闭Activity时分别取消。

本文主要讲的知识点是:Android的View事件传递,android上view touch事件的传递问题,Android的事件传递机制。

Android的View 事件传递

1、基础知识

(1) 所有 Touch 事件都被封装成了 MotionEvent 对象,包括 Touch 的位置、时间、历史记录以及第几个手指(多指触摸)等。

(2) 事件类型分为 ACTION_DOWN, ACTION_UP, ACTION_MOVE, ACTION_POINTER_DOWN, ACTION_POINTER_UP, ACTION_CANCEL,每个事件都是以 ACTION_DOWN 开始 ACTION_UP 结束。

(3) 对事件的处理包括三类,分别为传递——dispatchTouchEvent()函数、拦截——onInterceptTouchEvent()函数、消费——onTouchEvent()函数和 OnTouchListener


2、传递流程

(1) 事件从 Activity.dispatchTouchEvent()开始传递,只要没有被停止或拦截,从最上层的 View(ViewGroup)开始一直往下(子 View)传递。子 View 可以通过 onTouchEvent()对事件进行处理。

(2) 事件由父 View(ViewGroup)传递给子 View,ViewGroup 可以通过 onInterceptTouchEvent()对事件做拦截,停止其往下传递。

(3) 如果事件从上往下传递过程中一直没有被停止,且最底层子 View 没有消费事件,事件会反向往上传递,这时父 View(ViewGroup)可以进行消费,如果还是没有被消费的话,最后会到 Activity 的 onTouchEvent()函数。

(4) 如果 View 没有对 ACTION_DOWN 进行消费,之后的其他事件不会传递过来。

(5) OnTouchListener 优先于 onTouchEvent()对事件进行消费。
上面的消费即表示相应函数返回值为 true。


附上两张原文中流程图:
(1) View 不处理事件流程图

20150811162712052.jpeg


(2) View 处理事件流程图
20150811162712052.jpeg


3、最后说几句

Android Touch事件
假设布局层次为
Layout0
Layout1
Layout2
Layout3

如果谁都没有去interceptTouch,同时谁都没有处理onTouch事件。
那么Layout0->intercept Layout1->intercept Layout2->intercept Layout3->intercept
Layout3->onTouch Layout2->onTouch Layout1->onTouch Layout0->onTouch
由于谁都没有消费ACTION_DOWN事件,后续的MOVE,UP事件将不会传进来。

如果Layout2 intercept了,但是不消费onTouch
那么Layout0->intercept Layout1->intercept Layout2->intercept
Layout2->onTouch Layout1->onTouch Layout0->onTouch
后续事件不会传入

如果Layout2 intercept了,同时消费了。
那么 0->intercept 1->intercept 2->intercept 2->onTouch
0->intercept 1->intercept 2->onTouch
0->intercept 1->intercept 2->onTouch
0->intercept 1->intercept 2->onTouch

如果Layout2 intercept了,不消费,Layout1消费了。
那么0->intercept 1->intercept 2->intercept
2->onTouch 1->onTouch
0->intercept 1->onTouch
0->intercept 1->onTouch
0->intercept 1->onTouch

总结一下。规律就是
如果当前Layout intercept了,那么子View和子ViewGroup都没有机会去获得Touch事件了。如果当前Layout并不消费事件的话,这个事件会一直向上冒泡,直到某个父Layout的onTouchEvent消费了这个事件。如果没有任何一个父Layout消费这个事件,那么后续的事件都不会被接受。
如果在冒泡过程中有某个Layout消费了这个事件。那么这个Layout的所有父Layout的intercept仍然会被调用。但是当前Layout的intercept不会再被调用了。直接调用onTouch事件。

另外,对于底层的View来说,有一种方法可以阻止父层的View截获touch事件,就是调用getParent().requestDisallowInterceptTouchEvent(true);方法。一旦底层View收到touch的action后调用这个方法那么父层View就不会再调用onInterceptTouchEvent了,也无法截获以后的action。在实践过程中发现ListView在滚动的时候会调用这个方法。使得action不能被拦截。


android上view touch事件的传递问题

项目中要实现拖拽功能,即在scrollview里面放的imageview长按后,有影子被拖走的感觉。实现思路基本是这样的:
1、在布局文件里把imageview放到scrollview中
2、为imageview注册touch监听
3、重写scrollview的onTouchEvent函数。
4、创建由一张imgeview生成的popupwindow
5、通过touch的move更新所创建的popupwindow的位置。实现imageview上的图片被拖走的感觉。
开始时,由imageview响应touch事件,但随着手移出imageview,touch事件就不一定会还是由imageview接收了!那touch事件该传给谁呢?imageview上的touch事件是从哪步溜走了呢?经过尝试发现是经过imageview的cancel事件之后,后面的touch事件都交给了其父类操作,这里就是scrollview。touch事件从imageview消失后直接将后续的move事件和up事件交由了scrollview。我们要实现拖走imageview上图片的效果就可以通过创建popupwindow的方式来实现,所创建的popupwindow只有一张图片组成。通过更新popupwindow显示的位置就可以实现拖拽imageview上图片的效果了。


Android的事件传递机制

Android的事件传递机制分为按键事件和触摸事件,而这里的事件指的是touchevent,即触摸事件。

一个touchevent一般是由多个motionevent(有DOWN,UP,MOVE,CANCEL四种)构成,合理的分配这些motionevent到达指定的控件,这些控件才能够接收到相应的touchevent,然后做出处理。关于motionevent请参考我转的另一篇博文。

 
一.相关类和方法

1.与触摸事件有关系的类是view,viewgroup,activity。

1)这里view我们表示的是那些继承自view不能再容纳其他控件的类,比如textview,imageview。其中下面两个方法是三者都有的,且与touchevent相关的。

2)这里的viewgroup表示的是那些继承自viewgroup的类,它们的共同点是可以继续包含view。比如各种layout以及上面说到的恶心的listview。

3)这里的activity表示的就是那些继承自activity的类。

所以下面没有特殊描述,均用一个类代表它们整个群体。
 

2.与触摸事件有关系的方法是dispatchTouchEvent,onTouchEvent以及onInterceptTouchEvent,

public boolean dispatchTouchEvent(MotionEvent event) - 用于事件分发,三个类都有该方法

public boolean onTouchEvent(MotionEvent event) - 用于事件消费,三个类都有该方法

public boolean onInterceptTouchEvent(MotionEvent ev) - 用于拦截事件,只有viewgroup有该方法

这三个方法在三个类中的用途是一样的,但是详细的处理过程却不同。这我们将在下一部分去说明。

 
二.motionevent的dispatchTouchEvent流程

1.Activity部分

对于正常的理解来说,应该是activity拿到某一个motionevent,然后开始事件分发,所以我们来看看activity的dispatchTouchEvent源码

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}



 
1)Activity处理事件

对于一个手机程序来说,最先拿到手势的我觉得应该就是window了,所以首先用superDispatchTouchEvent分配motionevent到activity的内部控件,superDispatchTouchEvent实际做的事情就是FrameLayout.dispatchTouchEvent(处理流程如同下文的ViewGroup.dispatchTouchEvent),它将会去查找有没有view可以处理该事件。如果activity没有内部控件或者内部控件无法处理该motionevent时,superDispatchTouchEvent返回false,然后接收该motionevent的就是activity本身了,我们可以在Activity的onTouchEvent方法里做详细的处理。

2)Activity不处理事件

刚才1)中说到,Activity没有内部控件或者内部控件无法处理该motionevent时superDispatchTouchEvent会返回false,但是如果有内部控件切可以处理该motionevent时,将返回true,这时Activity的dispatchTouchEvent也会返回true告知系统我有一个控件接收了motionevent。

 
2.ViewGroup部分

superDispatchTouchEvent将事件进行分发,首先接到的当然是该Activity的Layout控件,它继承自ViewGroup。当它接收到了之后显然也要先考虑事件的分发。我们来看看ViewGroup的dispatchTouchEvent代码

public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
// if have a target, see if we're allowed to and want to intercept its
// events
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}



1)ViewGroup被当成普通的View

整体代码的意思是说有一个motionevent传递过来了,ViewGroup首先看自己的内部View能不能处理或者说哪个View能够处理,判断的方法是看看该motionevent是不是DOWN,如果是则看看View是否可见,如果可见再看看焦点是不是在View的内部,如果在其内部,那么我们说找到了一个View可能处理该motionevent,将其命名为target(We know we want to dispatch the event down, find a child who can handle it, start with the front-most child.),如果说没有一个View可以处理(We don't have a target, this means we're handling the event as a regular view.),此时ViewGroup将被当做普通的View来处理这个motionevent,那么将调用 super.dispatchTouchEvent(ev)方法,这里就是View的dispatchTouchEvent了。return语句返回的就是super.dispatchTouchEvent(ev)

2)ViewGroup内部有View可以处理

如果说ViewGroup内部有View可以处理,假设为target,那么将调用target.dispatchTouchEvent方法。return语句返回target.dispatchTouchEvent(ev)

3)onInterceptTouchEvent

这里有一个非常特殊的方法,就是onInterceptTouchEvent了,它可以让ViewGroup对motionevent进行拦截,意思就是我们发现某个target可以获得DOWN的焦点,但是ViewGroup不想让它内部的View处理事件,则进行拦截,此时dispatchTouchEvent返回true。


3.View部分

View部分在相对就简单一些了,在上面的target.dispatchTouchEvent之后,motionevent被传递到了View的dispatchTouchEvent中,看到这里应该也就明白了motionevent也是类似的从Activity传递到ViewGroup中的,在superDispatchTouchEvent里Framelayout.dispatchTouchEvent找到了某个View(实际是某个Layout的ViewGroup),调用了它的dispatchTouchEvent。

看View源代码中的dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}



因为View表示没有任何其他内部的控件了,所以它只有两种选择,这里首先将使用我们开发人员定义的OnTouchListener进行处理,如果可以处理,那么将返回true,如果不能处理将使用默认的onTouchEvent来处理。
 
三.motionevent的onTouchEvent流程

最底层的View的dispatchTouchEvent会调用onTouchListener来进行处理motionevent,或者使用onTouchEvent来处理motionevent,不论哪种都默认会返回true。所以这时ViewGroup的dispatchTouchEvent返回值为true,所以Activity的dispatchTouchEvent的返回值是true。

如果我们没定义自己的onTouchListener,并且重写了onTouchEvent,返回一个false,那么ViewGroup的dispatchTouchEvent返回为false,Activity将会调用它的onTouchEvent方法。

 
四.后续的motionevent

如果motionevent为DOWN的时候View没有处理,即在它的dispatchTouchEvent内返回了false,那么该View的容器ViewGroup不会再调用该View的dispatchTouchEvent了,即它将无法接收到后续的MOVE,UP。只有DOWN的时候被View处理了(在dispatchTouchEvent返回true),后续的MOVE,UP才会传递到该View。

 
五.ListView的问题

想给ListView实现左右滑动翻页的功能,正常是想着使用ViewFillper,但是不用ViewFlipper动态改变adapter的内容再刷新ListView的话应该如何实现呢。有下面的想法

1.给ListView定义一个手势对象gestureDector,重写它的onTouchEvent,在里面使用return gestureDector.onTouchEvent。gestureDector的手势监听器默认在onDown的时候返回false,所以一个DOWN的motionevent传过来,onTouchEvent返回false,根据上面的说法,dispatchTouchEvent也将返回false,后续motionevent将不会再传递到ListView,失败。

2.重写手势监听器的onDown返回值为true,这时可以实现左右翻页,但是ListView本身的onItemClickListener将没办法正常工作。失败。

3.重写dispatchTouchEvent,调用super.dispatchTouchEvent,然后始终返回true。重写onTouchEvent,首先调用gestureDector.onTouchEvent,如果返回为false,说明gestureDector.onTouchEvent没有处理该事件,我们的左右滑动也没有触发,那么return super.onTouchEvent处理,包括它的onItemClickListener等等都可以正常运行。不管super.onTouchEvent返回何值,因为dispatchTouchEvent返回了true,所以后续的动作都会传来。如果返回为true,说明gestureDector.onTouchEvent处理了左右滑动事件(前提是在手势监听器里面fling动作返回了true),此时return true。成功。

4.重写dispatchTouchEvent,一开始就调用gestureDector.onTouchEvent,然后同样的处理方式,最终保证能够return true。

[!--infotagslink--]

相关文章

  • Android子控件超出父控件的范围显示出来方法

    下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
  • Android开发中findViewById()函数用法与简化

    findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20
  • Android模拟器上模拟来电和短信配置

    如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
  • 利用JS实现点击按钮后图片自动切换的简单方法

    下面小编就为大家带来一篇利用JS实现点击按钮后图片自动切换的简单方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2016-10-25
  • 夜神android模拟器设置代理的方法

    夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
  • android自定义动态设置Button样式【很常用】

    为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
  • Android WebView加载html5页面实例教程

    如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
  • 深入理解Android中View和ViewGroup

    深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
  • Android自定义WebView网络视频播放控件例子

    下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02
  • js组件SlotMachine实现图片切换效果制作抽奖系统

    这篇文章主要介绍了js组件SlotMachine实现图片切换效果制作抽奖系统的相关资料,需要的朋友可以参考下...2016-04-19
  • Android用MemoryFile文件类读写进行性能优化

    java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。 Android匿名共享内存对外A...2016-09-20
  • Android设置TextView竖着显示实例

    TextView默认是横着显示了,今天我们一起来看看Android设置TextView竖着显示如何来实现吧,今天我们就一起来看看操作细节,具体的如下所示。 在开发Android程序的时候,...2016-10-02
  • android.os.BinderProxy cannot be cast to com解决办法

    本文章来给大家介绍关于android.os.BinderProxy cannot be cast to com解决办法,希望此文章对各位有帮助呀。 Android在绑定服务的时候出现java.lang.ClassCastExc...2016-09-20
  • 详解C#切换窗口

    最近项目不多忙,于是抽点时间巩固下切换窗口问题,感兴趣的朋友跟着小编一起学习吧...2020-06-25
  • Android 实现钉钉自动打卡功能

    这篇文章主要介绍了Android 实现钉钉自动打卡功能的步骤,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下...2021-03-15
  • Android 开发之布局细节对比:RTL模式

    下面我们来看一篇关于Android 开发之布局细节对比:RTL模式 ,希望这篇文章对各位同学会带来帮助,具体的细节如下介绍。 前言 讲真,好久没写博客了,2016都过了一半了,赶紧...2016-10-02
  • Android中使用SDcard进行文件的读取方法

    首先如果要在程序中使用sdcard进行存储,我们必须要在AndroidManifset.xml文件进行下面的权限设置: 在AndroidManifest.xml中加入访问SDCard的权限如下: <!--...2016-09-20
  • Android开发之PhoneGap打包及错误解决办法

    下面来给各位简单的介绍一下关于Android开发之PhoneGap打包及错误解决办法,希望碰到此类问题的同学可进入参考一下哦。 在我安装、配置好PhoneGap项目的所有依赖...2016-09-20
  • 用Intel HAXM给Android模拟器Emulator加速

    Android 模拟器 Emulator 速度真心不给力,, 现在我们来介绍使用 Intel HAXM 技术为 Android 模拟器加速,使模拟器运行度与真机比肩。 周末试玩了一下在Eclipse中使...2016-09-20
  • Android判断当前屏幕是全屏还是非全屏

    在安卓开发时我碰到一个问题就是需要实现全屏,但又需要我们来判断出用户是使用了全屏或非全屏了,下面我分别找了两段代码,大家可参考。 先来看一个android屏幕全屏实...2016-09-20