安卓应用开发秘籍第二版(五)
原文:
zh.annas-archive.org/md5/ceefdd89e585c59c20db6a7760dc11f1译者:飞龙
第十章:初识 OpenGL ES
本章节,我们将涵盖以下主题:
-
设置 OpenGL ES 环境
-
在 GLSurfaceView 上绘制形状
-
绘制时应用投影和摄像机视图
-
使用旋转移动三角形
-
使用用户输入旋转三角形
简介
正如我们在上一章看到的,Android 提供了许多处理图形和动画的工具。尽管画布和可绘制对象是为了自定义绘图设计的,但当你需要高性能图形,尤其是 3D 游戏图形时,Android 也支持 OpenGL ES。嵌入式系统开放图形库(OpenGL ES)针对的是嵌入式系统。(嵌入式系统包括游戏机和手机。)
本章旨在作为在 Android 上使用 OpenGL ES 的入门介绍。像往常一样,我们会提供步骤并解释事物是如何工作的,但不会深入探讨 OpenGL 的数学或技术细节。如果你在其他平台(如 iOS)上已经熟悉 OpenGL ES,那么本章应能让你快速上手。如果你是 OpenGL 的新手,希望这些教程能帮助你决定这是否是一个你想追求的领域。
Android 支持以下版本的 OpenGL:
-
OpenGL ES 1.0:Android 1.0
-
OpenGL ES 2.0:在 Android 2.2(API 8)中引入
-
OpenGL ES 3.0:在 Android 4.3(API 18)中引入
-
OpenGL ES 3.1:在 Android 5.0(API 21)中引入
本章节的教程具有入门性质,针对的是 OpenGL ES 2.0 及更高版本。几乎所有的现有设备都支持 OpenGL ES 2.0。与 OpenGL ES 2.0 及更低版本不同,OpenGL 3.0 及更高版本需要硬件制造商提供驱动程序实现。这意味着,即使你的应用程序运行在 Android 5.0 上,OpenGL 3.0 及更高版本可能也无法使用。因此,在运行时检查可用的 OpenGL 版本是一个好的编程实践。另外,如果你的应用程序需要 3.0 及更高版本的功能,你可以在 Android 清单中添加一个<uses-feature/>元素。(我们将在接下来的第一个教程中讨论这个问题。)
与本书的其他章节不同,本章更像是一个教程,每个教程都建立在从前一个教程中学到的知识上。《准备就绪》部分将每个教程的前提条件讲清楚。
建立 OpenGL ES 环境
我们第一个教程将从展示如何设置一个活动以使用 OpenGL GLSurfaceView的步骤开始。与画布类似,GLSurfaceView是你执行 OpenGL 绘图的地方。由于这是起点,其他教程在需要创建GLSurfaceView时会将这个教程作为基本步骤引用。
准备就绪
在 Android Studio 中创建一个新项目,并将其命名为:SetupOpenGL。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动。
如何操作...
我们将从在 Android Manifest 中指明应用程序对 OpenGL 的使用开始,然后将 OpenGL 类添加到活动中。以下是步骤:
-
打开 Android Manifest 并添加以下 XML:
<uses-feature android:glEsVersion="0x00020000" android:required="true" /> -
打开
MainActivity.java并添加以下全局变量:private GLSurfaceView mGLSurfaceView; -
向
MainActivity类添加以下内部类:class CustomGLSurfaceView extends GLSurfaceView { private final GLRenderer mGLRenderer; public CustomGLSurfaceView(Context context){ super(context); setEGLContextClientVersion(2); mGLRenderer = new GLRenderer(); setRenderer(mGLRenderer); } } -
向
MainActivity类添加另一个内部类:class GLRenderer implements GLSurfaceView.Renderer { public void onSurfaceCreated(GL10 unused, EGLConfig config) { GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f); } public void onDrawFrame(GL10 unused) { GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); } public void onSurfaceChanged(GL10 unused, int width, int height) { GLES20.glViewport(0, 0, width, height);} } -
在现有的
onCreate()方法中添加以下代码:mGLSurfaceView = new CustomGLSurfaceView(this); setContentView(mGLSurfaceView); -
你现在可以在设备或模拟器上运行这个应用程序了。
工作原理...
如果你运行了前面的应用程序,你会看到活动创建并且背景设置为灰色。由于这些是设置 OpenGL 的基本步骤,你将在这个章节的其他食谱中重用这段代码。以下是详细解释的过程:
在 Android Manifest 中声明 OpenGL
我们首先在 Android Manifest 中通过这行代码声明我们要求使用 OpenGL ES 版本 2.0:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
如果我们使用的是版本 3.0,我们会使用这个:
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
对于版本 3.1,使用这个:
<uses-feature android:glEsVersion="0x00030001" android:required="true" />
扩展 GLSurfaceView 类
通过扩展GLSurfaceView创建一个自定义的 OpenGL SurfaceView类,就像这段代码中做的那样:
class CustomGLSurfaceView extends GLSurfaceView {
private final GLRenderer mGLRenderer;
public CustomGLSurfaceView(Context context){
super(context);
setEGLContextClientVersion(2);
mGLRenderer = new GLRenderer();
setRenderer(mGLRenderer);
}
}
在这里,我们实例化一个 OpenGL 渲染类,并通过setRenderer()方法将其传递给GLSurfaceView类。OpenGL SurfaceView为我们的 OpenGL 绘制提供了一个表面,类似于Canvas和SurfaceView对象。实际的绘制在Renderer中完成,我们接下来会创建它:
创建一个 OpenGL 渲染类
最后一步是创建GLSurfaceView.Renderer类并实现以下三个回调:
-
onSurfaceCreated() -
onDrawFrame() -
onSurfaceChanged()
以下是代码:
class GLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
}
目前,我们用这个类所做的就是设置回调并使用glClearColor()(在这种情况下是灰色)清除屏幕。
还有更多...
设置好 OpenGL 环境后,我们将继续下一个食谱,在那里我们将实际在视图中进行绘制。
在 GLSurfaceView 上绘制形状
上一个食谱设置了使用 OpenGL 的活动。这个食谱将继续展示如何在OpenGLSurfaceView上进行绘制。
首先,我们需要定义形状。使用 OpenGL 时,要意识到形状顶点的定义顺序非常重要,因为它们决定了形状的前面(面)和后面。通常(也是默认行为)是按逆时针定义顶点。(尽管这种行为可以改变,但这需要额外的代码,并不是标准做法。)
了解 OpenGL 屏幕坐标系统同样重要,因为它与 Android 画布的坐标系统不同。默认的坐标系统将(0,0,0)定义为屏幕中心。四个边缘点的坐标如下:
-
左上角:(-1.0, 1.0, 0)
-
右上角:(1.0, 1.0, 0)
-
左下角:(-1.0, -1.0, 0)
-
右下角:(1.0, -1.0, 0)
z轴直接从屏幕前或屏幕后出来。
下面是一个展示x、y和z轴的图示:
我们将创建一个Triangle类,因为它是基本形状。在 OpenGL 中,你通常会使用一系列三角形来创建对象。要使用 OpenGL 绘制形状,我们需要定义以下内容:
-
顶点着色器:这是为了绘制形状
-
片元着色器:这是为了给形状上色
-
程序:这是前面着色器的 OpenGL ES 对象
着色器使用OpenGL 着色语言(GLSL)定义,然后编译并添加到 OpenGL 程序对象中。
这有两张屏幕截图,展示了三角形在纵向和横向的显示效果:
准备工作
在 Android Studio 中创建一个新项目,并将其命名为:ShapesWithOpenGL。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动。
本食谱使用了前一个食谱设置 OpenGL 环境中创建的 OpenGL 环境。如果你还没有完成那些步骤,请参考前一个食谱。
如何操作...
如前所述,我们将使用前一个食谱中创建的 OpenGL 环境。以下步骤将指导你创建一个三角形形状的类并在 GLSurfaceView 上绘制它:
-
创建一个名为
Triangle的新 Java 类。 -
向
Triangle类中添加以下全局声明:private final String vertexShaderCode ="attribute vec4 vPosition;" +"void main() {" +" gl_Position = vPosition;" +"}"; private final String fragmentShaderCode ="precision mediump float;" +"uniform vec4 vColor;" +"void main() {" +" gl_FragColor = vColor;" +"}"; final int COORDS_PER_VERTEX = 3; float triangleCoords[] = { 0.0f, 0.66f, 0.0f, -0.5f, -0.33f, 0.0f, 0.5f, -0.33f, 0.0f }; float color[] = { 0.63f, 0.76f, 0.22f, 1.0f }; private final int mProgram; private FloatBuffer vertexBuffer; private int mPositionHandle; private int mColorHandle; private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX; private final int vertexStride = COORDS_PER_VERTEX * 4; -
向
Triangle类中添加以下loadShader()方法:public int loadShader(int type, String shaderCode){ int shader = GLES20.glCreateShader(type); GLES20.glShaderSource(shader, shaderCode); GLES20.glCompileShader(shader); return shader; } -
添加如下所示的
Triangle构造函数:public Triangle() { int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER,vertexShaderCode); int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER,fragmentShaderCode); mProgram = GLES20.glCreateProgram(); GLES20.glAttachShader(mProgram, vertexShader); GLES20.glAttachShader(mProgram, fragmentShader); GLES20.glLinkProgram(mProgram); ByteBuffer bb = ByteBuffer.allocateDirect(triangleCoords.length * 4); bb.order(ByteOrder.nativeOrder()); vertexBuffer = bb.asFloatBuffer(); vertexBuffer.put(triangleCoords); vertexBuffer.position(0); } -
添加如下
draw()方法:public void draw() { GLES20.glUseProgram(mProgram); mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition"); GLES20.glEnableVertexAttribArray(mPositionHandle); GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,GLES20.GL_FLOAT, false,vertexStride, vertexBuffer); mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor"); GLES20.glUniform4fv(mColorHandle, 1, color, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount); GLES20.glDisableVertexAttribArray(mPositionHandle); } -
现在打开
MainActivity.java,并向GLRenderer类中添加一个Triangle变量,如下所示:private Triangle mTriangle; -
在
onSurfaceCreated()回调中初始化Triangle变量,如下所示:mTriangle = new Triangle(); -
在
onDrawFrame()回调中调用draw()方法:mTriangle.draw(); -
你现在可以在设备或模拟器上运行应用程序了。
工作原理...
如引言中所述,要使用 OpenGL 绘图,我们首先必须定义着色器,我们使用以下代码来完成:
private final String vertexShaderCode ="attribute vec4 vPosition;" +"void main() {" +" gl_Position = vPosition;" +"}";
private final String fragmentShaderCode ="precision mediump float;" +"uniform vec4 vColor;" +"void main() {" +" gl_FragColor = vColor;" +"}";
由于这是未编译的OpenGL 着色语言(OpenGLSL),下一步是编译并将其附加到我们的 OpenGL 对象上,我们使用以下两个 OpenGL ES 方法来完成:
-
glAttachShader() -
glLinkProgram()
设置着色器后,我们创建ByteBuffer来存储三角形顶点,这些顶点在triangleCoords中定义。draw()方法是实际使用 GLES20 库调用进行绘制的位置,它从onDrawFrame()回调中被调用。
还有更多...
你可能已经从引言中的屏幕截图注意到,纵向和横向的三角形看起来是相同的。从代码中可以看出,在绘制时我们没有区分方向。我们将在下一个食谱中解释为什么会这样,并展示如何纠正这个问题。
另请参阅
有关 OpenGL 着色语言的更多信息,请参考以下链接:
www.opengl.org/documentation/glsl/
在绘制时应用投影和摄像机视角
正如在前一个食谱中我们所看到的,当我们把形状绘制到屏幕上时,形状会被屏幕方向扭曲。之所以会发生这种情况,是因为 OpenGL 默认假设屏幕是完美正方形。我们之前提到过,默认的屏幕坐标右上角是(1,1,0),左下角是(-1,-1,0)。
由于大多数设备屏幕都不是完美正方形,我们需要将显示坐标映射到与我们的物理设备相匹配。在 OpenGL 中,我们通过投影来实现这一点。这个食谱将展示如何使用投影将 GLSurfaceView 坐标与设备坐标相匹配。除了投影,我们还将展示如何设置摄像机视角。以下是显示最终结果的屏幕截图:
准备工作
在 Android Studio 中创建一个新项目,并将其命名为:ProjectionAndCamera。使用默认的手机 & 平板选项,在选择活动类型时选择空活动。
这个食谱基于之前的食谱在 GLSurfaceView 上绘制形状。如果你还没有输入之前的食谱,请在开始这些步骤之前完成它。
如何操作...
如前所述,这个食谱将基于前一个食谱,所以在开始之前请完成那些步骤。我们将修改之前的代码,在绘图计算中添加投影和摄像机视角。以下是步骤:
-
打开
Triangle类,并在现有声明中添加以下全局声明:private int mMVPMatrixHandle; -
在
vertexShaderCode中添加一个矩阵变量,并在位置计算中使用它。以下是最终结果:private final String vertexShaderCode = "attribute vec4 vPosition;" + "uniform mat4 uMVPMatrix;" + "void main() {" + " gl_Position = uMVPMatrix * vPosition;" + "}"; -
改变
draw()方法,按以下方式传入一个矩阵参数:public void draw(float[] mvpMatrix) { -
为了使用变换矩阵,在
draw()方法中,在GLES20.glDrawArrays()方法之前添加以下代码:mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0); -
打开
MainActivity.java文件,在GLRenderer类中添加以下类变量:private final float[] mMVPMatrix = new float[16]; private final float[] mProjectionMatrix = new float[16]; private final float[] mViewMatrix = new float[16]; -
修改
onSurfaceChanged()回调,按照以下方式计算位置矩阵:public void onSurfaceChanged(GL10 unused, int width, int height) { GLES20.glViewport(0, 0, width, height); float ratio = (float) width / height; Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7); } -
修改
onDrawFrame()回调,按照以下方式计算摄像机视角:public void onDrawFrame(GL10 unused) { Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f); Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); mTriangle.draw(mMVPMatrix); } -
你现在可以在设备或模拟器上运行应用程序了。
工作原理...
首先,我们修改vertexShaderCode以包含一个矩阵变量。我们在onSurfaceChanged()回调中使用传入的宽度和高度参数来计算矩阵。我们将变换矩阵传递给draw()方法,在计算绘图位置时使用它。
在我们调用draw()方法之前,我们计算摄像机视角。这两行代码计算摄像机视角:
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
如果没有这段代码,实际上不会绘制出三角形,因为摄像机视角无法“看到”我们的顶点。(这回到了我们之前讨论的顶点顺序如何决定图像的前后。)
现在运行程序,你会看到在引言中展示的输出。注意,即使屏幕旋转,我们现在也有一个均匀的三角形。
还有更多...
在下一个教程中,我们将通过旋转三角形来展示 OpenGL 的强大功能。
通过旋转移动三角形
到目前为止,我们用 OpenGL 展示的内容可能使用传统的画布或可绘制对象更容易实现。这个教程将通过旋转三角形来展示 OpenGL 的一些强大功能。并不是说我们不能用其他绘图方法创建运动,但是使用 OpenGL 可以轻松实现这一点!
这个教程将演示如何旋转三角形,如下面的截图所示:
准备工作
在 Android Studio 中创建一个新项目,并将其命名为:CreatingMovement。在选择活动类型时,使用默认的手机 & 平板选项,并选择空活动。
本教程基于之前的教程在绘制时应用投影和相机视图。如果你还没有输入之前的教程,请在继续之前完成。
如何操作...
由于我们是从上一个教程继续,所以需要做的工作非常少。打开 MainActivity.java 并按照以下步骤操作:
-
向
GLRendered类中添加一个矩阵:private float[] mRotationMatrix = new float[16]; -
在
onDrawFrame()回调中,用以下代码替换现有的mTriangle.draw(mMVPMatrix);语句:float[] tempMatrix = new float[16]; long time = SystemClock.uptimeMillis() % 4000L; float angle = 0.090f * ((int) time); Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f); Matrix.multiplyMM(tempMatrix, 0, mMVPMatrix, 0, mRotationMatrix, 0); mTriangle.draw(tempMatrix); -
你已经准备好在设备或模拟器上运行应用程序了。
工作原理...
我们使用 Matrix.setRotateM() 方法来根据我们传入的角度计算新的旋转矩阵。在这个例子中,我们使用系统运行时间来计算一个角度。我们可以使用任何我们想要的方法来推导一个角度,比如传感器读数或触摸事件。
还有更多...
使用系统时钟提供了创建连续运动的额外好处,这对于演示目的来说肯定看起来更好。下一个教程将展示如何使用用户输入来导出一个旋转三角形的角。
渲染模式
OpenGL 提供了一个 setRenderMode() 选项,只有在视图变脏时才绘制。通过在 setRenderer() 调用下面的 CustomGLSurfaceView() 构造函数中添加以下代码,可以启用此功能:
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
这将导致显示更新一次,然后等待我们通过 requestRender() 请求更新。
使用用户输入旋转三角形
上一个示例演示了基于系统时钟旋转三角形。这创建了一个根据我们使用的渲染模式连续旋转的三角形。但是,如果你想要响应用户的输入呢?
在这个教程中,我们将通过覆盖 GLSurfaceView 的 onTouchEvent() 回调来展示如何响应用户输入。我们将仍然使用 Matrix.setRotateM() 方法来旋转三角形,但不是从系统时间导出角度,而是根据触摸位置计算角度。
这是一张在物理设备上运行此食谱的截图(为了突出触摸,启用了显示触摸的开发者选项):
准备就绪
在 Android Studio 中创建一个新项目,并将其命名为RotateWithUserInput。使用默认的手机 & 平板选项,并在提示活动类型时选择空活动。
本食谱展示了与上一个食谱不同的方法,因此将基于绘制时应用投影和摄像机视图(与上一个食谱相同的起点)。
如何操作...
如前所述,我们将继续从绘制时应用投影和摄像机视图的食谱开始,而不是从上一个食谱。打开MainActivity.java并按照以下步骤操作:
-
在
MainActivity类中添加以下全局变量:private float mCenterX=0; private float mCenterY=0; -
在
GLRendered类中添加以下代码:private float[] mRotationMatrix = new float[16]; public volatile float mAngle; public void setAngle(float angle) { mAngle = angle; } -
在同一类中,通过替换现有的
mTriangle.draw(mMVPMatrix);语句,修改onDrawFrame()方法,使用以下代码:float[] tempMatrix = new float[16]; Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f); Matrix.multiplyMM(tempMatrix, 0, mMVPMatrix, 0, mRotationMatrix, 0); mTriangle.draw(tempMatrix); -
在
onSurfaceChanged()回调中添加以下代码:mCenterX=width/2; mCenterY=height/2; -
在
CustomGLSurfaceView构造函数中添加以下代码,位于setRenderer()下方:setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); -
在
CustomGLSurfaceView类中添加以下onTouchEvent():@Override public boolean onTouchEvent(MotionEvent e) { float x = e.getX(); float y = e.getY(); switch (e.getAction()) { case MotionEvent.ACTION_MOVE: double angleRadians = Math.atan2(y-mCenterY,x-mCenterX); mGLRenderer.setAngle((float)Math.toDegrees(-angleRadians)); requestRender(); } return true; } -
你已经准备好在设备或模拟器上运行应用程序了。
它是如何工作的...
本示例与上一个食谱最明显的区别在于我们如何导出传递给Matrix.setRotateM()调用的角度。我们还通过使用setRenderMode()更改了GLSurfaceView的渲染模式,仅在请求时绘制。在onTouchEvent()回调中计算出新角度后,我们使用requestRender()发出请求。
我们还证明了派生我们自己的GLSurfaceView类的重要性。如果没有我们的CustomGLSurfaceView类,我们将无法重写onTouchEvent回调,也无法重写来自GLSurfaceView的其他任何回调。
还有更多...
这结束了 OpenGL ES 的食谱,但我们只是触及了 OpenGL 的强大功能。如果你认真想要学习 OpenGL,请查看下一节中的链接,并阅读关于 OpenGL 的众多书籍之一。
检查可用的众多框架之一,例如 Unreal Engine,也是值得的:
提示
Unreal Engine 4 是由游戏开发者为游戏开发者制作的一套完整的游戏开发工具。
www.unrealengine.com/what-is-unreal-engine-4
另请参阅
-
OpenGL:高性能图形的行业标准
-
OpenGL ES:嵌入式加速 3D 图形的标准
-
Unreal Engine:Android 快速入门
docs.unrealengine.com/latest/INT/Platforms/Android/GettingStarted/index.html
第十一章:多媒体
在本章中,我们将涵盖以下主题:
-
使用 SoundPool 播放声音效果
-
使用 MediaPlayer 播放音频
-
在您的应用程序中响应用户的硬件媒体控制
-
使用默认相机应用程序拍照
-
使用(旧的)Camera API 拍照
-
使用 Camera2(新的)API 拍照
引言
在前几章中我们已经探讨了图形和动画,现在是我们看看 Android 中可用的声音选项的时候了。播放声音的两个最受欢迎的选项包括:
-
SoundPool:这适用于短声音片段
-
MediaPlayer:这适用于较大的声音文件(如音乐)和视频文件
我们将首先探讨使用这些库的前两个食谱。我们还会看看如何使用与声音相关的硬件,比如音量控制和媒体播放控制(耳机上常有的播放、暂停等)。
本章的其余部分将重点介绍如何使用相机,既通过 Intents 间接使用(将相机请求传递给默认相机应用程序),也直接使用相机 API。我们将探讨随 Android 5.0 Lollipop(API 21)发布的新 Camera2 API,但也会看看原始的 Camera API,因为大约 75%的市场还没有 Lollipop。(为了帮助您利用 Camera2 API 提供的新功能,我们将展示一种使用旧 Camera API 的新方法,以简化在您自己的应用程序中使用这两个 Camera API。)
使用 SoundPool 播放声音效果
当您的应用程序需要声音效果时,SoundPool 通常是一个很好的起点。
SoundPool 很有趣,因为它允许我们通过改变播放速率和允许同时播放多个声音来为我们的声音创建特殊效果。
支持的热门音频文件类型包括:
-
3GPP(
.3gp) -
3GPP(
.3gp) -
FLAC(
.flac) -
MP3(
.mp3) -
MIDI 类型 0 和 1(
.mid、.xmf和.mxmf) -
Ogg(
.ogg) -
WAVE(
.wav)
请查看支持的媒体格式链接以获取完整列表,包括网络协议。
与 Android 中的常见做法一样,操作系统的更新带来了 API 的变化。SoundPool也不例外,原始的SoundPool构造函数在 Lollipop(API 21)中被弃用。我们不会将最小 API 设置为 21,也不会依赖可能随时停止工作的弃用代码,而是实现旧方法和新方法,并在运行时检查操作系统版本以使用适当的方法。
本食谱将演示如何使用 Android 的SoundPool库播放声音效果。为了演示同时播放声音,我们将创建两个按钮,每个按钮按下时都会播放声音。
准备就绪
在 Android Studio 中创建一个新项目,并将其命名为:SoundPool。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
为了演示同时播放声音,我们至少需要在项目中包含两个音频文件。我们访问了 SoundBible.com(免费版权声音),并找到了两个免费版权的公共领域声音,包含在下载项目文件中:
第一个声音是较长的播放声音:
第二个声音较短:
如何操作...
如前所述,我们需要在项目中包含两个音频文件。准备好您的声音文件后,请按照以下步骤操作:
-
创建一个新的 raw 文件夹(文件 | 新建 | Android 资源目录),并在 资源类型 下拉菜单中选择
raw。 -
将你的声音文件复制到
res/raw作为sound_1和sound_2。(保留它们的原始扩展名。) -
打开
activity_main.xml并用以下按钮替换现有的TextView:<Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Play Sound 1" android:id="@+id/button1" android:layout_centerInParent="true" android:onClick="playSound1"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Play Sound 2" android:id="@+id/button2" android:layout_below="@+id/button1" android:layout_centerHorizontal="true" android:onClick="playSound2"/> -
现在,打开
ActivityMain.java并添加以下全局变量:HashMap<Integer, Integer> mHashMap= null; SoundPool mSoundPool; -
修改现有的
onCreate()方法,如下所示:final Button button1=(Button)findViewById(R.id.button1); button1.setEnabled(false); final Button button2=(Button)findViewById(R.id.button2); button2.setEnabled(false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { createSoundPoolNew(); }else{ createSoundPooolOld(); } mSoundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() { @Override public void onLoadComplete(SoundPool soundPool, int sampleId, int status) { button1.setEnabled(true); button2.setEnabled(true); } }); mHashMap = new HashMap<>(); mHashMap.put(1, mSoundPool.load(this, R.raw.sound_1, 1)); mHashMap.put(2, mSoundPool.load(this, R.raw.sound_2, 1)); -
添加
createSoundPoolNew()方法:@TargetApi(Build.VERSION_CODES.LOLLIPOP) private void createSoundPoolNew() { AudioAttributes audioAttributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .build(); mSoundPool = new SoundPool.Builder() .setAudioAttributes(audioAttributes) .setMaxStreams(2) .build(); } -
添加
createSoundPooolOld()方法:@SuppressWarnings("deprecation") private void createSoundPooolOld(){ mSoundPool = new SoundPool(2, AudioManager.STREAM_MUSIC, 0); } -
添加按钮
onClick()方法:public void playSound1(View view){ mSoundPool.play(mHashMap.get(1), 0.1f, 0.1f, 1, 0, 1.0f); } public void playSound2(View view){ mSoundPool.play(mHashMap.get(2), 0.9f, 0.9f, 1, 1, 1.0f); } -
按如下方式重写
onStop()回调:protected void onStop() { super.onStop(); mSoundPool.release(); } -
在设备或模拟器上运行应用程序。
工作原理...
首先要注意的是我们如何构建这个对象本身。正如我们在引言中提到的,SoundPool 构造函数在 Lollipop(API 21)中有所改变。旧的构造函数已弃用,推荐使用 SoundPool.Builder()。在像 Android 这样不断变化的环境中,API 的变化是很常见的,因此学习如何应对这些变化是个好主意。如您所见,在这个案例中,这并不困难。我们只需检查当前的操作系统版本,并调用相应的方法。值得注意的是方法注解:
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
以及:
@SuppressWarnings("deprecation")
创建 SoundPool 后,我们设置了一个 setOnLoadCompleteListener() 监听器。启用按钮主要是为了演示 SoundPool 需要在声音资源可用之前加载它们。
使用 SoundPool 的最后一点是调用 play()。我们需要传入 soundID,这是我们在使用 load() 加载声音时返回的。play() 为我们提供了一些选项,包括声音音量(左右)、循环次数和播放速率。为了演示其灵活性,我们以较低的音量播放第一个声音(较长),以产生类似流水背景的效果。第二个声音以较高的音量播放,并且我们播放两次。
还有更多...
如果你只需要一个基本的声音效果,比如点击声,你可以使用 AudioManager 的 playSoundEffect() 方法。以下是一个示例:
AudioManager audioManager =(AudioManager)
this.getSystemService(Context.AUDIO_SERVICE);
audioManager.playSoundEffect(SoundEffectConstants.CLICK);
你只能从 SoundEffectConstants 指定一个声音;你不能使用自己的声音文件。
另请参阅
-
开发者文档:SoundPool
-
开发者文档:
使用 MediaPlayer 播放音频
MediaPlayer 是为您的应用程序添加多媒体功能最重要的类之一。它支持以下媒体源:
-
项目资源
-
本地文件
-
外部资源(例如 URL,包括流媒体)
MediaPlayer 支持以下流行的音频文件:
-
3GPP (
.3gp) -
3GPP (
.3gp) -
FLAC (
.flac) -
MP3 (
.mp3) -
MIDI 类型 0 和 1 (
.mid,.xmf, 和.mxmf) -
Ogg (
.ogg) -
WAVE (
.wav)
以及以下流行的文件类型:
-
3GPP (
.3gp) -
Matroska (
.mkv) -
WebM (
.webm) -
MPEG-4 (
.mp4,.m4a)
查看支持的媒体格式链接以获取完整列表,包括网络协议。
本示例将演示如何在您的应用中设置 MediaPlayer 以播放项目中的声音。(要全面了解 MediaPlayer 提供的全部功能,请查看本示例末尾的开发者文档链接。)
准备工作
在 Android Studio 中创建一个新项目,命名为 MediaPlayer。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
我们这个示例还需要一个声音文件,将使用上一个示例中的相同长音效“水声”。
第一个声音是一个较长的音效:水声
如何操作...
如前所述,我们需要在项目中包含一个声音文件。准备好声音文件后,请按照以下步骤操作:
-
创建一个新的原始资源文件夹(文件 | 新建 | Android 资源目录),并在 资源类型 下拉菜单中选择
raw -
将您的声音文件复制到
res/raw目录下,命名为sound_1。(保留原始扩展名。) -
打开
activity_main.xml文件,将现有的TextView替换为以下按钮:<Button android:layout_width="100dp" android:layout_height="wrap_content" android:text="Play" android:id="@+id/buttonPlay" android:layout_above="@+id/buttonPause" android:layout_centerHorizontal="true" android:onClick="buttonPlay" /> <Button android:layout_width="100dp" android:layout_height="wrap_content" android:text="Pause" android:id="@+id/buttonPause" android:layout_centerInParent="true" android:onClick="buttonPause"/> <Button android:layout_width="100dp" android:layout_height="wrap_content" android:text="Stop" android:id="@+id/buttonStop" android:layout_below="@+id/buttonPause" android:layout_centerHorizontal="true" android:onClick="buttonStop"/> -
现在,打开
ActivityMain.java文件,并添加以下全局变量:MediaPlayer mMediaPlayer; -
添加
buttonPlay()方法:public void buttonPlay(View view){ if (mMediaPlayer==null) { mMediaPlayer = MediaPlayer.create(this, R.raw.sound_1); mMediaPlayer.setLooping(true); mMediaPlayer.start(); } else { mMediaPlayer.start(); } } -
添加
buttonPause()方法:public void buttonPause(View view){ if (mMediaPlayer!=null && mMediaPlayer.isPlaying()) { mMediaPlayer.pause(); } } -
添加
buttonStop()方法:public void buttonStop(View view){ if (mMediaPlayer!=null) { mMediaPlayer.stop(); mMediaPlayer.release(); mMediaPlayer = null; } } -
最后,用以下代码重写
onStop()回调方法:protected void onStop() { super.onStop(); if (mMediaPlayer!=null) { mMediaPlayer.release(); mMediaPlayer = null; } } -
现在,您可以在设备或模拟器上运行应用程序了。
工作原理...
这里的代码非常直观。我们创建一个带有声音的 MediaPlayer 并开始播放声音。按钮将相应地重新播放、暂停和停止。
即使这个基本示例也说明了关于 MediaPlayer 的一个非常重要的概念,那就是 状态。如果您要严肃使用 MediaPlayer,请查看下面提供的链接以获取详细信息。
还有更多...
为了让我们的演示更容易理解,我们使用 UI 线程进行所有操作。对于这个例子,我们使用项目中包含的短音频文件,不太可能导致 UI 延迟。通常,在准备 MediaPlayer 时使用后台线程是一个好主意。为了使这个常见任务更容易,MediaPlayer 已经包含了一个名为prepareAsync()的异步准备方法。以下代码将创建一个OnPreparedListener()监听器,并使用prepareAsync()方法:
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mMediaPlayer.start();
}
});
try {
mMediaPlayer.setDataSource(*//*URI, URL or path here*//*));
} catch (IOException e) {
e.printStackTrace();
}
mMediaPlayer.prepareAsync();
在后台播放音乐
我们的示例旨在应用程序在前台时播放音频,并在onStop()回调中释放 MediaPlayer 资源。如果你正在创建一个音乐播放器,并希望在其他应用程序使用时也能在后台播放音乐,该怎么办呢?在这种情况下,你需要在服务中使用 MediaPlayer,而不是 Activity。你仍然会以同样的方式使用 MediaPlayer 库;你只需要将从 UI 传递信息(如声音选择)到你的服务。
注意
请注意,由于服务与活动在同一个 UI 线程中运行,你仍然不希望在服务中执行可能阻塞的操作。MediaPlayer 确实处理后台线程以防止阻塞你的 UI 线程,否则,你需要自己执行线程操作。(有关线程和选项的更多信息,请参见第十四章,让你的应用准备好上架 Play 商店。)
使用硬件音量键控制你的应用的音频音量
如果你希望音量控制能控制你应用中的音量,请使用setVolumeControlStream()方法来指定应用程序的音频流,如下所示:
setVolumeControlStream(AudioManager.STREAM_MUSIC);
有关其他流选项,请参见以下AudioManager链接。
另请参阅
-
支持的媒体格式
developer.android.com/guide/appendix/media-formats.html -
开发者文档:MediaPlayer
developer.android.com/reference/android/media/MediaPlayer.html -
开发者文档:音频管理器
developer.android.com/reference/android/media/AudioManager.html
在你的应用中响应硬件媒体控制
让你的应用响应用户的媒体控制,如播放、暂停、跳过等,是一个用户会非常欣赏的贴心功能。
安卓通过媒体库使这成为可能。与之前使用 SoundPool 播放声音效果的食谱一样,Lollipop 版本改变了解决这个问题的方式。与SoundPool示例不同,这个食谱能够利用另一种方法——兼容性库。
本示例将展示如何设置 MediaSession 以响应硬件按钮,这将适用于 Lollipop 及以上版本,以及使用 MediaSessionCompat 库的早期 Lollilop 版本。(兼容性库将自动处理检查操作系统版本并使用正确的 API 调用。)
准备工作。
在 Android Studio 中创建一个新项目,并将其命名为 HardwareMediaControls。使用默认的 Phone & Tablet 选项,并在提示选择 Activity Type 时选择 Empty Activity。
如何操作...
我们将仅使用 Toast 消息来响应硬件事件,因此无需对活动布局进行任何更改。要开始,请打开 ActivityMain.java 并按照以下步骤操作:
-
创建以下
mMediaSessionCallback以响应媒体按钮:MediaSessionCompat.Callback mMediaSessionCallback = new MediaSessionCompat.Callback() { @Override public void onPlay() { super.onPlay(); Toast.makeText(MainActivity.this, "onPlay()", Toast.LENGTH_SHORT).show(); } @Override public void onPause() { super.onPause(); Toast.makeText(MainActivity.this, "onPause()", Toast.LENGTH_SHORT).show(); } @Override public void onSkipToNext() { super.onSkipToNext(); Toast.makeText(MainActivity.this, "onSkipToNext()", Toast.LENGTH_SHORT).show(); } @Override public void onSkipToPrevious() { super.onSkipToPrevious(); Toast.makeText(MainActivity.this, "onSkipToPrevious()", Toast.LENGTH_SHORT).show(); } }; -
在现有的
onCreate()回调中添加以下代码:MediaSessionCompat mediaSession = new MediaSessionCompat(this, getApplication().getPackageName()); mediaSession.setCallback(mMediaSessionCallback); mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS); mediaSession.setActive(true); PlaybackStateCompat state = new PlaybackStateCompat.Builder() .setActions( PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS).build(); mediaSession.setPlaybackState(state); -
在带有媒体控制功能(如耳机)的设备或模拟器上运行应用程序,以查看 Toast 消息。
工作原理...
设置此功能共有四个步骤:
-
创建一个
MediaSession.Callback并将其附加到 MediaSession。 -
设置 MediaSession 标志,以表示我们希望使用媒体按钮。
-
将
SessionState设置为active。 -
使用我们将要处理的操作来设置
PlayBackState。
步骤 4 和步骤 1 一起工作,因为回调只会接收到在 PlayBackState 中设置的事件。
由于在本示例中我们实际上并未控制任何播放,只是演示如何响应硬件事件。你需要在 PlayBackState 中实现实际功能,并在 setActions() 调用后包含一个 setState() 的调用。
这是一个很好的示例,展示了 API 的变化如何使事情变得更容易。由于新的 MediaSession 和 PlaybackState 被整合到兼容性库中,我们可以在旧版本的操作系统上利用这些新的 API。
还有更多内容...
检查正在使用的硬件。
如果你想根据当前的输出硬件让应用有不同的响应,可以使用 AudioManager 来检查。以下是一个示例:
AudioManager audioManager =(AudioManager) this.getSystemService(Context.AUDIO_SERVICE);
if (audioManager.isBluetoothA2dpOn()) {
// Adjust output for Bluetooth.
} else if (audioManager.isSpeakerphoneOn()) {
// Adjust output for Speakerphone.
} else if (audioManager.isWiredHeadsetOn()) {
//Only checks if a wired headset is plugged in
//May not be the audio output
} else {
// Regular speakers?
}
另请参阅
-
开发者文档:MediaSession
developer.android.com/reference/android/media/session/MediaSession.html -
开发者文档:MediaSessionCompat
developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat.html -
开发者文档:PlaybackState
developer.android.com/reference/android/support/v4/media/session/PlaybackStateCompat.html -
开发者文档:PlaybackStateCompat
developer.android.com/reference/android/support/v4/media/session/PlaybackStateCompat.html
使用默认相机应用程序拍照
如果你的应用程序需要来自相机的图像,但不是相机的替代应用,那么允许“默认”相机应用拍照可能更好。这也尊重用户选择的首选相机应用程序。
当你拍照时,除非它仅适用于你的应用程序,否则最好将照片公开。 (这允许它包含在用户的照片库中。)这个方法将演示如何使用默认的照片应用程序拍照,将其保存到公共文件夹,并显示图像。
准备就绪
在 Android Studio 中创建一个新项目,并将其命名为:UsingTheDefaultCameraApp。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何操作...
我们将创建一个带有 ImageView 和按钮的布局。按钮将创建一个 Intent 来启动默认的相机应用。当相机应用完成时,我们的应用将得到一个回调。首先打开 Android Manifest 并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> -
打开
activity_main.xml文件,将现有的TextView替换为以下视图:<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/imageView" android:src="img/ic_launcher" android:layout_centerInParent="true"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Take Picture" android:id="@+id/button" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="takePicture"/> -
打开
MainActivity.java并将以下全局变量添加到MainActivity类中:final int PHOTO_RESULT=1; private Uri mLastPhotoURI=null; -
添加以下方法来创建照片的 URI:
private Uri createFileURI() { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(System.currentTimeMillis()); String fileName = "PHOTO_" + timeStamp + ".jpg"; return Uri.fromFile(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),fileName)); } -
添加以下方法来处理按钮点击:
public void takePicture(View view) { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_ CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { mLastPhotoURI = createFileURI(); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mLastPhotoURI); startActivityForResult(takePictureIntent, PHOTO_RESULT); } } -
添加一个新的方法来重写
onActivityResult(),如下所示:@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == PHOTO_RESULT && resultCode == RESULT_OK ) { mImageView.setImageBitmap(BitmapFactory.decodeFile(mLastPhotoURI.getPath())); } } -
你可以准备在设备或模拟器上运行应用程序了。
它是如何工作的...
使用默认相机应用程序有两个部分。第一部分是设置意图来启动应用程序。我们使用 MediaStore.ACTION_IMAGE_CAPTURE 创建 Intent,表示我们想要一个拍照应用。我们通过检查 resolveActivity() 的结果来验证默认应用是否存在。只要它不是 null,我们就知道有一个应用程序可以处理这个意图。(否则,我们的应用会崩溃。)我们创建一个文件名,并将其添加到意图中:putExtra(MediaStore.EXTRA_OUTPUT, mLastPhotoURI)。
当我们在 onActivityResult() 中得到回调时,我们首先确保它是 PHOTO_RESULT 和 RESULT_OK(用户可能已取消),然后在 ImageView 中加载照片。
还有更多...
如果你不在意图片存储在哪里,可以在不使用 MediaStore.EXTRA_OUTPUT 额外参数的情况下调用意图。如果你没有指定输出文件,onActivityResult() 将在 data Intent 中包含图像的缩略图。以下是如何显示缩略图的方法:
if (data != null) {
imageView
.setImageBitmap((Bitmap) data.getExtras().get("data"));
}
以下是使用 data Intent 返回的 URI 加载全分辨率图像的代码:
if (data != null) {
try {
imageView.setImageBitmap(
MediaStore.Images.Media. getBitmap(getContentResolver(),
Uri.parse(data.toUri(Intent.URI_ALLOW_UNSAFE))));
} catch (IOException e) {
e.printStackTrace();
}
}
调用默认视频应用
如果你想要调用默认的视频捕捉应用程序,过程是相同的。只需在步骤 5 中更改意图,如下所示:
Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
你可以在 onActivityResult() 中获取到视频的 URI,如下所示:
Uri videoUri = intent.getData();
另请参阅
- 第九章《图形和动画》中的缩小大图像以避免内存溢出异常食谱。
使用(旧的)Camera API 拍照
之前的食谱演示了如何使用意图调用默认照片应用程序。如果你只需要快速拍照,意图可能是理想的解决方案。如果不是,并且你需要更多控制相机,这个食谱将向你展示如何直接使用 Camera API。
实际上有两个使用 Camera API 的食谱——一个是针对在 Android 1.0(API 1)中发布的原始 Camera API,另一个是 Camera2 API,在 Android 5.0(API 21)中发布。我们将介绍新旧 API。理想情况下,你会希望根据可用的最新和最伟大的 API 编写应用程序,但在撰写本文时,Android 5.0(API 21)的市场份额只有大约 23%。如果你只使用 Camera2 API,你会排除超过 75%的市场。
编写你的应用程序以使用 Camera2 API 利用新功能,但对于其余用户仍然可以使用原来的 Camera API 实现功能性的应用。为了帮助同时使用两者,本食谱将利用 Android 的新功能,特别是从 Android 4.0(API 14)引入的TextureView。我们将使用TextureView代替更传统的SurfaceView来显示相机预览。这将允许你使用与新 Camera2 API 相同的布局,因为它也使用TextureView。(将最低 API 设置为 Android 4.0(API 14)及以上,其市场份额超过 96%,对你的用户群限制不大。)
准备就绪
在 Android Studio 中创建一个新项目,并将其命名为CameraAPI。在目标 Android 设备对话框中,选择手机 & 平板电脑选项,并为最低 SDK选择 API 14(或更高)。当提示选择活动类型时,选择空活动。
如何操作...
首先,打开 Android 清单文件并按照以下步骤操作:
-
添加以下两个权限:
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> -
现在打开
activity_main.xml文件,并用以下视图替换现有的 TextView:<TextureView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/textureView" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Take Picture" android:id="@+id/button" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="takePicture"/> -
打开
MainActivity.java文件,并将MainActivity类声明修改为实现SurfaceTextureListener,如下所示:public class MainActivity extends AppCompatActivity implements TextureView.SurfaceTextureListener { -
向
MainActivity添加以下全局声明:@Deprecated private Camera mCamera; private TextureView mTextureView; -
创建以下
PictureCallback以处理保存照片:Camera.PictureCallback pictureCallback = new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { try { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(System.currentTimeMillis()); String fileName = "PHOTO_" + timeStamp + ".jpg"; File pictureFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),fileName); FileOutputStream fileOutputStream =new FileOutputStream(pictureFile.getPath()); fileOutputStream.write(data); fileOutputStream.close(); Toast.makeText(MainActivity.this, "Picture Taken", Toast.LENGTH_SHORT).show(); } catch (Exception e) { e.printStackTrace(); } } }; -
向现有的
onCreate()回调添加以下代码:mTextureView = (TextureView)findViewById(R.id.textureView); mTextureView.setSurfaceTextureListener(this); -
添加以下方法来实现
SurfaceTextureListener接口:public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { mCamera = Camera.open(); if (mCamera!=null) { try { mCamera.setPreviewTexture(surface); mCamera.startPreview(); } catch (IOException e) { e.printStackTrace(); } } } public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { if (mCamera!=null) { mCamera.stopPreview(); mCamera.release(); } return true; } public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { // Unused } public void onSurfaceTextureUpdated(SurfaceTexture surface) { // Unused } -
添加以下方法来处理按钮点击:
public void takePicture(View view) { if (mCamera!=null) { mCamera.takePicture(null, null, pictureCallback); } } -
在带有相机的设备或模拟器上运行应用程序。
工作原理...
首先要注意的是,在 Android Studio 中查看这段代码时,你会看到很多带有以下警告的删除线代码:
'android.hardware.Camera' is deprecated
如引言所述,android.hardware.camera2 API 在 Android 5.0(API 19)中引入,并替代了android.hardware.camera API。
提示
你可以添加以下注解来抑制弃用警告:
@SuppressWarnings("deprecation")
使用 Camera API 时有两个主要步骤:
-
设置预览
-
捕获图像
我们从布局中获取TextureView,然后使用以下代码将我们的活动(实现了SurfaceTextureListener)作为监听器:
mTextureView.setSurfaceTextureListener(this);
当TextureView的表面准备就绪时,我们会收到onSurfaceTextureAvailable回调,在那里我们使用以下代码设置预览表面:
mCamera.setPreviewTexture(surface);
mCamera.startPreview();
下一步是在按下按钮时拍照。我们使用以下代码实现:
mCamera.takePicture(null, null, pictureCallback);
当图片准备好时,我们在创建的Camera.PictureCallback类中收到onPictureTaken()回调。
还有更多...
请记住,这段代码是为了展示其工作原理,并非用于创建完整的商业应用程序。正如大多数开发者所知,编码中的真正挑战在于处理所有的问题场景。改进的一些方面包括增加切换摄像头的功能,因为当前应用使用的是默认摄像头。同时,也要查看设备在预览和保存图片时的方向。更复杂的应用程序会在后台线程处理一些工作,以避免 UI 线程的延迟。(查看下一个食谱,了解我们如何在后台线程上处理一些摄像头处理工作。)
设置摄像头参数
Camera API 包括参数,使我们能够调整摄像头设置。通过这个例子,我们可以更改预览的大小:
Camera.Parameters parameters = mCamera.getParameters();
parameters.setPreviewSize(mPreviewSize.width,
mPreviewSize.height);
mCamera.setParameters(parameters);
请记住,硬件也必须支持我们想要的设置。在这个例子中,我们首先需要查询硬件以获取所有可用的预览模式,然后设置符合我们要求的模式。(在下一个食谱中设置图片分辨率时,可以看到一个这样的例子。)请参阅 Camera 文档链接中的getParameters()。
另请参阅
-
下一个食谱:使用 Camera2(新)API 拍照
-
在第八章,使用触摸屏和传感器中的读取设备方向食谱,了解检测当前设备方向的示例
-
开发者文档:构建摄像头应用程序位于:
developer.android.com/guide/topics/media/camera.html#custom-camera -
developer.android.com/reference/android/hardware/Camera.html
使用 Camera2(新)API 拍照
现在我们已经了解了旧的 Camera API,是时候学习新的 Camera2 API 了。不幸的是,由于 API 的异步性质,它有点复杂。幸运的是,总体概念与之前的 Camera API 相同。
准备就绪
在 Android Studio 中创建一个新项目,命名为Camera2API。在Target Android Devices对话框中,选择Phone & Tablet选项,并将Minimum SDK设置为 API 21(或更高)。当提示选择Activity Type时,选择Empty Activity。
如何操作...
你会看到,这个配方有很多代码。首先打开 Android Manifest 文件,并按照以下步骤操作:
-
添加以下两个权限:
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> -
现在,打开
activity_main.xml文件,用以下视图替换现有的 TextView:<TextureView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/textureView" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Take Picture" android:id="@+id/button" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="takePicture"/> -
现在,打开
MainActivity.java文件,并在MainActivity类中添加以下全局变量:private CameraDevice mCameraDevice = null; private CaptureRequest.Builder mCaptureRequestBuilder = null; private CameraCaptureSession mCameraCaptureSession = null; private TextureView mTextureView = null; private Size mPreviewSize = null; -
添加以下
Comparator类:static class CompareSizesByArea implements Comparator<Size> { @Override public int compare(Size lhs, Size rhs) { return Long.signum((long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); } } -
添加以下
CameraDevice.StateCallback:private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice camera) { mCameraDevice = camera; SurfaceTexture texture = mTextureView.getSurfaceTexture(); if (texture == null) { return; } texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); Surface surface = new Surface(texture); try { mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); } catch (CameraAccessException e){ e.printStackTrace(); } mCaptureRequestBuilder.addTarget(surface); try { mCameraDevice.createCaptureSession(Arrays.asList(surface), mPreviewStateCallback, null); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onError(CameraDevice camera, int error) {} @Override public void onDisconnected(CameraDevice camera) {} }; -
添加以下
SurfaceTextureListener:private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) {} @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {} @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return false; } @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { openCamera(); } }; -
添加以下
CameraCaptureSession.StateCallback:private CameraCaptureSession.StateCallback mPreviewStateCallback = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession session) { startPreview(session); } @Override public void onConfigureFailed(CameraCaptureSession session) {} }; -
在现有的
onCreate()回调中添加以下代码:mTextureView = (TextureView) findViewById(R.id.textureView); mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); -
添加以下方法以覆盖
onPause()和onResume():@Override protected void onPause() { super.onPause(); if (mCameraDevice != null) { mCameraDevice.close(); mCameraDevice = null; } } @Override public void onResume() { super.onResume(); if (mTextureView.isAvailable()) { openCamera(); } else { mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); } } -
添加
openCamera()方法:private void openCamera() { CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE); try{ String cameraId = manager.getCameraIdList()[0]; CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); mPreviewSize = map.getOutputSizes(SurfaceTexture.class) [0]; manager.openCamera(cameraId, mStateCallback, null); } catch(CameraAccessException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } } -
添加
startPreview()方法:private void startPreview(CameraCaptureSession session) { mCameraCaptureSession = session; mCaptureRequestBuilder.set(CaptureRequest.CONTROL_MODE,CameraMetadata.CONTROL_MODE_AUTO); HandlerThread backgroundThread = new HandlerThread("CameraPreview"); backgroundThread.start(); Handler backgroundHandler = new Handler(backgroundThread. getLooper()); try { mCameraCaptureSession.setRepeatingRequest(mCaptureRequestBuilder.build(), null, backgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } -
添加
getPictureFile()方法:private File getPictureFile() { String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss"). format(System.currentTimeMillis()); String fileName = "PHOTO_" + timeStamp + ".jpg"; return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),fileName); } -
添加
takePicture()方法,该方法保存图像文件:protected void takePicture(View view) { if (null == mCameraDevice) { return; } CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { CameraCharacteristics characteristics = manager.getCameraCharacteristics(mCameraDevice.getId()); StreamConfigurationMap configurationMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); if (configurationMap == null) return; Size largest = Collections.max( Arrays.asList(configurationMap.getOutputSizes(ImageFormat.JPEG)), new CompareSizesByArea()); ImageReader reader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(), ImageFormat.JPEG, 1); List < Surface > outputSurfaces = new ArrayList < Surface > (2); outputSurfaces.add(reader.getSurface()); outputSurfaces.add(new Surface(mTextureView.getSurfaceTexture())); final CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_ CAPTURE); captureBuilder.addTarget(reader.getSurface()); captureBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); ImageReader.OnImageAvailableListener readerListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { Image image = null; try { image = reader.acquireLatestImage(); ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.capacity()]; buffer.get(bytes); OutputStream output = new FileOutputStream( get PictureFile()); output.write(bytes); output.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (image != null) { image.close(); } } } }; HandlerThread thread = new HandlerThread("CameraPicture"); thread.start(); final Handler backgroudHandler = new Handler(thread.getLooper()); reader.setOnImageAvailableListener(readerListener, backgroudHandler); final CameraCaptureSession.CaptureCallback captureCallback = new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted( CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { super.onCaptureCompleted(session, request, result); Toast.makeText(MainActivity.this, "Picture Saved", Toast.LENGTH_SHORT).show(); startPreview(session); } }; mCameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() { @Override public vod onConfigured(CameraCaptureSession session) { try { session.capture(captureBuilder.build(), captureCallback, backgroudHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } @Override public void onConfigureFailed(CameraCaptureSession session) { } }, backgroudHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } -
在带有摄像头的设备或模拟器上运行应用程序。
工作原理...
由于我们在上一个配方中了解了 TextureView,我们可以跳转到新的 Camera2 API 信息。
尽管涉及更多类,但与旧的 Camera API 一样,有两个基本步骤:
-
设置预览
-
捕获图像
设置预览
下面是代码如何设置预览的概要:
-
首先,我们在
onCreate()中使用setSurfaceTextureListener()方法设置TextureView.SurfaceTextureListener。 -
当我们收到
onSurfaceTextureAvailable()回调时,我们打开相机。 -
我们将我们的
CameraDevice.StateCallback类传递给openCamera()方法,该方法最终调用onOpened()回调。 -
onOpened()通过调用getSurfaceTexture()获取预览的表面,并通过调用createCaptureSession()将其传递给 CameraDevice。 -
最后,当调用
CameraCaptureSession.StateCallback onConfigured()时,我们使用setRepeatingRequest()方法开始预览。
捕获图像
尽管takePicture()方法看起来可能是程序性的,但捕获图像也涉及到几个类,并且依赖于回调。以下是代码如何拍照的分解说明:
-
用户点击拍照按钮。
-
然后查询相机以找到最大的可用图像尺寸。
-
然后,创建一个
ImageReader。 -
然后,设置
OnImageAvailableListener,并在onImageAvailable()回调中保存图像。 -
然后,创建
CaptureRequest.Builder并包含ImageReader表面。 -
接下来,创建
CameraCaptureSession.CaptureCallback,它定义了onCaptureCompleted()回调。当捕获完成时,它会重新启动预览。 -
然后,调用
createCaptureSession()方法,创建一个CameraCaptureSession.StateCallback。在这里,会调用capture()方法,并传入之前创建的CameraCaptureSession.CaptureCallback。
还有更多...
与之前的 Camera 示例一样,我们刚刚创建了基础代码以展示一个工作的摄像头应用程序。同样,还有改进的空间。首先,你应该处理设备方向,既要考虑预览,也要在保存图片时考虑。(请参阅上一个食谱中的链接。)另外,随着 Android 6.0(API 23)的推出,现在是一个很好的时机来开始使用新的权限模型。我们不应该只在openCamera()方法中检查异常,而应该检查所需的权限。
另请参阅
-
上一个食谱:使用(旧的)Camera API 拍照
-
新的 Android 6.0 运行时权限模型,在第十四章*,让你的应用准备好上架 Play 商店*中介绍。
-
开发者文档:Camera2 API
-
developer.android.com/reference/android/hardware/camera2/package-summary.html
第十二章:电信、网络与互联网
本章将涵盖以下主题:
-
如何拨打电话
-
监控电话通话事件
-
如何发送短信(文本消息)
-
接收短信
-
在你的应用中显示网页
-
检查在线状态和连接类型
-
Volley 网络请求入门
-
取消 Volley 请求
-
使用 Volley 请求 JSON 响应
-
使用 Volley 请求图像
-
使用 Volley 的 NetworkImageView 和 ImageLoader
引言
我们将通过《如何拨打电话》一节开始本章,了解电话功能。探索了如何拨打电话之后,我们将通过《监控电话通话事件》了解如何监控电话通话。然后,我们将通过《如何发送短信》转到短信通讯,接着通过《接收短信》介绍接收短信。
我们将探索WebView以向应用添加浏览器功能。在基本层面上,WebView是一个基本的 HTML 查看器。我们将展示如何扩展WebViewClient类并通过WebSettings修改设置以创建完整的浏览器功能,包括 JavaScript 和缩放功能。
本章剩余部分将介绍 Volley,这是通过 AOSP 提供的一个新库。《Volley 网络请求入门》一节将提供一些关于 Android 上可用的在线库的背景信息,并讨论为何创建 Volley。它还提供了将 Volley 添加到你的 Android Studio 项目的完整演练。
如何拨打电话
如我们在之前的食谱中所见,只需使用 Intent 即可调用默认应用。为了拨打电话,创建 Intent 时使用Intent.ACTION_DIAL。你可以使用setData()方法包含一个电话号码。以下是调用拨号应用并指定电话号码的示例代码:
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:" + number));
startActivity(intent);
由于你的应用不执行拨号操作,且用户必须按下拨号按钮,因此你的应用无需任何拨号权限。以下步骤将向你展示如何直接拨打电话,绕过Dial活动。(为此,你需要添加一个权限。)
准备就绪
在 Android Studio 中创建一个新项目,将其命名为DialPhone。选择默认的电话 & 平板选项,并在提示活动类型时选择空活动。
如何操作...
首先,我们需要添加适当的权限来拨打电话。然后,我们需要添加一个按钮来调用我们的Dial方法。从打开 Android Manifest 开始,按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.CALL_PHONE"></uses-permission> -
打开
activity_main.xml,用以下按钮替换现有的TextView:<Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Dial" android:layout_centerInParent="true" android:onClick="dialPhone"/> -
添加此方法,检查你的应用是否被授予了
CALL_PHONE权限:private boolean checkPermission(String permission) { int permissionCheck = ContextCompat.checkSelfPermission( this, permission); return (permissionCheck == PackageManager.PERMISSION_GRANTED); } -
添加拨号的代码:
public void dialPhone(View view){ if (checkPermission("android.permission.CALL_PHONE")) { Intent intent = new Intent(Intent.ACTION_CALL); intent.setData(Uri.parse("tel:0123456789")); startActivity(intent); } } -
在你的设备上运行此操作之前,请确保将
0123456789替换为有效的电话号码。
工作原理...
正如我们在引言中的代码所看到的,当调用默认拨号应用时,我们不需要任何权限。但如果我们想直接拨号,我们需要添加CALL_PHONE权限。从 Android 6.0 Marshmallow(API 23)开始,权限不再在安装时授予,因此,在尝试拨号之前,我们会检查应用是否拥有权限。
参见
- 想获取更多信息,请参阅第十四章中的新的运行时权限模型食谱,准备应用上架 Play 商店。
监控电话呼叫事件
在上一个食谱中,我们演示了如何拨打电话,既通过意图调用默认应用程序,也可以直接拨号,无需 UI。
如果你希望在通话结束时得到通知,这就有点复杂了,因为你需要监听电话事件并跟踪电话状态。在本食谱中,我们将演示如何创建一个PhoneStateListener来读取电话状态事件。
准备就绪
在 Android Studio 中创建一个新项目,命名为PhoneStateListener。选择默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
尽管这不是必须的,但你可以使用上一个食谱来发起电话呼叫以查看事件。否则,使用默认拨号盘和/或观察来电事件。(下载文件中提供的示例代码包括上一个食谱,以便更容易查看事件。)
如何操作...
我们只需要在布局中添加一个TextView来显示事件信息。无论你是接着上一个食谱继续操作,还是开始一个新食谱,打开activity_main.xml文件,并按照以下步骤操作:
-
按照以下方式添加或修改
TextView:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" /> -
向 Android Manifest 中添加以下权限:
<uses-permission android:name="android.permission.READ_PHONE_STATE"> </uses-permission> -
打开
MainActivity.java,并在MainActivity类中添加以下PhoneStateListener类:PhoneStateListener mPhoneStateListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String number) { String phoneState = number; switch (state) { case TelephonyManager.CALL_STATE_IDLE: phoneState += "CALL_STATE_IDLE\n"; case TelephonyManager.CALL_STATE_RINGING: phoneState += "CALL_STATE_RINGING\n"; case TelephonyManager.CALL_STATE_OFFHOOK: phoneState += "CALL_STATE_OFFHOOK\n"; } TextView textView = (TextView)findViewById(R.id.textView); textView.append(phoneState); } }; -
修改
onCreate()以设置监听器:final TelephonyManager telephonyManager = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE); telephonyManager.listen(mPhoneStateListener,PhoneStateListener.LISTEN_CALL_STATE); -
在设备上运行应用程序,并发起和/或接收电话以查看事件。
工作原理...
为了演示如何使用监听器,我们在onCreate()中创建 Telephony 监听器,用这两行代码:
final TelephonyManager telephonyManager = (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
telephonyManager.listen(mPhoneStateListener,PhoneStateListener.LISTEN_CALL_STATE);
当PhoneState事件发生时,它会被发送到我们的PhoneStateListener类。
还有更多...
在这个食谱中,我们监听通话状态事件,用这个常量表示:LISTEN_CALL_STATE。其他有趣的选项包括以下内容:
-
LISTEN_CALL_FORWARDING_INDICATOR -
LISTEN_DATA_CONNECTION_STATE -
LISTEN_SIGNAL_STRENGTHS
查看以下PhoneStateListener链接以获取完整列表。
当我们完成事件监听时,调用listen()方法,并传递LISTEN_NONE,如下所示:
telephonyManager.listen(mPhoneStateListener,PhoneStateListener.LISTEN_NONE);
参见
- 开发者文档:PhoneStateListener在
developer.android.com/reference/android/telephony/PhoneStateListener.html
如何发送短信(文本消息)
由于你可能已经熟悉短信(或文本消息),我们不会花时间解释它们是什么或为什么重要。(如果你不熟悉短信或想了解更多信息,请查看本示例中另请参阅部分提供的链接。)本示例将演示如何发送短信。(下一个示例将演示如何接收新消息的通知以及如何读取现有消息。)
准备就绪
在 Android Studio 中创建一个新项目,命名为SendSMS。选择默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
如何操作...
首先,我们将添加发送短信所需的权限。然后,我们将创建一个带有Phone Number和Message字段以及Send按钮的布局。点击发送按钮时,我们将创建并发送短信。以下是步骤:
-
打开 Android Manifest 并添加以下权限:
<uses-permission android:name="android.permission.SEND_SMS"/> -
打开
activity_main.xml,用以下 XML 替换现有的TextView:<EditText android:id="@+id/editTextNumber" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="number" android:ems="10" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:hint="Number"/> <EditText android:id="@+id/editTextMsg" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/editTextNumber" android:layout_centerHorizontal="true" android:hint="Message"/> <Button android:id="@+id/buttonSend" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Send" android:layout_below="@+id/editTextMsg" android:layout_centerHorizontal="true" android:onClick="send"/> -
打开
MainActivity.java并添加以下全局变量:final int SEND_SMS_PERMISSION_REQUEST_CODE=1; Button mButtonSend; -
在现有的
onCreate()回调中添加以下代码:mButtonSend = (Button)findViewById(R.id.buttonSend); mButtonSend.setEnabled(false); if (checkCallPermission(Manifest.permission.SEND_SMS)) { mButtonSend.setEnabled(true); } else { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.SEND_SMS}, SEND_SMS_PERMISSION_REQUEST_CODE); } -
添加以下方法以检查权限:
private boolean checkPermission(String permission) { int permissionCheck = ContextCompat.checkSelfPermission(this,permission); return (permissionCheck == PackageManager.PERMISSION_GRANTED); } -
重写
onRequestPermissionsResult()以处理权限请求响应:@Override public void onRequestPermissionsResult(int requestCode,String permissions[], int[] grantResults) { switch (requestCode) { case SEND_SMS_PERMISSION_REQUEST_CODE: { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { mButtonSend.setEnabled(true); } return; } } } -
最后,添加实际发送短信的方法:
public void send(View view) { String phoneNumber = ((EditText)findViewById(R.id.editTextNumber)).getText().toString(); String msg = ((EditText)findViewById(R.id.editTextMsg)).getText().toString(); if (phoneNumber==null || phoneNumber.length()==0 || msg==null || msg.length()==0 ) { return; } if (checkPermission(Manifest.permission.SEND_SMS)) { SmsManager smsManager = SmsManager.getDefault(); smsManager.sendTextMessage(phoneNumber, null, msg, null, null); } else { Toast.makeText(MainActivity.this, "No Permission", Toast.LENGTH_SHORT).show(); } } -
你已经准备好在设备或模拟器上运行应用程序了。(向另一个模拟器发送时,使用模拟器设备号,如 5556。)
工作原理...
发送短信的代码只有两行,如下所示:
SmsManager smsManager = SmsManager.getDefault();
smsManager.sendTextMessage(phoneNumber, null, msg, null, null);
sendTextMessage()方法负责实际发送操作。本示例的大部分代码是为了设置权限,因为从 Android 6.0 Marshmallow(API 23)开始权限模型发生了变化。
还有更多...
尽管发送短信很简单,但我们仍然有更多的选项。
多部分消息
虽然根据运营商可能有所不同,但通常每条短信允许的最大字符数是 160。你可以修改前面的代码,检查消息是否超过 160 个字符,如果是,可以调用 SMSManager 的divideMessage()方法。该方法返回一个ArrayList,你可以将其发送给sendMultipartTextMessage()。以下是示例代码:
ArrayList<String> messages=smsManager.divideMessage(msg);
smsManager.sendMultipartTextMessage(phoneNumber, null, messages, null, null);
提示
请注意,使用模拟器时,通过sendMultipartTextMessage()发送的消息可能无法正常工作,因此请务必在真实设备上进行测试。
发送状态通知
如果你希望收到消息状态的通知,有两个可选字段可以使用。以下是SMSManager文档中定义的sendTextMessage()方法:
sendTextMessage(String destinationAddress, String scAddress, String text, PendingIntent sentIntent, PendingIntent deliveryIntent)
你可以包含一个待定意图,以便在发送状态和/或投递状态时得到通知。收到你的待定意图后,它将包含一个结果代码,如果发送成功,则为 Activity.RESULT_OK,或者如 SMSManager 文档(在以下另请参阅部分提到的链接)中定义的错误代码:
-
RESULT_ERROR_GENERIC_FAILURE:一般失败原因 -
RESULT_ERROR_NO_SERVICE:由于服务当前不可用而失败 -
RESULT_ERROR_NULL_PDU:由于没有提供 PDU 而失败 -
RESULT_ERROR_RADIO_OFF:由于无线电被明确关闭而失败
另请参阅
-
维基百科上的短信服务
en.wikipedia.org/wiki/Short_Message_Service -
开发者文档:SMSManager 在
developer.android.com/reference/android/telephony/SmsManager.html
接收短信消息
本教程将演示如何设置一个广播接收器,以便在接收到新短信时通知你。值得注意的是,你的应用无需运行即可接收短信意图。Android 会启动你的服务来处理短信。
准备工作
在 Android Studio 中创建一个新项目,并将其命名为 ReceiveSMS。使用默认的 Phone & Tablet 选项,并在提示 Activity Type 时选择 Empty Activity。
如何操作...
在本演示中,我们将不使用布局,因为所有工作都将在广播接收器中进行。我们将使用 Toast 来显示传入的短信。打开 Android 清单文件并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.RECEIVE_SMS" /> -
向
<application>元素添加以下广播接收器的声明:<receiver android:name=".SMSBroadcastReceiver"> <intent-filter> <action android:name="android.provider.Telephony.SMS_RECEIVED"> </action> </intent-filter> </receiver> -
打开
MainActivity.java并添加以下方法:private boolean checkPermission(String permission) { int permissionCheck = ContextCompat.checkSelfPermission( this, permission); return (permissionCheck == PackageManager.PERMISSION_GRANTED); } -
修改现有的
onCreate()回调以检查权限:if (!checkPermission(Manifest.permission.RECEIVE_SMS)) { ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.RECEIVE_SMS}, 0); } -
使用以下代码向项目中添加一个名为
SMSBroadcastReceiver的新 Java 类:public class SMSBroadcastReceiver extends BroadcastReceiver { final String SMS_RECEIVED = "android.provider.Telephony.SMS_RECEIVED"; @Override public void onReceive(Context context, Intent intent) { if (SMS_RECEIVED.equals(intent.getAction())) { Bundle bundle = intent.getExtras(); if (bundle != null) { Object[] pdus = (Object[]) bundle.get("pdus"); String format = bundle.getString("format"); final SmsMessage[] messages = new SmsMessage[pdus.length]; for (int i = 0; i < pdus.length; i++) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i], format); } else { messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]); } Toast.makeText(context, messages[0].getMessageBody(), Toast.LENGTH_SHORT).show(); } } } } } -
现在你可以在设备或模拟器上运行应用程序了。
工作原理...
就像在前一个关于发送短信的教程中一样,我们首先需要检查应用是否有权限。(在 Android 6.0 之前的设备上,清单声明将自动提供权限,但对于棉花糖及以后的版本,我们需要像这里一样提示用户。)
如你所见,广播接收器接收新短信的通知。我们使用此代码在 Android 清单文件中告诉系统我们希望接收新的短信接收广播:
<receiver android:name=".SMSBroadcastReceiver">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED"></action>
</intent-filter>
</receiver>
通知通过标准的 onRecieve() 回调传入,因此我们使用此代码检查动作:
if (SMS_RECEIVED.equals(intent.getAction())) {}
这可能是本演示中最复杂的代码行:
messages[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
基本上,它调用 SmsMessage 库从 PDU 创建一个 SMSMessage 对象。(PDU,即协议数据单元,是短信的二进制数据格式。)如果你不熟悉 PDU 格式,你不需要了解。SmsMessage 库会为你处理并返回一个 SMSMessage 对象。
提示
如果你的应用没有收到短信广播消息,可能是其他应用阻止了你的应用。你可以尝试像这样增加 intent-filter 中的优先级值,或者禁用/卸载其他应用:
<intent-filter android:priority="100">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
还有更多...
本指南演示了接收到短信时如何显示短信,但如何读取现有消息呢?
阅读现有短信
首先,要读取现有消息,你需要以下权限:
<uses-permission android:name="android.permission.READ_SMS" />
这是一个使用短信内容提供者获取游标的示例:
Cursor cursor = getContentResolver().query(Uri.parse("content://sms/"), null, null, null, null);
while (cursor.moveToNext()) {
textView.append("From :" + cursor.getString(1) + " : " + cursor.getString(11)+"\n");
}
在撰写本文时,短信内容提供者有超过 30 列。以下是前 12 列,最有用(记住,列计数从零开始):
0. _id
1. thread_id
2. address
3. person
4. date
5. protocol
6. read
7. status
8. type
9. reply_path_present
10. subject
11. body
请记住,内容提供者不是公共 API 的一部分,可能会在未经通知的情况下更改。
另请参阅
-
开发者文档:SmsManager 在
developer.android.com/reference/android/telephony/SmsManager.html -
PDU (协议数据单元) 在
en.wikipedia.org/wiki/Protocol_data_unit -
开发者文档:Telephony.Sms.Intents 在
developer.android.com/reference/android/provider/Telephony.Sms.Intents.html
在应用中显示网页
当你想要在网页上显示 HTML 内容时,你有两个选择:调用默认浏览器或在你的应用内显示。如果你只想调用默认浏览器,可以使用如下 Intent:
Uri uri = Uri.parse("https://www.packtpub.com/");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
如果你需要在自己的应用内显示内容,可以使用 WebView。本指南将展示如何在你的应用中显示网页,如图所示:
准备工作
在 Android Studio 中创建一个新项目,命名为 WebView。选择默认的手机和平板选项,并在提示活动类型时选择空活动。
如何操作...
我们将通过代码创建 WebView,因此不会修改布局。我们将从打开 Android Manifest 开始,并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/> -
修改现有的
onCreate()以包含以下代码:WebView webview = new WebView(this); setContentView(webview); webview.loadUrl("https://www.packtpub.com/"); -
你已经准备好在设备或模拟器上运行应用程序。
工作原理...
我们创建一个 WebView 作为我们的布局,并使用 loadUrl() 加载我们的网页。前面的代码可以工作,但在这一级别上,它非常基础,仅显示第一页。如果你点击任何链接,默认浏览器将处理请求。
还有更多...
如果你希望拥有完整的网页浏览功能,以便他们点击的任何链接仍在你的 WebView 中加载?按照此代码所示创建一个 WebViewClient:
webview.setWebViewClient(new WebViewClient());
控制页面导航
如果你想要更多控制页面导航,例如只允许在你自己的网站内链接,你可以创建自己的 WebViewClient 类并覆盖 shouldOverrideUrlLoading() 回调,如下所示:
private class mWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (Uri.parse(url).getHost().equals("www.packtpub.com")) {
return false; //Don't override since it's the same //host
} else {
return true; //Stop the navigation since it's a //different site
}
}
}
如何启用 JavaScript
我们可以通过 WebView 的 WebSettings 进行许多其他自定义设置。如果你想启用 JavaScript,获取 WebView 的 WebSettings 并调用 setJavaScriptEnabled(),如下所示:
WebSettings webSettings = webview.getSettings();
webSettings.setJavaScriptEnabled(true);
启用内置缩放
另一个 webSetting 选项是 setBuiltInZoomControls()。从前面的代码继续,只需添加:
webSettings.setBuiltInZoomControls(true);
在下一节中查看 webSetting 链接,以获取大量附加选项。
另请参阅
-
开发者文档:WebView 在
developer.android.com/reference/android/webkit/WebView.html -
开发者文档:WebSettings 在
developer.android.com/reference/android/webkit/WebSettings.html -
开发者文档:android.webkit 在
developer.android.com/reference/android/webkit/package-summary.html
检查在线状态和连接类型
这是一个简单的食谱,但非常常见,可能会包含在你构建的每个互联网应用程序中:检查在线状态。在检查在线状态时,我们还可以检查连接类型:WIFI 或 MOBILE。
准备就绪
在 Android Studio 中创建一个新项目,并将其命名为 isOnline。在选择 Activity Type 时,使用默认的 Phone & Tablet 选项,并选择 Empty Activity。
如何操作...
首先,我们需要添加必要的权限来访问网络。然后,我们将创建一个带有 Button 和 TextView 的简单布局。要开始,请打开 Android Manifest 文件,并按照以下步骤操作:
-
添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -
打开
activity_main.xml文件,并用以下视图替换现有的TextView:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Check" android:layout_centerInParent="true" android:onClick="checkStatus"/> -
添加以下方法来报告连接状态:
private boolean isOnline() { ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); return (networkInfo != null && networkInfo.isConnected()); } -
添加以下方法来处理按钮点击:
public void checkStatus(View view) { TextView textView = (TextView)findViewById(R.id.textView); if (isOnline()) { ConnectivityManager connectivityManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); textView.setText(networkInfo.getTypeName()); } else { textView.setText("Offline"); } } -
你现在可以在设备或模拟器上运行应用程序了。
工作原理...
我们创建了 isOnline() 方法,以方便重用此代码。
为了检查状态,我们获取 ConnectivityManager 的实例来读取 NetworkInfo 的状态。如果它报告我们已连接,我们可以通过调用 getType() 来获取活动网络的名字,这将返回以下常量之一:
-
TYPE_MOBILE -
TYPE_WIFI -
TYPE_WIMAX -
TYPE_ETHERNET -
TYPE_BLUETOOTH
另外,请参阅后面的ConnectivityManager链接,了解其他常量。为了显示目的,我们调用getTypeName()。我们也可以调用getType()来获取数值常量。
还有更多...
让我们看看ConnectivityManager的一些其他常量。
监控网络状态变化
如果你的应用需要响应网络状态的变化,请查看ConnectivityManager中的CONNECTIVITY_ACTION。你需要创建一个广播接收器,然后注册该事件。以下是如何通过 Android 清单在接收器的意图过滤器中包含动作的示例:
<receiver android:name="com.vcs.timetrac.VCSBroadcastReceiver">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
使用 Android 清单时要小心,因为它会在每次网络状态变化时通知你的应用,即使你的应用没有被使用。这可能会导致不必要的电池消耗。如果你的应用只需要在用户实际使用你的应用时响应网络变化,请在代码中创建监听器。
另请参阅
-
开发者文档:ConnectivityManager 在
developer.android.com/reference/android/net/ConnectivityManager.html -
开发者文档:NetworkInfo 在
developer.android.com/reference/android/net/NetworkInfo.html
开始使用 Volley 进行网络请求
Android 提供了多个用于互联网查询的库,包括 Apache 的HttpClient和HttpURLConnection。在 Android 2.3 Gingerbread(API 9)之前,Apache HttpClient是推荐的库。在 Android 2.3 Gingerbread(API 9)中,对HttpURLConnection库进行了许多改进,使其成为了推荐的库,至今仍然如此。随着 Android 6.0 的发布,Apache HttpClient已完全从 SDK 中移除,HttpURLConnection库成为了推荐的替代品。
尽管HttpURLConnection库仍然可用并有其用途,但也有其缺点:如果你是初次编写网络请求,它不是最易于使用的库,并且需要编写很多重复的样板代码。幸运的是,来自 Google Play 团队的谷歌开发者Ficus Kirkpatrick提供了一个新选择。他发布了一个名为 Volley 的库,该库提供了一个简化的封装器。(默认使用HttpURLConnection库,也可以与其他库一起使用。)
注意
你可以在这里观看他的 Google I/O 演讲:
使用 Volley 而不是HttpURLConnection的几个原因包括以下:
-
线程池(默认为四个线程)
-
透明磁盘缓存
-
队列优先级设置
还有其他的好处,但这三点就足以让你想要了解 Volley。第四个好处,如果你曾经使用过HttpURLConnection,就会很明显,那就是它减少了模板代码。你不需要围绕许多调用编写一堆标准的try/catch代码,库将在内部处理检查,让你更专注于手头的具体任务。
Volley 内置支持以下请求类型:
-
字符串
-
JSON
-
图像
-
自定义
虽然 Volley 擅长处理多个小型请求调用(例如在ListView中滚动时),但不适合大文件下载,因为返回的对象是在内存中解析的。对于大文件下载,请查看DownloadManager(请参考食谱末尾的链接)。同样,由于这个原因,它也不适合流式内容;对于流式内容,请参考HttpURLConnection。
由于 Volley 目前不在 Android SDK 中,我们需要下载代码并将其添加到我们的项目中。这个食谱将指导你完成将 Volley 添加到你的应用程序项目并发出一个简单请求的步骤。
准备就绪
在创建你的新项目之前,使用以下 Git 命令从Android Open Source Project(AOSP)网站下载 Volley 项目文件:
git clone https://android.googlesource.com/platform/frameworks/volley
如果你不太熟悉 Git,请查看本食谱末尾的 Git(软件)链接,获取更多信息以及帮助你找到适合你平台的 Git 客户端。Git 是一种在许多平台上使用的版本控制软件(VCS)。(安装后,你还可以在 Android Studio 中集成 Git VCS。)
在 Android Studio 中创建一个新项目,并将其命名为SetupVolley。使用默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
如何操作...
在开始以下步骤之前,请确保你已经按照先前的描述下载了 Volley 项目。下面我们将从将 Volley 添加到我们的项目开始,进行一个简单的互联网调用。我们将在布局中使用一个按钮来发起请求,并使用TextView来显示结果。以下是步骤:
-
打开 Android Manifest 并添加以下权限:
<uses-permission android:name="android.permission.INTERNET"/> -
通过选择File | New | Import Module导入
Volley模块(请参考以下截图),并按照向导操作。 -
在New Module导入向导的第二页(请参考以下截图),你需要指定 Volley 文件的位置并分配Module name。这是下一步我们需要用到的名称:
-
在
Gradle Scripts部分,打开build.gradle (Module: app)文件。请参考以下截图: -
在
dependencies部分添加/验证以下声明:compile project(":Volley")注意
括号内的值需要与你上一步指定的模块名称相匹配。
-
在
Gradle Scripts下,打开settings.gradle文件,并按以下内容验证:include ':app', ':Volley' -
打开
activity_main.xml文件,用以下TextView和Button元素替换现有的TextView:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_above="@+id/button" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Request" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="sendRequest"/> -
添加由按钮点击调用的
sendRequest()方法:public void sendRequest(View view) { final TextView textView = (TextView)findViewById(R.id.textView); RequestQueue queue = Volley.newRequestQueue(this); String url ="https://www.packtpub.com/"; StringRequest stringRequest = new StringRequest(Request.Method.GET, url,new Response.Listener<String>() { @Override public void onResponse(String response) { textView.setText(response.substring(0,500)); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { textView.setText("onErrorResponse(): "+ error.getMessage()); } }); queue.add(stringRequest); } -
您已经准备好在设备或模拟器上运行应用程序。
工作原理...
了解在 Volley 中,网络事务被称为请求可能很有帮助。要执行请求,请将其添加到队列中。为此,我们首先创建一个 Volley RequestQueue的实例,然后创建一个StringRequest并将其添加到队列中。StringRequest顾名思义;我们请求的是字符串响应。
对于这个食谱,我们只需调用 Packt Publishing 网站,并将页面作为字符串响应获取。由于这只是为了说明,我们只显示前 500 个字符。
还有更多...
现在您已经正确设置了 Volley 并进行了网络请求,这个食谱将是后续 Volley 食谱的基础。
另请参阅
-
Volley: 在 Google 的 Git 存储库中位于
android.googlesource.com/platform/frameworks/volley -
Git(软件): 维基百科,自由的百科全书,位于
en.wikipedia.org/wiki/Git_(software) -
开发者文档:DownloadManager位于
developer.android.com/reference/android/app/DownloadManager.html -
开发者文档:HttpURLConnection位于
developer.android.com/reference/java/net/HttpURLConnection.html
取消 Volley 请求
在上一个食谱中,我们演示了如何将请求添加到 Volley 队列中。如果您不再需要响应会怎样?这可能发生在用户通过ListView滚动时,您通过从网上获取信息来更新ListItems。如果允许请求完成,知道您将丢弃响应,这将是带宽、电力和 CPU 周期的浪费。
如果您之前使用的是HTTPURLConnection库,您需要手动跟踪并取消所有请求。这个食谱将向您展示如何在 Volley 中轻松取消请求。
准备工作
如果您还没有完成之前的食谱,即开始使用 Volley 进行网络请求,您需要按照步骤 1-5 将 Volley 模块添加到您的应用程序中。
在 Android Studio 中创建一个新项目,并将其命名为CancelVolleyRequest。选择默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
如何操作...
如果您还没有将 Volley 模块添加到您的应用程序中,请回顾之前的章节。在您的项目中添加了 Volley 之后,请按照以下步骤操作:
-
打开
activity_main.xml,用以下 XML 替换现有的TextView:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_above="@+id/button" /> <Button android:id="@+id/button" android:layout_width="100dp" android:layout_height="wrap_content" android:text="Request" android:layout_centerInParent="true" android:onClick="sendRequest"/> <Button android:id="@+id/buttonClose" android:layout_width="100dp" android:layout_height="wrap_content" android:layout_below="@+id/button" android:layout_centerHorizontal="true" android:text="Close" android:onClick="close"/> -
打开
MainActivity.java并添加以下全局变量:RequestQueue mRequestQueue; -
编辑现有的
onCreate()以初始化RequestQueue:mRequestQueue = Volley.newRequestQueue(this); -
添加以下
sendRequest()方法(注意,这与前一个食谱中的sendRequest()方法相似,但有几个变化):public void sendRequest(View view) { final TextView textView = (TextView)findViewById(R.id.textView); String url ="https://www.packtpub.com/"; StringRequest stringRequest = new StringRequest(Request.Method.GET, url,new Response.Listener<String>() { @Override public void onResponse(String response) { textView.setText(response.substring(0,500)); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { textView.setText("onErrorResponse(): "+ error.getMessage()); } }); stringRequest.setTag(this); mRequestQueue.add(stringRequest); finish(); } -
添加关闭按钮的
onClick方法:public void close(View view){ finish(); } -
为
onStop()回调创建以下覆盖方法:@Override protected void onStop() { super.onStop(); mRequestQueue.cancelAll(this); } -
你已经准备好在设备或模拟器上运行应用程序。
工作原理...
要取消请求,我们可以调用RequestQueue的cancelAll()方法,并传入我们的标签。在这个例子中,我们使用活动this作为我们的标签,但我们可以使用任何对象作为标签。这允许你为请求创建所需的任何分组。
还有更多...
我们不仅仅是在展示如何轻松取消请求,同时也在演示一种防御性编程策略。通过确保取消所有请求,我们无需在响应中添加检查 null 活动的代码,因为 Volley 保证在请求被取消后,我们将不会收到任何响应。
使用 Volley 请求 JSON 响应。
由于 JavaScript Object Notation(JSON)可能是最常见的数据交换格式,你可能需要调用一个 JSON 网络服务。(如果你不熟悉 JSON,请查看本食谱末尾的链接。)本食谱将演示如何使用 Volley 进行 JSON 请求。
准备工作。
在 Android Studio 中创建一个新项目,名为JSONRequest。选择默认的Phone & Tablet选项,并在提示Activity Type时选择Empty Activity。
本食谱将使用如Volley 入门进行网络请求中所述的 Volley 设置。按照步骤 1-5 将 Volley 添加到你的新项目中。
如何操作...
按照前面的描述将 Volley 添加到你的项目中,然后执行以下步骤:
-
打开
activity_main.xml,用以下 XML 替换现有的TextView:<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentLeft="true" android:layout_above="@+id/button" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Request" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="sendRequest"/> -
添加以下
sendRequest()方法:public void sendRequest(View view) { final TextView textView = (TextView)findViewById(R.id.textView); RequestQueue queue = Volley.newRequestQueue(this); String url ="<json service>"; //"http://ip.jsontest.com/" JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.GET, url, null, new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { textView.setText(response.toString()); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { textView.setText("onErrorResponse(): "+ error.getMessage()); } }); queue.add(jsonObjectRequest); } -
在运行此应用程序之前,请替换代码中的
url字符串。
工作原理...
使用JsonObjectRequest()请求 JSON 响应基本上与StringRequest()相同。不同之处在于响应,它返回一个JSONObject。
要运行此代码,你需要将url参数替换为你的网络服务 URL。如果你没有可测试的网络服务,你可以尝试来自 JSON Test 网站(www.jsontest.com/)的链接。
还有更多...
在前面的示例中,我们使用JsonObjectRequest请求了JSONObject。我们还可以使用JsonArrayRequest请求JSONARray。
另请参阅。
-
访问
json.org/的 JSON 网页。
使用 Volley 请求图像。
一旦你按照前一个菜谱中的演示进行了 JSON 请求,接下来最可能进行的调用就是获取一个图片。本示例将演示如何请求一个图片来更新一个 ImageView。
准备工作
在 Android Studio 中创建一个新项目,并将其命名为 ImageRequest。使用默认的 手机 & 平板 选项,并在提示选择 活动类型 时选择 空活动。
本示例将使用在 Volley 网络请求入门 示例中描述的设置。按照步骤 1-5 在你的新项目中添加 Volley。
如何操作...
按照之前的描述将 Volley 添加到你的项目中,然后遵循以下步骤:
-
打开
activity_main.xml并用以下 XML 替换现有的TextView:<ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Request" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:onClick="sendRequest"/> -
添加以下
sendRequest()方法:public void sendRequest(View view) { final ImageView imageView = (ImageView)findViewById(R.id.imageView); RequestQueue queue = Volley.newRequestQueue(this); String url ="http://www.android.com/static/img/logos-2x/android-wordmark-8EC047.png"; ImageRequest imageRequest = new ImageRequest(url, new Response.Listener<Bitmap>() { @Override public void onResponse(Bitmap bitmap) { imageView.setImageBitmap(bitmap); } }, 0, 0, ImageView.ScaleType.CENTER, null, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { error.printStackTrace(); } }); queue.add(imageRequest); } -
在设备或模拟器上运行应用程序。
工作原理...
本示例基本上与前两个 Volley 请求的方式相同。在这个示例中,我们传递一个图片的 URL 并在响应中加载 ImageView。
我们现在已经涵盖了三种基本请求类型:String,JSON 和 Image。
还有更多内容...
尽管基本类型可能覆盖了你的大部分需求,但 Volley 是可扩展的,你也可以通过扩展 Request<T> 来实现自定义响应。
本示例演示了我们的示例代码中存在的问题。如果你改变设备的方向,你会看到活动重新创建时图像闪烁。
创建一个 Volley 单例
建议将 Volley 实例化为单例。(另一种方法是在应用类中创建队列。)要在 Android Studio 中创建单例类,请转到 新建 | 文件 | 单例 并为其提供一个类名,例如 VolleySingleton。
将创建请求队列的代码移动到单例类中。如果你按照如下方式创建一个方法:
public <T> void addToRequestQueue(Request<T> req) {
mRequestQueue.add(req);
}
然后,你可以使用以下代码从任何地方向你的队列中添加请求:
VolleySingleton.getInstance(this).addToRequestQueue(stringRequest);
正确实现这一点的关键在于始终通过在传入的上下文中调用 getApplicationContext() 来使用应用上下文(而不是活动或广播接收器的上下文)。
另请参阅
使用 Volley 的 NetworkImageView 和 ImageLoader
我们关于 Volley 的最后一个菜谱不是一个请求本身,而是对 ImageView 的替换。请求图片来填充 ImageView 是如此常见的任务;Volley 将这一功能组合到一个名为 NetworkImageView 的新视图中。本示例将演示如何使用 NetworkImageView。
准备工作
在 Android Studio 中创建一个新项目,并将其命名为 NetworkImageView。使用默认的 手机 & 平板 选项,并在提示选择 活动类型 时选择 空活动。
本示例将使用在 Volley 网络请求入门 示例中描述的设置。按照步骤 1-5 在你的新项目中添加 Volley。
如何操作...
按照之前描述的方式将 Volley 添加到你的项目中,然后执行以下步骤:
-
打开
activity_main.xml文件,将现有的TextView替换为以下 XML 代码:<com.android.volley.toolbox.NetworkImageView android:id="@+id/networkImageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /> -
将以下代码添加到现有的
onCreate()回调中:NetworkImageView networkImageView = (NetworkImageView)findViewById(R.id.networkImageView); String url="http://www.android.com/static/img/logos-2x/android-wordmark-8EC047.png"; RequestQueue queue = Volley.newRequestQueue(this); ImageLoader imageLoader = new ImageLoader(queue,new ImageLoader.ImageCache() { private final LruCache<String, Bitmap>cache = new LruCache<String, Bitmap>(20); @Override public Bitmap getBitmap(String url) { return cache.get(url); } @Override public void putBitmap(String url, Bitmap bitmap) { cache.put(url, bitmap); } }); networkImageView.setImageUrl(url,imageLoader); -
你已经准备好在设备或模拟器上运行应用程序。
工作原理...
这个示例与之前的 Volley 示例非常不同。我们没有创建请求对象,而是创建了一个 ImageLoader。ImageLoader 类允许我们覆盖默认的缓存行为,比如位图的数量或大小计算方式(我们可以将缓存改为基于总内存而不是图片数量)。有关更多信息,请参阅后面的 LruCache 链接。
创建了 ImageLoader 之后,你可以将图片 URL 分配给 NetworkImageView,并将 ImageLoader 作为第二个参数传递。
还有更多...
如我们在上一个食谱中提到的,我们 Volley 示例的问题在于我们在活动中创建了队列。这在处理图片时最为明显,但不管怎样,建议创建一个 Volley 单例。有关更多信息,请参阅上一个食谱中的 创建 Volley 单例 部分。
如果你按照前一个食谱创建了一个单例,你还可以将 ImageLoader 代码移动到单例中,并像这样公开 ImageLoader:
public ImageLoader getImageLoader() {
return mImageLoader;
}
创建了单例后,这个食谱可以按照以下方式编写代码:
NetworkImageView networkImageView = (NetworkImageView)findViewById(R.id.networkImageView);
String url="http://www.android.com/static/img/logos-2x/android-wordmark-8EC047.png";
networkImageView.setImageUrl(url, VolleySingleton.getInstance(this).getImageLoader());
另请参阅
- 开发者文档:LruCache 在
developer.android.com/reference/android/util/LruCache.html