原文:
zh.annas-archive.org/md5/ceefdd89e585c59c20db6a7760dc11f1
译者:飞龙
第八章:使用触摸屏和传感器
在本章中,我们将涵盖以下主题:
-
监听点击和长按事件
-
识别轻敲和其他常见手势
-
多指触控下的捏合缩放手势
-
滑动刷新
-
列出可用的传感器——安卓传感器框架简介
-
读取传感器数据——使用安卓传感器框架事件
-
读取设备方向
引言
如今,移动设备配备了众多传感器,通常包括陀螺仪、磁力、重力、压力和/或温度传感器,更不用说触摸屏了。这为与用户互动提供了许多新颖而激动人心的选项。通过传感器,您可以确定三维设备位置以及设备本身的使用方式,如摇晃、旋转、倾斜等。即使是触摸屏也提供了许多新的输入方法,从简单的点击到手势和多指触控。
我们将从探索触摸屏交互开始本章内容,先从简单的点击和长按开始,然后使用SimpleOnGestureListener
类来检测常见的手势。接下来,我们将通过ScaleGestureDetector
来探讨多指触控下的捏合缩放手势。
本书旨在为您的应用添加特性和功能提供快速指南。因此,它专注于所需的代码。强烈建议您花些时间阅读设计指南。
提示
请参考谷歌手势设计指南
在本章的后面部分,我们将探讨安卓中的传感器能力,使用安卓传感器框架。我们将演示如何获取所有可用传感器的列表,以及如何检查特定传感器。一旦获取到传感器,我们将演示如何设置一个监听器来读取传感器数据。最后,我们将通过确定设备方向来结束本章的演示。
监听点击和长按事件
几乎每个应用都需要识别并响应基本的点击和长按事件。这是如此基础,在大多数菜谱中,我们使用 XML 的onClick
属性,但对于更高级的监听器,需要通过代码设置。
安卓提供了一个事件监听器接口,当发生某些特定动作时会收到通知,如下所示:
-
onClick()
: 当视图被按下时调用 -
onLongClick()
: 当视图被长按时调用 -
onFocusChange()
: 当用户导航到或离开视图时调用 -
onKey()
: 当硬件按键被按下或释放时调用 -
onTouch()
: 当触摸事件发生时调用
本节将演示如何响应用户的点击事件以及长按事件。
准备就绪
在 Android Studio 中创建一个新项目,命名为PressEvents
。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动。
如何操作…
接收基本视图事件设置非常简单。首先我们将创建一个视图;我们的示例将使用按钮,然后在活动的onCreate()
方法中设置事件监听器。以下是步骤:
-
打开
activity_main.xml
,并用以下Button
替换现有的TextView
:<Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" android:layout_centerVertical="true" android:layout_centerHorizontal="true" />
-
现在打开
MainActivy.java
,并在现有的onCreate()
方法中添加以下代码:Button button = (Button)findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, "Click", Toast.LENGTH_SHORT).show(); } }); button.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { Toast.makeText(MainActivity.this, "Long Press", Toast.LENGTH_SHORT).show(); return true; } });
-
在设备或模拟器上运行应用程序,尝试常规点击和长按。
工作原理…
在本书中使用的大多数示例中,我们使用以下属性在 XML 中设置onClick
监听器:
android:onClick=""
你可能会注意到 XML 的onClick()
方法回调需要与setOnClickListener
的.onClick()
回调具有相同的方法签名:
public void onClick(View v) {}
这是因为当我们使用 XML 的onClick
属性时,Android 会自动为我们设置回调。此示例还演示了我们可以对单个视图设置多个监听器。
需要注意的最后一点是,onLongClick()
方法返回一个布尔值,大多数其他事件监听器也是如此。返回true
表示事件已被处理。
还有更多…
尽管按钮通常用于指示用户应该“按下”的位置,但我们也可以在任何视图上使用setOnClickListener()
和setOnLongClickListener()
,甚至是一个TextView
。
如引言所述,还有其他事件监听器。你可以使用 Android Studio 的自动完成功能。首先输入以下命令:
button.setOn
然后按Ctrl + 空格键查看列表。
识别轻敲和其他常见手势
与前一个食谱中描述的事件监听器不同,手势识别需要两个步骤的过程:
-
收集移动数据
-
分析数据以确定它是否与已知的手势匹配
第一步是当用户触摸屏幕时开始,此时会触发onTouchEvent()
回调,并通过MotionEvent
对象发送移动数据。幸运的是,Android 通过GestureDetector
类使第二步(分析数据)变得更容易,该类可以检测以下手势:
-
onTouchEvent()
-
onDown()
-
onFling()
-
onLongPress()
-
onScroll()
-
onShowPress()
-
onDoubleTap()
-
onDoubleTapEvent()
-
onSingleTapConfirmed()
本食谱将演示使用GestureDetector.SimpleOnGestureListener
来识别触摸和双击手势。
准备工作
在 Android Studio 中创建一个新项目,命名为CommonGestureDetector
。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动。
如何操作…
我们将使用活动本身来检测手势,因此不需要向布局中添加任何视图。打开MainActivity.java
并按照以下步骤操作:
-
添加以下全局变量:
private GestureDetectorCompat mGestureDetector;
-
在
MainActivity
类中添加以下GestureListener
类:private class GestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onSingleTapConfirmed(MotionEvent e) { Toast.makeText(MainActivity.this, "onSingleTapConfirmed", Toast.LENGTH_SHORT).show(); return super.onSingleTapConfirmed(e); } @Override public boolean onDoubleTap(MotionEvent e) { Toast.makeText(MainActivity.this, "onDoubleTap", Toast.LENGTH_SHORT).show(); return super.onDoubleTap(e); } }
-
如下重写
onTouchEvent()
:public boolean onTouchEvent(MotionEvent event) { mGestureDetector.onTouchEvent(event); return super.onTouchEvent(event); }
-
最后,在
onCreate()
中添加以下代码行:mGestureDetector = new GestureDetectorCompat(this, new GestureListener());
-
在设备或模拟器上运行此应用程序。
工作原理…
我们使用了来自支持库的GestureDetectorCompat
,它使得 Android 1.6 及以上版本的设备能够支持手势操作。
如食谱介绍中所述,检测手势是一个两步的过程。为了收集运动或手势数据,我们从触摸事件开始追踪运动。每次调用onTouchEvent()
时,我们将这些数据发送给GestureDetector
。GestureDetector
处理第二步,即分析数据。一旦检测到手势,就会进行适当的回调。我们的示例处理了单点和双击手势。
还有更多…
您的应用程序只需通过重写适当的回调,就可以轻松添加对GestureDetector
检测到的其余手势的支持。
另请参阅
- 有关多指手势,请参阅下一个食谱,使用多指手势进行捏合缩放
使用多指手势进行捏合缩放
之前的食谱使用了SimpleOnGestureListener
来提供简单的一指手势的检测。在本食谱中,我们将使用SimpleOnScaleGestureListener
类来演示常见的捏合缩放多指手势。
下面的截图展示了使用以下食谱创建的应用程序中图标缩小后的效果:
下面的截图展示了图标放大后的效果:
准备工作
在 Android Studio 中创建一个新项目,命名为MultiTouchZoom
。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
如何操作…
为了提供捏合缩放的视觉提示,我们将使用一个带有应用图标的ImageView
。打开activity_main.xml
并按照以下步骤操作:
-
使用以下
ImageView
替换现有的TextView
:<ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="img/ic_launcher" android:layout_centerVertical="true" android:layout_centerHorizontal="true" />
-
现在打开
MainActivity.java
,并向类中添加以下全局变量:private ScaleGestureDetector mScaleGestureDetector; private float mScaleFactor = 1.0f; private ImageView mImageView;
-
如下重写
onTouchEvent()
:public boolean onTouchEvent(MotionEvent motionEvent) { mScaleGestureDetector.onTouchEvent(motionEvent); return true; }
-
在
MainActivity
类中添加以下ScaleListener
类:private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector scaleGestureDetector) { mScaleFactor *= scaleGestureDetector.getScaleFactor(); mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f)); mImageView.setScaleX(mScaleFactor); mImageView.setScaleY(mScaleFactor); return true; } }
-
在现有的
onCreate()
方法中添加以下代码:mImageView=(ImageView)findViewById(R.id.imageView); mScaleGestureDetector = new ScaleGestureDetector(this, new ScaleListener());
-
要实验捏合缩放功能,请在带有触摸屏的设备上运行应用程序。
工作原理…
ScaleGestureDetector
通过分析手势数据并最终通过onScale()
回调报告缩放因子来完成所有工作。我们通过在ScaleGestureDetector
上调用getScaleFactor()
来获取实际的缩放因子。
我们使用一个带有应用图标的ImageView
,通过设置ImageView
的缩放比例来提供视觉上的缩放表示,该比例来自ScaleGestureDetector
返回的缩放因子。为了防止缩放过大或过小,我们添加了以下检查:
mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f));
滑动刷新
通过下拉列表来表示手动刷新的手势称为 Swipe-to-Refresh(下拉刷新)。这是一个如此常见的功能,以至于这个功能已经被封装在一个名为SwipeRefreshLayout
的单个小部件中。
本食谱将展示如何使用小部件为ListView
添加 Swipe-to-Refresh(下拉刷新)功能。以下屏幕截图显示了刷新动作:
准备就绪
在 Android Studio 中创建一个新项目,命名为SwipeToRefresh
。选择默认的Phone & Tablet(手机和平板)选项,并在提示Activity Type(活动类型)时选择Empty Activity(空活动)。
如何操作…
首先,我们需要将SwipeRefreshLayout
小部件和ListView
添加到活动布局中,然后我们将在 java 代码中实现刷新监听器。以下是详细步骤:
-
打开
activity_main.xml
文件,并用以下内容替换现有的<TextView>
:<android.support.v4.widget.SwipeRefreshLayout android:id="@+id/swipeRefresh" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="match_parent" /> </android.support.v4.widget.SwipeRefreshLayout>
-
现在打开
MainActivity.java
文件,并向类中添加以下全局变量:SwipeRefreshLayout mSwipeRefreshLayout; ListView mListView; List mArrayList = new ArrayList<>(); private int mRefreshCount=0;
-
添加以下方法以处理刷新:
private void refreshList() { mRefreshCount++; mArrayList.add("Refresh: " + mRefreshCount); ListAdapter countryAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mArrayList); mListView.setAdapter(countryAdapter); mSwipeRefreshLayout.setRefreshing(false); }
-
在现有的
onCreate()
方法中添加以下代码:mSwipeRefreshLayout = (SwipeRefreshLayout)findViewById(R.id.swipeRefresh); mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { refreshList(); } }); mListView = (ListView)findViewById(android.R.id.list); final String[] countries = new String[]{"China", "France", "Germany", "India", "Russia", "United Kingdom", "United States"}; mArrayList = new ArrayList<String>(Arrays.asList(countries)); ListAdapter countryAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mArrayList); mListView.setAdapter(countryAdapter);
-
在设备或模拟器上运行应用程序。
工作原理…
本食谱的大部分代码是为了在每次调用刷新方法时向ListView
添加项以模拟刷新。实现 Swipe-to-Refresh(下拉刷新)的主要步骤包括:
-
添加
SwipeRefreshLayout
小部件。 -
在
SwipeRefreshLayout
中包含ListView
。 -
添加
OnRefreshListener
以调用你的刷新方法。 -
更新完成后调用
setRefreshing(false)
。
就这样。这个小部件使得添加 Swipe-to-Refresh(下拉刷新)变得非常简单!
还有更多…
尽管 Swipe-to-Refresh(下拉刷新)手势如今是应用程序的常见功能,但仍然建议包含一个菜单项(特别是出于可访问性的考虑)。以下是菜单布局的 XML 代码片段:
<menu >
<item
android:id="@+id/menu_refresh"
android:showAsAction="never"
android:title="@string/menu_refresh"/>
</menu>
在onOptionsItemSelected()
回调中调用你的刷新方法。当从代码中执行刷新,比如从菜单项事件中,你需要通知SwipeRefreshLayout
刷新,以便它可以更新 UI。使用以下代码实现:
SwipeRefreshLayout.setRefreshing(true);
这告诉SwipeRefreshLayout
刷新开始了,所以它可以显示进行中的指示器。
列出可用的传感器——Android 传感器框架简介
Android 通过 Android Sensor Framework 支持硬件传感器。该框架包括以下类和接口:
-
SensorManager
-
Sensor
-
SensorEventListener
-
SensorEvent
大多数 Android 设备包括硬件传感器,但它们在不同的制造商和模型之间有很大差异。如果你的应用程序使用传感器,你有两个选择:
-
在 Android Manifest 中指定传感器
-
在运行时检查传感器
要指定你的应用程序使用传感器,请在 Android Manifest 中包含<uses-feature>
声明。以下是一个要求提供指南针的示例:
<uses-feature android:name="android.hardware.sensor.compass" android:required="true"/>
如果你的应用程序使用指南针,但不需要它来正常工作,你应该设置android:required="false"
,否则应用程序将无法通过 Google Play 获得。
传感器被分为以下三个类别:
-
运动传感器
-
环境传感器
-
位置传感器
Android SDK 支持以下传感器类型:
传感器 | 检测 | 用途 |
---|---|---|
TYPE_ACCELEROMETER | 包括重力在内的运动检测 | 用于确定震动、倾斜等 |
TYPE_AMBIENT_TEMPERATURE | 测量环境室温 | 用于确定本地温度 |
TYPE_GRAVITY | 测量三个轴上的重力 | 用于运动检测 |
TYPE_GYROSCOPE | 测量三个轴上的旋转 | 用于确定转弯、旋转等 |
TYPE_LIGHT | 测量光线水平 | 用于设置屏幕亮度 |
TYPE_LINEAR_ACCELERATION | 排除重力影响的运动检测 | 用于确定加速度 |
TYPE_MAGNETIC_FIELD | 测量地磁场 | 用于创建指南针或确定方位 |
TYPE_PRESSURE | 测量气压 | 用于气压计 |
TYPE_PROXIMITY | 测量物体相对于屏幕的位置 | 用于确定在通话期间设备是否紧贴耳朵 |
TYPE_RELATIVE_HUMIDITY | 测量相对湿度 | 用于确定露点和湿度 |
TYPE_ROTATION_VECTOR | 测量设备方向 | 用于检测运动和旋转 |
还有两个额外的传感器:TYPE_ORIENTATION
和TYPE_TEMPERATURE
,它们已被弃用,因为它们已被新型传感器所取代。
本教程将演示如何获取可用传感器的列表。以下是物理设备上的屏幕截图:
准备工作
在 Android Studio 中创建一个新项目,命名为:ListDeviceSensors
。选择默认的手机 & 平板选项,并在提示活动类型时选择空活动。
如何操作…
首先,我们将查询可用的传感器列表,然后在ListView
中显示结果。以下是详细步骤:
-
打开
activity_main.xml
,并用以下内容替换现有的TextView
:<ListView android:id="@+id/list" android:layout_width="match_parent" android:layout_height="match_parent" />
-
接下来,打开
ActivityMain.java
,并在现有的onCreate()
方法中添加以下代码:ListView listView = (ListView)findViewById(R.id.list); List sensorList = new ArrayList<String>(); List<Sensor> sensors = ((SensorManager) getSystemService(Context.SENSOR_SERVICE)).getSensorList(Sensor.TYPE_ALL); for (Sensor sensor : sensors ) { sensorList.add(sensor.getName()); } ListAdapter sensorAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, sensorList); listView.setAdapter(sensorAdapter);
-
在设备或模拟器上运行程序。
工作原理…
以下代码行负责获取可用传感器的列表;其余代码用于填充ListView
:
List<Sensor> sensors = ((SensorManager) getSystemService(Context.SENSOR_SERVICE)).getSensorList(Sensor.TYPE_ALL);
请注意,我们获得了一个Sensor
对象的列表。我们只获取传感器名称以在ListView
中显示,但还有其他属性可用。有关完整列表,请参阅另请参阅部分提供的链接。
还有更多…
如介绍中的 Nexus 9 屏幕截图所示,一个设备可以有多个相同类型的传感器。如果你在寻找一个特定的传感器,可以传入介绍中表格显示的一个常量。在这种情况下,如果你想查看所有可用的加速度计传感器,可以使用以下调用:
List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER);
如果你不是在寻找传感器列表,而是需要与特定传感器一起工作,可以使用以下代码检查默认传感器:
if (sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null){
//Sensor is available - do something here
}
另请参阅
- Android 开发者传感器网站在
developer.android.com/reference/android/hardware/Sensor.html
读取传感器数据——使用 Android 传感器框架事件
前一个菜谱,列出可用传感器——Android 传感器框架简介,提供了对 Android 传感器框架的介绍。现在,我们将使用SensorEventListener
来读取传感器数据。SensorEventListener
接口只有两个回调:
-
onSensorChanged
() -
onAccuracyChanged
()
当传感器有新数据要报告时,它会使用SensorEvent
对象调用onSensorChanged()
。本示例将演示如何读取光线传感器,但由于所有传感器都使用相同的框架,因此很容易将此示例适配到其他任何传感器。(请参阅前一个菜谱介绍中可用的传感器类型列表。)
准备就绪
在 Android Studio 中创建一个新项目,并将其命名为:ReadingSensorData
。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动。
如何操作…
我们将在活动布局中添加一个TextView
以显示传感器数据,然后我们将SensorEventListener
添加到 java 代码中。我们将使用onResume()
和onPause()
事件来启动和停止我们的事件监听器。要开始,请打开activity_main.xml
并按照以下步骤操作:
-
按照以下方式修改现有的
TextView
:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="0"/>
-
现在,打开
MainActivity.java
并添加以下全局变量声明:private SensorManager mSensorManager; private Sensor mSensor; private TextView mTextView;
-
按照以下方式将
SensorListener
类添加到MainActivity
类:private SensorEventListener mSensorListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { mTextView.setText(String.valueOf(event.values[0])); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { //Nothing to do } };
-
我们将在
onResume()
和onPause()
中如下注册和注销传感器事件:@Override protected void onResume() { super.onResume(); mSensorManager.registerListener(mSensorListener, mSensor, SensorManager.SENSOR_DELAY_NORMAL); } @Override protected void onPause() { super.onPause(); mSensorManager.unregisterListener(mSensorListener); }
-
在
onCreate()
中添加以下代码:mTextView = (TextView)findViewById(R.id.textView); mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);
-
现在,你可以在物理设备上运行应用程序,以查看光传感器的原始数据。
工作原理…
使用 Android 传感器框架从获取传感器开始,这一步我们在onCreate()
方法中完成。在这里,我们调用getDefaultSensor()
,请求TYPE_LIGHT
类型的传感器。我们在onResume()
中注册监听器,在onPause()
中再次注销,以减少电池消耗。在调用registerListener()
时,我们传入我们的mSensorListener
对象。
在我们的案例中,我们只需要传感器数据,这些数据在onSensorChanged()
回调中发送。当传感器变化时,我们使用传感器数据更新TextView
。
还有更多…
现在您已经使用了一个传感器,您就知道如何使用所有传感器了,因为它们都使用相同的框架。当然,您对数据的处理会根据您读取的数据类型而有很大差异。如这里所示,环境传感器返回单一值,但位置和运动传感器也可以返回其他元素,如下所示。
环境传感器
Android 支持以下四种环境传感器:
-
湿度
-
光线
-
压力
-
温度
环境传感器通常更容易处理,因为返回的数据是单一元素,通常不需要校准或过滤。由于大多数设备都包括一个控制屏幕亮度的光线传感器,我们在此演示中使用了光线传感器。
位置传感器
位置传感器包括:
-
地磁场
-
近距离
以下传感器类型使用地磁场:
-
TYPE_GAME_ROTATION_VECTOR
-
TYPE_GEOMAGNETIC_ROTATION_VECTOR
-
TYPE_MAGNETIC_FIELD
-
TYPE_MAGNETIC_FIELD_UNCALIBRATED
除了TYPE_MAGNETIC_FIELD_UNCALIBRATED
发送六个值外,这些传感器在onSensorChanged()
事件中返回三个值。
第三个传感器,方向传感器,已被弃用,现在建议使用getRotation()
和getRotationMatrix()
来计算方向变化。(对于设备方向,例如纵向和横向模式,请参阅下一个菜谱:读取设备方向。)
运动传感器
运动传感器包括以下内容:
-
加速度计
-
陀螺仪
-
重力
-
线性加速度
-
旋转矢量
这些包括以下传感器类型:
-
TYPE_ACCELEROMETE
-
TYPE_GRAVITY
-
TYPE_GYROSCOPE
-
TYPE_GYROSCOPE_UNCALIBRATED
-
TYPE_LINEAR_ACCELERATION
-
TYPE_ROTATION_VECTOR
-
TYPE_SIGNIFICANT_MOTION
-
TYPE_STEP_COUNTER
-
TYPE_STEP_DETECTOR
除了最后三个之外,这些传感器也包括三个数据元素。TYPE_SIGNIFICANT_MOTION
和TYPE_STEP_DETECTOR
表示事件,而TYPE_STEP_COUNTER
返回自上次启动以来(传感器处于活动状态时)的步数。
另请参阅
-
列举可用传感器——介绍 Android 传感器框架的菜谱
-
第九章,图形和动画中的使用传感器数据和 RotateAnimation 创建指南针的菜谱
-
关于设备方向,请参阅读取设备方向的菜谱
-
第十三章,获取位置和使用地理围栏,涵盖了GPS 和位置的菜谱
读取设备方向
尽管 Android 框架会在方向变化时自动加载新资源(如布局),但有时您可能希望禁用此行为。如果您希望被通知方向变化而不是让 Android 自动处理,请在 Android Manifest 中的 Activity 添加以下属性:
android:configChanges="keyboardHidden|orientation|screenSize"
当以下配置更改发生时,系统将通过onConfigurationChanged()
方法通知你,而不是自动处理:
-
keyboardHidden
-
orientation
-
screenSize
onConfigurationChanged()
的签名如下:
onConfigurationChanged (Configuration newConfig)
你可以在newConfig.orientation
中找到新的方向。
提示
禁用自动配置更改(这会导致布局重新加载并重置状态信息)不应用于替代正确保存状态信息。你的应用程序仍然可能在任何时候被中断或完全停止,并被系统杀死。(请参阅第一章中的保存活动状态,了解如何正确保存状态。)
本教程将演示如何确定当前设备方向。
准备就绪
在 Android Studio 中创建一个新项目,并将其命名为:GetDeviceOrientation
。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动。
如何操作…
我们将在布局中添加一个按钮,以便按需检查方向。首先打开activity_main.xml
并按照以下步骤操作:
-
使用以下
Button
替换现有的TextView
:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Check Orientation" android:id="@+id/button" android:layout_centerVertical="true" android:layout_centerHorizontal="true" android:onClick="checkOrientation"/>
-
添加以下方法来处理按钮点击:
public void checkOrientation(View view){ int orientation = getResources().getConfiguration().orientation; switch (orientation) { case Configuration.ORIENTATION_LANDSCAPE: Toast.makeText(MainActivity.this, "ORIENTATION_LANDSCAPE", Toast.LENGTH_SHORT).show(); break; case Configuration.ORIENTATION_PORTRAIT: Toast.makeText(MainActivity.this, "ORIENTATION_PORTRAIT", Toast.LENGTH_SHORT).show(); break; case Configuration.ORIENTATION_UNDEFINED: Toast.makeText(MainActivity.this, "ORIENTATION_UNDEFINED", Toast.LENGTH_SHORT).show(); break; } }
-
在设备或模拟器上运行应用程序。
提示
使用Ctrl + F11旋转模拟器。
工作原理…
要获取当前方向,我们只需调用以下代码行:
getResources().getConfiguration().orientation
方向以int
类型返回,我们可以将其与三个可能值之一进行比较,如示例所示。
还有更多…
获取当前设备旋转
另一个可能需要知道当前方向的情况是处理摄像头数据时——图片和/或视频。通常,图像可能会根据设备方向或补偿当前方向而旋转。在这种情况下,有另一个选项可用于获取旋转:
int rotation = getWindowManager().getDefaultDisplay().getRotation();
在上面的代码行中,rotation
将是以下值之一:
-
Surface.ROTATION_0
-
Surface.ROTATION_90
-
Surface.ROTATION_180
-
Surface.ROTATION_270
提示
旋转值将从其正常方向开始。例如,当使用正常方向为横屏的桌子时,如果在纵向方向拍摄照片,该值将是ROTATION_90
或ROTATION_270
。
另请参阅
-
第一章中的保存活动状态教程
-
请参考配置开发者链接在
developer.android.com/reference/android/content/res/Configuration.html
-
请参考显示开发者链接在
developer.android.com/reference/android/view/Display.html#getRotation()
第九章:图形和动画
在本章中,我们将涵盖以下主题:
-
缩小大图片以避免内存溢出异常
-
过渡动画——定义场景并应用过渡
-
使用传感器数据和 RotateAnimation 创建指南针
-
使用 ViewPager 创建幻灯片展示
-
使用片段创建卡片翻转动画
-
使用自定义过渡创建缩放动画
引言
动画既可以是视觉上吸引人的,也可以是功能性的,简单的按钮按下操作就证明了这一点。按钮按下的图形表示使应用生动起来,同时通过为用户的事件提供视觉反馈,提供了功能性价值。
Android 框架提供了几种动画系统,以便在您的应用程序中更容易地包含动画。它们包括以下内容:
-
视图动画:(原始动画系统。)它通常需要的代码较少,但动画选项有限
-
属性动画:这是一个更灵活的系统,允许对任何对象的任何属性进行动画处理
-
Drawable 动画:它使用 drawable 资源逐帧创建动画(像电影一样)
属性动画系统在 Android 3.0 中引入,通常由于其灵活性而比视图动画更受欢迎。视图动画的主要缺点包括:
-
可以动画化的方面有限——例如缩放和旋转
-
只能对视图的内容进行动画处理——它不能改变视图在屏幕上的绘制位置(因此它不能动画化球在屏幕上的移动)
-
只能对 View 对象进行动画处理
这是一个简单的示例,演示了使用视图动画使视图“闪烁”(模拟按钮按下的简单操作):
Animation blink =AnimationUtils.loadAnimation(this,R.anim.blink);
view.startAnimation(blink);
这是位于res/anim
文件夹中的blink.xml
资源文件的内容:
<?xml version="1.0" encoding="utf-8"?>
<set >
<alpha android:fromAlpha="1.0"
android:toAlpha="0.0"
android:background="#000000"
android:interpolator="@android:anim/linear_interpolator"
android:duration="100"
android:repeatMode="restart"
android:repeatCount="0"/>
</set>
如您所见,创建这种动画非常简单,所以如果视图动画实现了您的目标,请使用它。当它不符合您的需求时,转向属性动画系统。我们将在使用片段创建卡片翻转动画和使用自定义过渡创建缩放动画的菜谱中展示属性动画,使用新的objectAnimator
。
过渡动画——定义场景并应用过渡的菜谱将提供有关 Android Transition Framework 的额外信息,我们将在许多菜谱中使用它。
注意
插值器是定义动画变化速率的函数。
插值器
将在本章的几个菜谱和前面的闪烁示例中提到。插值器定义了过渡是如何计算的。线性插值器会在设定的持续时间内均匀计算变化,而AccelerateInterpolator
函数会在持续时间内创建更快的移动。以下是可用的插值器完整列表,以及 XML 标识符:
-
AccelerateDecelerateInterpolator (@android:anim/accelerate_decelerate_interpolator)
-
AccelerateInterpolator (@android:anim/accelerate_interpolator)
-
AnticipateInterpolator (@android:anim/anticipate_interpolator)
-
AnticipateOvershootInterpolator (@android:anim/anticipate_overshoot_interpolator)
-
BounceInterpolator (@android:anim/bounce_interpolator)
-
CycleInterpolator (@android:anim/cycle_interpolator)
-
DecelerateInterpolator (@android:anim/decelerate_interpolator)
-
LinearInterpolator (@android:anim/linear_interpolator)
-
OvershootInterpolator (@android:anim/overshoot_interpolator)
尽管动画通常不会占用太多内存,但图形资源往往需要。你可能想要处理的许多图片经常超出设备可用内存。在本章的第一个食谱中,我们将会讨论如何避免内存溢出异常,即缩小大图片。
避免内存溢出异常,缩小大图片
处理图片可能非常占用内存,经常导致应用程序因内存溢出异常而崩溃。这在处理设备摄像头拍摄的图片时尤其如此,因为它们的分辨率通常比设备本身要高得多。
由于在这个例子中,加载比 UI 支持的分辨率更高的图片并不会带来任何视觉上的好处,因此这个食谱将演示如何为显示取用图片的小样本。我们将使用BitmapFactory
首先检查图片大小,然后加载缩小后的图片。
这是本食谱的一个截图,展示了一幅非常大图片的缩略图:
准备工作
在 Android Studio 中创建一个新项目,命名为LoadLargeImage
。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
对于这个食谱,我们需要一张大图片;因此我们访问了www.Pixabay.com获取了一张图片。由于图片本身并不重要,我们下载了当时显示的第一张图片。(原始尺寸的图片是 6000 x 4000,3.4MB 大小。)
如何操作…
如在准备工作中所述,我们需要一张大图片来演示缩放。一旦你有了图片,按照以下步骤操作:
-
将图片复制到
res/drawable
目录下,并命名为image_large.jpg
(如果你选择了不同的文件类型,请使用适当的扩展名)。 -
打开
activity_main.xml
文件,将现有的TextView
替换为以下ImageView
:<ImageView android:id="@+id/imageViewThumbnail" android:layout_width="100dp" android:layout_height="100dp" android:layout_centerInParent="true" />
-
现在打开
MainActivity.java
文件,并添加这个方法,我们稍后会解释:public Bitmap loadSampledResource(int imageID, int targetHeight, int targetWidth) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), imageID, options); final int originalHeight = options.outHeight; final int originalWidth = options.outWidth; int inSampleSize = 1; while ((originalHeight / (inSampleSize *2)) > targetHeight && (originalWidth / (inSampleSize *2)) > targetWidth) { inSampleSize *= 2; } options.inSampleSize = inSampleSize; options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(getResources(), imageID, options); }
-
在现有的
onCreate()
方法中添加以下代码:ImageView imageView = (ImageView)findViewById(R.id.imageViewThumbnail); imageView.setImageBitmap(loadSampledResource(R.drawable.image_large, 100, 100));
-
在设备或模拟器上运行应用程序。
工作原理…
loadSampledResource()
方法的目的是加载一张较小的图片,以减少图片的内存消耗。如果我们尝试加载从www.Pixabay.Com选择的完整图片(见前面的准备部分),应用需要超过 3MB 的 RAM 来加载。这对于大多数设备来说(至少目前是这样)是过多的内存,即使能够完全加载,对于我们的缩略图视图也没有视觉上的好处。
为了避免出现内存不足
的情况,我们使用BitmapFactory.Options
的inSampleSize
属性来减少或子采样图像。(如果我们设置inSampleSize=2
,它会将图像减少一半。如果我们使用inSampleSize=4
,它会将图像减少四分之一)要计算inSampleSize
,首先我们需要知道图像的大小。我们可以使用以下方法中的inJustDecodeBounds
属性:
options.inJustDecodeBounds = true;
这告诉BitmapFactory
获取图像尺寸,而实际上不存储图像内容。一旦我们有了图像大小,我们使用以下代码计算采样:
while ((originalHeight / (inSampleSize *2)) > targetHeight && (originalWidth / (inSampleSize *2)) > targetWidth) {
inSampleSize *= 2;
}
这段代码的目的是确定不将图像尺寸降低到目标尺寸以下的最大的采样大小。为此,我们加倍采样大小并检查尺寸是否超过目标尺寸。如果没有,我们保存加倍后的采样大小并重复此过程。一旦缩小后的尺寸低于目标尺寸,我们使用最后保存的inSampleSize
。
注意
从inSampleSize
的文档(在下面的另请参阅部分中的链接)中注意,解码器使用基于 2 的幂的最终值,任何其他值都会被四舍五入到最近的 2 的幂。
一旦我们有了采样大小,我们设置inSampleSize
属性并将inJustDecodeBounds
设置为false
,以正常加载。以下是代码:
options.inSampleSize = inSampleSize;
options.inJustDecodeBounds = false;
需要注意的是,这个方法说明了在您自己的应用程序中应用该任务的概念。加载和处理图像可能是一个长时间的操作,这可能导致您的应用程序停止响应。这不是一件好事,可能导致 Android 显示应用程序无响应(ANR)对话框。建议在后台线程上执行长时间的任务,以保持 UI 线程响应。AsyncTask
类可用于执行后台网络处理,但还有许多其他库可用(在食谱末尾的链接):
-
Volley:在网络上进行快速、可扩展的 UI 操作(见第十二章,电信、网络与互联网)
-
Picasso:一个强大的 Android 图像下载和缓存库
-
Android Universal Image Loader:用于加载、缓存和显示图片的强大而灵活的库
还有更多…
需要注意的是,我们传递给loadSampledResource()
方法的targetHeight
和targetWidth
参数实际上并不设置图片大小。如果你使用我们使用的相同大小的图片运行应用程序,那么样本大小将为 32,导致加载的图片大小为 187 x 125。
如果你的布局需要特定大小的图片,可以在布局文件中设置大小,或者可以直接使用 Bitmap 类修改图片大小。
另请参阅
-
开发者文档: BitmapFactory.inSampleSize() 请访问
developer.android.com/reference/android/graphics/BitmapFactory.Options.html#inSampleSize
-
请参考Picasso的相关内容
-
检查第十四章中的AsyncTask任务,让你的应用准备好上架 Play 商店,以在后台线程上处理长时间运行的操作。
转换动画——定义场景并应用转换
安卓转换框架提供了以下内容:
-
组级别动画: 动画适用于层次结构中的所有视图
-
基于转换的动画: 基于开始和结束属性变化的动画
-
内置动画: 一些常见的过渡效果,如淡入/淡出和移动
-
资源文件支持: 将动画值保存到资源(XML)文件中,以便在运行时加载
-
生命周期回调: 动画期间接收回调通知
转换动画包括以下内容:
-
起始场景: 动画开始时的视图(或
ViewGroup
) -
Transition: 变化类型(稍后介绍)
-
结束场景: 动画结束时的视图(或
ViewGroup
) -
Transitions: 安卓为以下三种转换提供了内置支持:
-
AutoTransition(默认转换): 淡出,移动和调整大小,然后淡入(按此顺序)
-
Fade: 淡入,淡出(默认),或两者都有(指定顺序)
-
ChangeBounds: 移动和调整大小
-
转换框架将自动创建从开始场景到结束场景所需的帧。
以下是在使用以下类时转换框架的一些已知限制:
-
SurfaceView: 由于
SurfaceView
动画是在非 UI 线程上执行的,因此动画可能不会正确显示,因此它们可能与应用程序不同步 -
TextView: 动态文本大小变化可能不会正确工作,导致文本直接跳转到最终状态
-
AdapterView: 扩展了
AdapterView
的类,如ListView
和GridView
可能会挂起 -
TextureView: 某些转换可能不起作用
本教程提供了一个关于使用过渡动画系统的快速指南。我们将从定义场景和过渡资源开始,然后应用过渡,创建动画。以下步骤将指导您使用 XML 创建资源,这是通常推荐的。也可以通过代码创建资源,我们将在更多内容部分讨论这一点。
准备工作
在 Android Studio 中创建一个新项目,命名为:TransitionAnimation
。在目标 Android 设备对话框中,选择手机和平板选项,并为最低 SDK选择 API 19(或更高)。当提示选择活动类型时,选择空活动。
如何操作…
以下是创建资源文件并应用过渡动画的步骤:
-
按照以下方式更改现有的
activity.main.xml
布局文件:<?xml version="1.0" encoding="utf-8"?> <RelativeLayout android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Top" android:id="@+id/textViewTop" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Bottom" android:id="@+id/textViewBottom" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Go" android:id="@+id/button" android:layout_centerInParent="true" android:onClick="goAnimate"/> </RelativeLayout>
-
使用以下 XML 创建一个名为
activity_main_end.xml
的新布局文件:<?xml version="1.0" encoding="utf-8"?> <RelativeLayout android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Bottom" android:id="@+id/textViewBottom" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Top" android:id="@+id/textViewTop" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Go" android:id="@+id/button" android:layout_centerInParent="true"/> </RelativeLayout>
-
新建一个过渡资源目录(文件 | 新建 | Android 资源目录,并选择过渡作为资源类型)。
-
在
res/transition
文件夹中创建一个名为transition_move.xml
的新文件,使用以下 XML:<?xml version="1.0" encoding="utf-8"?> <changeBounds />
-
使用以下代码添加
goAnimate()
方法:public void goAnimate(View view) { ViewGroup root = (ViewGroup) findViewById(R.id.layout); Scene scene = Scene.getSceneForLayout(root, R.layout.activity_main_end, this); Transition transition = TransitionInflater.from(this).inflateTransition(R.transition.transition_move); TransitionManager.go(scene, transition); }
-
您已经准备好在设备或模拟器上运行应用程序。
工作原理…
您可能会发现代码本身相当简单。正如简介中所概述的,我们只需要创建开始和结束场景并设置过渡类型。以下是代码的详细分解。
创建开始场景
运行以下代码行将创建开始场景:
ViewGroup root = (ViewGroup) findViewById(R.id.layout);
创建过渡:
运行以下代码行将创建过渡:
Transition transition = TransitionInflater.from(this).inflateTransition(R.transition.transition_move);
定义结束场景:
运行以下代码行将定义结束场景:
Scene scene = Scene.getSceneForLayout(root, R.layout.activity_main_end, this);
开始过渡:
运行以下代码行将开始过渡:
TransitionManager.go(scene, transition);
尽管简单,但本教程的大部分工作在于创建必要的资源文件。
更多内容…
现在,我们将看看仅使用代码解决方案创建相同的过渡动画(尽管我们仍将使用初始activity_main.xml
布局文件):
ViewGroup root = (ViewGroup) findViewById(R.id.layout);
Scene scene = new Scene(root);
Transition transition = new ChangeBounds();
TransitionManager.beginDelayedTransition(root,transition);
TextView textViewTop = (TextView)findViewById(R.id.textViewTop);
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)textViewTop.getLayoutParams();
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM,1);
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, 0);
textViewTop.setLayoutParams(params);
TextView textViewBottom = (TextView)findViewById(R.id.textViewBottom);
params = (RelativeLayout.LayoutParams)textViewBottom.getLayoutParams();
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM,0);
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, 1);
textViewBottom.setLayoutParams(params);
TransitionManager.go(scene);
我们仍然需要开始和结束场景以及过渡;唯一的区别在于我们如何创建资源。在前面的代码中,我们使用当前布局创建了开始场景。
在我们开始通过代码修改布局之前,我们使用TransitionManager
的beginDelayedTransition()
方法以及过渡类型。TransitionManager
将跟踪结束场景的变化。当我们调用go()
方法时,TransitionManager
会自动对变化进行动画处理。
另请参阅
使用传感器数据和 RotateAnimation 创建罗盘。
在上一章中,我们演示了从物理设备传感器读取传感器数据。在那个教程中,我们使用了光线传感器,因为环境传感器数据通常不需要额外的处理。尽管获取磁感应强度数据很容易,但这些数字本身并没有太多意义,当然也不能创建吸引人的显示效果。
在本教程中,我们将演示如何获取磁感应数据以及加速度计数据来计算磁北。我们将使用SensorManager.getRotationMatrix
来响应设备移动时对指南针进行动画处理。下面是在物理设备上我们指南针应用程序的截图:
准备工作
在 Android Studio 中创建一个新项目,命名为Compass
。使用默认的手机和平板选项,在选择活动类型时选择空活动。
我们需要一个图像作为指南针指示器。同样,我们可以从www.Pixabay.Com获取图像。我们使用了以下这张图片:
pixabay.com/en/geography-map-compass-rose-plot-42608/
尽管不是必须的,但这张图片具有透明背景,在旋转图像时看起来更好。
如何操作…
如前文准备工作部分所述,我们需要一个指南针的图像。你可以下载之前链接的那个,或者使用你喜欢的任何图像,然后按照以下步骤操作:
-
将你的图像复制到
res/drawable
文件夹中,并将其命名为compass.png
。 -
打开
activity_main.xml
文件,将现有的TextView
替换为以下ImageView
:<ImageView android:id="@+id/imageViewCompass" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:src="img/compass"/>
-
现在打开
MainActivity.java
文件,并添加以下全局变量声明:private SensorManager mSensorManager; private Sensor mMagnetometer; private Sensor mAccelerometer; private ImageView mImageViewCompass; private float[] mGravityValues=new float[3]; private float[] mAccelerationValues=new float[3]; private float[] mRotationMatrix=new float[9]; private float mLastDirectionInDegrees = 0f;
-
在
MainActivity
类中添加以下SensorEventListener
类:private SensorEventListener mSensorListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { calculateCompassDirection(event); } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { //Nothing to do } };
-
按如下方式覆盖
onResume()
和onPause()
:@Override protected void onResume() { super.onResume(); mSensorManager.registerListener(mSensorListener, mMagnetometer, SensorManager.SENSOR_DELAY_FASTEST); mSensorManager.registerListener(mSensorListener, mAccelerometer, SensorManager.SENSOR_DELAY_FASTEST); } @Override protected void onPause() { super.onPause(); mSensorManager.unregisterListener(mSensorListener); }
-
在现有的
onCreate()
方法中添加以下代码:mImageViewCompass=(ImageView)findViewById(R.id.imageViewCompass); mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
-
最终代码执行实际计算和动画:
private void calculateCompassDirection(SensorEvent event) { switch (event.sensor.getType()) { case Sensor.TYPE_ACCELEROMETER: mAccelerationValues = event.values.clone(); break; case Sensor.TYPE_MAGNETIC_FIELD: mGravityValues = event.values.clone(); break; } boolean success = SensorManager.getRotationMatrix(mRotationMatrix, null, mAccelerationValues, mGravityValues); if(success){ float[] orientationValues = new float[3]; SensorManager.getOrientation(mRotationMatrix, orientationValues); float azimuth = (float)Math.toDegrees(-orientationValues[0]); RotateAnimation rotateAnimation = new RotateAnimation(mLastDirectionInDegrees, azimuth, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); rotateAnimation.setDuration(50); rotateAnimation.setFillAfter(true); mImageViewCompass.startAnimation(rotateAnimation); mLastDirectionInDegrees = azimuth; }
-
你已经准备好运行应用程序了。尽管你可以在模拟器上运行这个应用程序,但如果没有加速度计和磁力计,你是看不到指南针移动的。
工作原理…
由于我们已经在上一章的读取传感器数据——使用 Android 传感器框架中涵盖了读取传感器数据,所以我们不会重复解释传感器框架,而是直接跳到calculateCompassDirection()
方法。
我们直接从onSensorChanged()
回调中调用这个方法。由于我们使用了相同的类来处理磁力计和加速度计的传感器回调,因此我们首先检查SensorEvent
中报告的是哪个传感器。然后我们调用SensorManager.getRotationMatrix()
,传入最后的传感器数据。如果计算成功,它将返回一个RotationMatrix
,我们使用它来调用SensorManager.getOrientation()
方法。getOrientation()
将在orientationValues
数组中返回以下数据:
-
Azimuth:
value [0]
-
Pitch:
value [1]
-
Roll:
value [2]
方位角以弧度报告,方向相反,因此我们改变符号并使用Math.toDegrees()
将其转换为度数。方位角表示北方的方向,因此我们在RotateAnimation
中使用它。
由于SensorManager
已经完成了数学计算,实际的指南针动画非常简单。我们使用之前的方向和新方向创建一个RotateAnimation
。我们使用Animation.RELATIVE_TO_SELF
标志和 0.5f(或 50%)将图像中心设置为旋转点。在调用startAnimation()
更新指南针之前,我们使用setDuration()
和setFillAfter(true)
设置动画持续时间。(使用true
表示动画完成后我们希望图像保持原样,否则图像将重置回原始图像。)最后,我们保存方位角以供下次传感器更新使用。
还有更多…
花时间实验RotationAnimation
设置和传感器更新时机是值得的。在我们注册传感器监听器的调用中,我们使用SensorManager.SENSOR_DELAY_FASTEST
和 50 毫秒的setDuration()
来创建快速动画。你也可以尝试使用更慢的传感器更新和更慢的动画,并比较结果。
另请参阅
-
有关读取传感器数据的详细信息,请参阅前一章中的使用 Android 传感器框架读取传感器数据。
-
请参考getRotationMatrix()开发者文档,位于http://developer.android.com/reference/android/hardware/SensorManager.html#getRotationMatrix(float[], float[], float[], float[])
-
请参考getOrientation()开发者文档,位于http://developer.android.com/reference/android/hardware/SensorManager.html#getOrientation(float[], float[])
-
请参考RotateAnimation 开发者文档,位于
developer.android.com/reference/android/view/animation/RotateAnimation.html
使用 ViewPager 创建幻灯片放映
本教程将向您展示如何使用ViewPager
类创建幻灯片。以下是显示从一张图片过渡到另一张图片的屏幕截图:
准备就绪
在 Android Studio 中创建一个新项目,名为SlideShow
。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
我们需要几幅图像用于幻灯片。为了演示目的,我们从www.Pixabay.com下载了四张图片包含在项目源文件中,但你可以使用任何图片。
如何操作…
我们将创建一个 Fragment 来显示幻灯片中的每张图片,然后在主活动中设置ViewPager
。以下是步骤:
-
将四张图片复制到
/res/drawable
文件夹中,并将它们命名为slide_0
至slide_3
,保持其原始文件扩展名。 -
使用以下 XML 创建一个名为
fragment_slide.xml
的新布局文件:<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/imageView" android:layout_gravity="center_horizontal" /> </LinearLayout>
-
现在创建一个名为
SlideFragment
的新 Java 类。它将如下扩展Fragment
:public class SlideFragment extends Fragment {
使用以下导入:
import android.support.v4.app.Fragment;
-
添加以下全局声明:
private int mImageResourceID;
-
添加以下空的默认片段构造函数:
public SlideFragment() {}
-
添加以下方法以保存图像资源 ID:
public void setImage(int resourceID) { mImageResourceID=resourceID; }
-
如下重写
onCreateView()
:@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_slide, container, false); ImageView imageView = (ImageView)rootView.findViewById(R.id.imageView); imageView.setImageResource(mImageResourceID); return rootView; }
-
我们的主活动将只显示一个
ViewPager
。打开activity_main.xml
,并按以下方式替换文件内容:<android.support.v4.view.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent" />
-
现在打开
MainActivity.java
,将MainActivity
更改为扩展FragmentActivity
,如下所示:public class MainActivity extends FragmentActivity {
使用以下导入:
import android.support.v4.app.FragmentActivity;
-
添加以下全局声明:
private final int PAGE_COUNT=4; private ViewPager mViewPager; private PagerAdapter mPagerAdapter;
使用以下导入:
import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager;
-
在
MainActivity
中创建以下子类:private class SlideAdapter extends FragmentStatePagerAdapter { public SlideAdapter(FragmentManager fm) { super(fm); } @Override public Fragment getItem(int position) { SlideFragment slideFragment = new SlideFragment(); switch (position) { case 0: slideFragment.setImage(R.drawable.slide_0); break; case 1: slideFragment.setImage(R.drawable.slide_1); break; case 2: slideFragment.setImage(R.drawable.slide_2); break; case 3: slideFragment.setImage(R.drawable.slide_3); break; } return slideFragment; } @Override public int getCount() { return PAGE_COUNT; } }
使用以下导入:
import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter;
-
如下重写
onBackPressed()
:@Override public void onBackPressed() { if (mViewPager.getCurrentItem() == 0) { super.onBackPressed(); } else { mViewPager.setCurrentItem(mViewPager.getCurrentItem() - 1); } }
-
在
onCreate()
方法中添加以下代码:mViewPager = (ViewPager) findViewById(R.id.viewPager); mPagerAdapter = new SlideAdapter(getSupportFragmentManager()); mViewPager.setAdapter(mPagerAdapter);
-
在设备或模拟器上运行应用程序。
工作原理…
第一步是创建一个片段。由于我们要做幻灯片,我们只需要一个ImageViewer
。我们还更改MainActivity
以扩展FragmentActivity
,以便将片段加载到ViewPager
中。
ViewPager
使用FragmentStatePagerAdapter
作为片段过渡的来源。我们创建SlideAdapter
来处理FragmentStatePagerAdapter
类中的两个回调:
-
getCount()
-
getItem()
getCount()
简单地返回我们幻灯片中的页面数。getItem()
返回实际要显示的片段。这里我们指定要显示的图像。如您所见,添加或更改幻灯片会非常容易。
处理返回键不是ViewPager
的要求,但它确实提供了更好的用户体验。onBackPressed()
递减当前页面,直到达到第一页,然后它将返回键发送给超类,退出应用程序。
还有更多…
如示例所示,ViewPager
处理了大部分工作,包括过渡动画的处理。如果我们需要,可以通过实现ViewPager.PageTransformer
接口上的transformPage()
回调来自定义过渡。(有关自定义动画,请参见下一个食谱。)
创建设置向导
ViewPager
还可以用于创建设置向导。而不是创建一个用于显示图片的单个碎片,为向导的每个步骤创建一个碎片,并在getItem()
回调中返回适当的碎片。
另请参阅
-
要创建自定义动画,请参考创建自定义缩放动画食谱中的示例。
使用碎片创建卡牌翻转动画
牌面翻转是一个常见的动画,我们将使用碎片过渡来演示。我们将使用两张不同的图片——一张用于正面,一张用于背面,以创建牌面翻转效果。我们需要四个动画资源:两个用于正面过渡,两个用于背面过渡,我们将使用objectAnimator
在 XML 中定义它们。
这是我们将要构建的应用程序的屏幕截图,展示了卡牌翻转动画的实际效果:
准备工作
在 Android Studio 中创建一个新项目,命名为CardFlip
。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
对于扑克牌的正反面图片,我们在www.Pixabay.com上找到了以下图片:
如何操作…
我们将需要两个碎片——一个用于牌的正面,另一个用于背面。每个碎片将定义牌的图片。然后我们需要四个动画文件以实现完整的牌面翻转效果。以下是正确设置项目结构并创建所需资源的步骤:
-
一旦有了牌的正反面图片,将它们复制到
res/drawable
文件夹中,分别命名为card_front.jpg
和card_back.jpg
(如果图片原始文件扩展名不同,请保持原样)。 -
创建一个动画资源目录:
res/animator
。(在 Android Studio 中,转到File | New | Android resource directory。当显示New Android Resource对话框时,在Resource Type下拉菜单中选择animator
。) -
在
res/animator
中创建card_flip_left_enter.xml
,使用以下 XML:<set > <objectAnimator android:valueFrom="1.0" android:valueTo="0.0" android:propertyName="alpha" android:duration="0" /> <objectAnimator android:valueFrom="-180" android:valueTo="0" android:propertyName="rotationY" android:interpolator="@android:interpolator/accelerate_decelerate" android:duration="@integer/card_flip_duration_full"/> <objectAnimator android:valueFrom="0.0" android:valueTo="1.0" android:propertyName="alpha" android:startOffset="@integer/card_flip_duration_half" android:duration="1" /> </set>
-
在
res/animator
中创建card_flip_left_exit.xml
,使用以下 XML:<set > <objectAnimator android:valueFrom="0" android:valueTo="180" android:propertyName="rotationY" android:interpolator="@android:interpolator/accelerate_decelerate" android:duration="@integer/card_flip_duration_full"/> <objectAnimator android:valueFrom="1.0" android:valueTo="0.0" android:propertyName="alpha" android:startOffset="@integer/card_flip_duration_half" android:duration="1" /> </set>
-
在
res/animator
中创建card_flip_right_enter.xml
,使用以下 XML:<set > <objectAnimator android:valueFrom="1.0" android:valueTo="0.0" android:propertyName="alpha" android:duration="0" /> <objectAnimator android:valueFrom="180" android:valueTo="0" android:propertyName="rotationY" android:interpolator="@android:interpolator/accelerate_decelerate" android:duration="@integer/card_flip_duration_full" /> <objectAnimator android:valueFrom="0.0" android:valueTo="1.0" android:propertyName="alpha" android:startOffset="@integer/card_flip_duration_half" android:duration="1" /> </set>
-
在
res/animator
中创建card_flip_right_exit.xml
,使用以下 XML:<set > <objectAnimator android:valueFrom="0" android:valueTo="-180" android:propertyName="rotationY" android:interpolator="@android:interpolator/accelerate_decelerate" android:duration="@integer/card_flip_duration_full" /> <objectAnimator android:valueFrom="1.0" android:valueTo="0.0" android:propertyName="alpha" android:startOffset="@integer/card_flip_duration_half" android:duration="1" /> </set>
-
在
res/values
中创建一个名为timing.xml
的新资源文件,使用以下 XML:<?xml version="1.0" encoding="utf-8"?> <resources> <integer name="card_flip_duration_full">1000</integer> <integer name="card_flip_duration_half">500</integer> </resources>
-
在
res/layout
中创建一个名为fragment_card_front.xml
的新文件,使用以下 XML:<?xml version="1.0" encoding="utf-8"?> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:src="img/card_front" android:scaleType="centerCrop" />
-
在
res/layout
中创建一个名为fragment_card_back.xml
的新文件,使用以下 XML:<?xml version="1.0" encoding="utf-8"?> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:src="img/card_back" android:scaleType="centerCrop" />
-
使用以下代码创建一个名为
CardFrontFragment
的新 Java 类:public class CardFrontFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_card_front, container, false); } }
-
使用以下代码创建一个名为
CardBackFragment
的新 Java 类:public class CardBackFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_card_back, container, false); } }
-
使用以下 XML 替换现有的
activity_main.xml
文件:<FrameLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" />
-
打开
MainActivity.java
并添加以下全局声明:boolean mShowingBack = false;
-
在现有的
onCreate()
方法中添加以下代码:FrameLayout frameLayout = (FrameLayout)findViewById(R.id.frameLayout); frameLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { flipCard(); } }); if (savedInstanceState == null) { getFragmentManager() .beginTransaction() .add(R.id.frameLayout, new CardFrontFragment()) .commit(); }
-
添加以下方法,该方法处理实际的片段过渡:
private void flipCard() { if (mShowingBack) { mShowingBack = false; getFragmentManager().popBackStack(); } else { mShowingBack = true; getFragmentManager() .beginTransaction() .setCustomAnimations( R.animator.card_flip_right_enter, R.animator.card_flip_right_exit, R.animator.card_flip_left_enter, R.animator.card_flip_left_exit) .replace(R.id.frameLayout, new CardBackFragment()) .addToBackStack(null) .commit(); } }
-
现在你可以在设备或模拟器上运行应用程序了。
工作原理…
创建卡牌翻转的大部分工作在于设置资源。由于我们需要卡牌的正面和背面视图,因此我们创建了两个带有适当图片的片段。当点击卡牌时,我们调用 flipCard()
方法。实际的动画由 setCustomAnimations()
处理。在这里,我们传递了在 XML 中定义的四个动画资源。正如你所看到的,Android 使这一过程变得非常简单。
需要注意的是,我们没有使用 Support Library Fragment Manager,因为支持库不支持 objectAnimator
。如果你想要支持低于 Android 3.0 的版本,你需要包含旧的 anim
资源并在运行时检查操作系统版本,或者在代码中创建动画资源。(请参阅下一个示例。)
参见
-
请参阅下一个示例,使用自定义过渡创建缩放动画,了解在代码中创建的动画资源示例。
-
参阅 Integer Resource Type 的网页,链接为
developer.android.com/guide/topics/resources/more-resources.html#Integer
。
使用自定义过渡创建缩放动画
前一个示例,使用片段创建卡牌翻转动画,演示了使用动画资源文件的过渡动画。在本示例中,我们将使用代码中创建的动画资源来创建缩放效果。应用程序显示一个缩略图,当点击时会放大显示完整图片。
下图包含了三个屏幕截图,展示了缩放动画的实际效果:
准备工作
在 Android Studio 中创建一个新项目,命名为 <项目名称>
。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
为了这个示例所需的图片,我们从 www.Pixabay.com 下载了一张图片并包含在项目源文件中,但你可以使用任何图片。
如何操作…
按照之前所述准备好你的图片后,执行以下步骤:
-
将你的图片复制到
res/drawable
文件夹中,并将其命名为image.jpg
(如果不是 jpeg 格式,保持原始文件扩展名)。 -
现在,打开
activity_main.xml
文件,用以下内容替换现有的 XML:<?xml version="1.0" encoding="utf-8"?> <FrameLayout android:id="@+id/frameLayout" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <ImageButton android:id="@+id/imageViewThumbnail" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="centerCrop" android:background="@android:color/transparent"/> </LinearLayout> <ImageView android:id="@+id/imageViewExpanded" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible" /> </FrameLayout>
-
现在,打开
MainActivity.java
文件,并声明以下全局变量:private Animator mCurrentAnimator; private ImageView mImageViewExpanded;
-
添加我们在缩放大型图片以避免内存溢出异常食谱中创建的
loadSampledResource()
方法来缩放图片:public Bitmap loadSampledResource(int imageID, int targetHeight, int targetWidth) { final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), imageID, options); final int originalHeight = options.outHeight; final int originalWidth = options.outWidth; int inSampleSize = 1; while ((originalHeight / (inSampleSize *2)) > targetHeight && (originalWidth / (inSampleSize *2)) > targetWidth) { inSampleSize *= 2; } options.inSampleSize =inSampleSize; options.inJustDecodeBounds = false; return (BitmapFactory.decodeResource(getResources(), imageID, options)); }
-
在
onCreate()
方法中添加以下代码:final ImageView imageViewThumbnail = (ImageView)findViewById(R.id.imageViewThumbnail); imageViewThumbnail.setImageBitmap(loadSampledResource(R.drawable.image, 100, 100)); imageViewThumbnail.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { zoomFromThumbnail((ImageView) view); } }); mImageViewExpanded = (ImageView) findViewById(R.id.imageViewExpanded); mImageViewExpanded.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mImageViewExpanded.setVisibility(View.GONE); mImageViewExpanded.setImageBitmap(null); imageViewThumbnail.setVisibility(View.VISIBLE); } });
-
添加以下
zoomFromThumbnail()
方法,该方法处理实际的动画,稍后会进行解释:private void zoomFromThumbnail(final ImageView imageViewThumb) { if (mCurrentAnimator != null) { mCurrentAnimator.cancel(); } final Rect startBounds = new Rect(); final Rect finalBounds = new Rect(); final Point globalOffset = new Point(); imageViewThumb.getGlobalVisibleRect(startBounds); findViewById(R.id.frameLayout).getGlobalVisibleRect(finalBounds, globalOffset); mImageViewExpanded.setImageBitmap(loadSampledResource(R.drawable.image, finalBounds.height(), finalBounds.width())); startBounds.offset(-globalOffset.x, -globalOffset.y); finalBounds.offset(-globalOffset.x, -globalOffset.y); float startScale; if ((float) finalBounds.width() / finalBounds.height() > (float) startBounds.width() / startBounds.height()) { startScale = (float) startBounds.height() / finalBounds.height(); float startWidth = startScale * finalBounds.width(); float deltaWidth = (startWidth - startBounds.width()) / 2; startBounds.left -= deltaWidth; startBounds.right += deltaWidth; } else { startScale = (float) startBounds.width() / finalBounds.width(); float startHeight = startScale * finalBounds.height(); float deltaHeight = (startHeight - startBounds.height()) / 2; startBounds.top -= deltaHeight; startBounds.bottom += deltaHeight; } imageViewThumb.setVisibility(View.GONE); mImageViewExpanded.setVisibility(View.VISIBLE); mImageViewExpanded.setPivotX(0f); mImageViewExpanded.setPivotY(0f); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.play(ObjectAnimator.ofFloat(mImageViewExpanded, View.X,startBounds.left, finalBounds.left)).with(ObjectAnimator.ofFloat(mImageViewExpanded, View.Y,startBounds.top, finalBounds.top)) .with(ObjectAnimator.ofFloat(mImageViewExpanded, View.SCALE_X, startScale, 1f)) .with(ObjectAnimator.ofFloat(mImageViewExpanded, View.SCALE_Y, startScale, 1f)); animatorSet.setDuration(1000); animatorSet.setInterpolator(new DecelerateInterpolator()); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mCurrentAnimator = null; } @Override public void onAnimationCancel(Animator animation) { mCurrentAnimator = null; } }); animatorSet.start(); mCurrentAnimator = animatorSet; }
-
在设备或模拟器上运行应用程序。
它是如何工作的…
首先,看看我们使用的布局文件。它有两部分——带有缩略图ImageView
的LinearLayout
,以及展开的ImageView
。当点击图片时,我们控制这两个视图的可见性。我们使用与缩放大型图片以避免内存溢出异常食谱中讨论的相同的loadSampledResource()
来设置开始的缩略图。
zoomFromThumbnail()
是本次演示中实际完成工作的地方。代码很多,下面进行分解说明。
首先,我们将当前动画存储在mCurrentAnimator
中,这样如果动画当前正在运行,我们可以取消它。
接下来,我们使用getGlobalVisibleRect()
方法获取图片的起始位置。这返回视图的屏幕位置。当我们获取展开的ImageView
的可见边界时,我们还会获取视图的GlobalOffset
,以将坐标从应用坐标偏移到屏幕坐标。
设置了起始边界后,下一步是计算结束边界。我们希望最终图片保持相同的宽高比,以防止它变形。我们需要计算边界需要如何调整,以保持宽高比在展开的ImageView
内。引言中的截图显示了此图片的大小,但这将因图片和设备而异。
计算好起始和结束边界后,我们现在可以创建动画——实际上,在这种情况下是四个动画。如下代码所示,每个矩形的每个点都有一个动画:
animatorSet.play(ObjectAnimator.ofFloat(mImageViewExpanded, View.X,startBounds.left, finalBounds.left)).with(ObjectAnimator.ofFloat(mImageViewExpanded, View.Y,startBounds.top, finalBounds.top)).with(ObjectAnimator.ofFloat(mImageViewExpanded, View.SCALE_X,startScale, 1f)).with(ObjectAnimator.ofFloat(mImageViewExpanded, View.SCALE_Y, startScale, 1f));
这两行代码控制动画的外观:
animatorSet.setDuration(1000);
animatorSet.setInterpolator(new AccelerateInterpolator());
setDuration()
方法告诉动画对象完成之前设置的转换需要多长时间。setInterpolator()
控制如何进行转换。(在引言中提到了插值器,并在后面提供了链接。)使用start()
方法启动动画后,我们将当前动画保存到mCurrentAnimator
变量中,以便在需要时可以取消动画。我们创建了一个AnimatorListenerAdapter
以响应动画事件,清除mCurrentAnimator
变量。
还有更多…
当用户点击展开的图片时,应用只是隐藏了展开的ImageView
并将缩略图设置为可见
。我们可以在mImageViewExpanded
的点击事件中使用扩展边界作为起点,返回到缩略图边界来创建反向缩放动画。(在zoomFromThumbnail()
中创建mImageViewExpanded
事件可能更容易,以避免再次重复计算开始和结束边界。)
获取默认动画持续时间
我们的代码在调用setDuration()
设置持续时间时使用了 1000 毫秒。我们故意使用较长的持续时间,以便更容易查看动画。我们可以使用以下代码获取默认的 Android 动画持续时间:
getResources().getInteger(android.R.integer.config_shortAnimTime)
另请参阅
-
第一个食谱,缩小大图以避免内存溢出异常,其中详细解释了
loadSampledResource()
方法。 -
请参考
developer.android.com/reference/android/view/animation/Interpolator.html
的插值器开发者文档。