65.9K
CodeProject 正在变化。 阅读更多。
Home

使用小部件消费 Android 手电筒的应用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2015 年 12 月 5 日

CPOL

16分钟阅读

viewsIcon

27140

downloadIcon

478

本文讨论Android编程,旨在教你Android编程中的相机和窗口小部件。

引言和背景

好了,我这个月买了一部新手机。我非常喜欢它,但它缺少一些基本工具包。其中之一就是手电筒应用程序,它可以按需打开或关闭设备的闪光灯。Android支持你在应用程序中使用Camera API(毕竟,闪光灯是为了拍照或录像而提供的,而不是作为泛光灯)。所以,我想我应该从Play商店下载一个应用程序,对吧?不行!虽然它能为我的手电筒问题提供一个“优雅”的解决方案,但我要面对多少广告,或者应用程序不清晰的用户界面。所以我考虑为自己构建一个应用程序来打开或关闭闪光灯。

既然我为自己做了,为什么不也教教你们呢,这样你们就可以使用相同的应用程序,或者在此基础上进行构建并与我分享,这样我也可以利用你们的技能。:-) 源代码将可用,并且该应用程序正在发布到Google的Play商店。

在本文中,我将讨论Android编程中的Camera API以及Android应用程序中窗口小部件编程的简要概述。虽然这种类型的应用程序非常基础和直接,但它至少会让你对在设备中使用Camera API以及通过主屏幕上的窗口小部件管理这些后台服务有一个非常基本的了解。

创建类似的应用程序并不难,但仍然在很多方面,能够创建一个响应用户交互以触发我们将要使用的服务来打开或关闭闪光灯的窗口小部件可能是一项艰巨的任务。在接下来的章节中,你将学习如何在考虑“简单UI”的情况下创建类似的应用程序,以及如何编程窗口小部件以使应用程序的概念更加简单。

先决条件

你不需要对Android API或Linux的工作原理有深入的了解,后面的权限和其他东西将在文章中讨论。但我会尽量做到像初学者一样清晰易懂。

编写Android应用程序

在本节中,我将讨论应用程序的构建。我将把它分成两个部分,一个用于构建手电筒应用程序的服务,另一个部分用于构建我们应用程序的窗口小部件,以便用户可以从任何屏幕触发服务。

在这两个部分中,我将尽量保持我的英语水平非常简单,以便每个人都能理解我每段想表达的意思。

此时,我建议创建一个新的Android应用程序。关于“如何创建Android项目”已经有很多很棒的教程,你可以考虑阅读其中一个来学习如何创建新项目,因为我不会在本文中涵盖“基础”主题。

构建后端服务

首先,我们需要定义每次需要执行的服务。Android API允许我们在一定程度上使用硬件组件。一旦我们试图使用一个组件的子组件,这个程度就结束了。闪光灯在Android设备中与相机捆绑在一起,这意味着你不能单独使用闪光灯,你必须使用相机。这就是破解的开始,你开始使用相机,但只是为了使用闪光灯,而不是图像或视频捕获。

我们感兴趣的对象是android.hardware包中的Camera对象。该对象允许我们创建与设备提供的相机建立连接,检查是否存在任何相机,还允许我们创建自己的特殊应用程序来使用相机,例如在创建受Instagram启发的应用程序时使用的那些。因此,我们将使用相同的对象来向Android OS发出请求,让我们的应用程序使用相机一段时间。

设置我们的应用程序权限

与Linux环境中的所有其他进程一样,我们首先需要设置应用程序的权限才能使用。没有权限,我们的应用程序最终会遇到RuntimeException。因此,在继续之前,打开你的应用程序的清单文件并写入以下权限。

<uses-permission
        android:name="android.permission.FLASHLIGHT"
        android:permissionGroup="android.permission-group.HARDWARE_CONTROLS"
        android:protectionLevel="normal"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.hardware.camera.flash"/>

<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.flash" android:required="false" />

上面的代码只是所需的权限集。我们还需要能够使用闪光灯,因此我们单独设置它们。需要注意的一点是,虽然相机附带的所有内容都由相机控制,但我们的应用程序需要获得使用包括闪光灯在内的每个组件的权限。如果你的清单不共享权限详细信息,相机将不允许你的应用程序与组件原生通信;因此会发生RuntimeException

<uses-feature>元素允许我们指出我们的应用程序需要这些功能,并且没有这些功能就没有用。当然,如果设备中没有闪光灯,我们的应用程序将毫无用处。android:required属性允许我们设置应用程序安装的条件,或者根据该硬件功能的存在而忽略。你可以将其设置为true或false,表示该功能是必需的。

权限就这些了,我们只需要使用Camera对象,然后让它为我们打开闪光灯,只要我们想要。

创建MainActivity

在Android编程中,MainActivity是用于第一个活动的约定名称,Android Studio强制(或者说,简单地,使用)此名称用于第一个活动。所以,在我们的MainActivity中,我们将创建一个按钮,使我们能够触发Camera对象。实际执行此操作的代码将在后面显示,本节只讨论活动。

首先,创建一个按钮,可以单击以触发服务。


图 1:Android Studio显示MainActivity的渲染,带有一个Button对象和一些文本。

请注意按钮本身,按钮将触发服务,这是XML代码(如果有人感兴趣的话

<Button
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/turn_on_string"
        android:id="@+id/turn_on_button"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"/>

处理活动中的用户交互

你可能已经注意到,我还没有给Button对象附加任何“onClick”属性。这不是“设计使然”,我忘了添加那个事件:laugh:在我粘贴代码时才想起来。

无论如何,我在代码后面做了这件事,并将点击监听器附加到按钮上。按钮监听器反过来触发我们应用程序的后端服务以激活(或停用)我们设备中的闪光灯。以下代码可以实现这一点,

// Find the button from the view.
final Button button = (Button)findViewById(R.id.turn_on_button);

// Attach the event handler
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {

        // Create an intent (Empty at the point)
        Intent serviceIntent = new Intent(getApplicationContext(), FlashlightIntentService.class);
        if (button.getText().toString().equals(getString(R.string.turn_on_string))) {
            // Turn on the flashlight
            serviceIntent.setAction(FlashlightIntentService.ACTION_TURN_ON);
            startService(serviceIntent);

            button.setText(R.string.turn_off_string);
        } else {
            // Turn off the flashlight
            serviceIntent.setAction(FlashlightIntentService.ACTION_TURN_OFF);
            startService(serviceIntent);

            button.setText(R.string.turn_on_string);
        }
    }
});

我可能漏掉的一件事是后台服务,我想稍后讨论它。但是,如果你想知道FlashlightIntentService.ACTION_TURN_ON来自哪里,请查看服务的操作。上面的程序只是将一个事件处理程序附加到按钮上,然后用户可以使用该按钮来触发此服务。你也可以看到我们正在使用一个服务,通过函数startService(serviceIntent)

我尽量使应用程序简洁紧凑,这就是为什么我创建了一个单独的服务来处理请求,而不是将每个命令硬编码到MainActivity中。这还将允许我更新服务以添加任何进一步的功能。这使我可以在UI线程中管理UI交互,并在后台线程中执行后台操作。

构建后台服务—FlashlightIntentService

我们所有的逻辑都依赖于我们应用程序的这个后端服务。我在这个服务中实现了所有的“if-then-else”概念,并提供了两个操作,一个用于打开,一个用于关闭。后端服务需要在单独的线程上执行,因为我们也想在我们的窗口小部件中使用它。因此,即使没有MainActivity的实例,我们仍然需要我们的用户能够使用该服务。此时,在你的应用程序中创建一个新的IntentService,随意命名。我将其命名为FlashlightIntentService。你应该考虑使用相同的名称以避免任何冲突。

服务将执行什么操作?

我们的服务在我们应用程序中只执行以下两个操作。

  1. 打开闪光灯。
  2. 关闭闪光灯。

很简单,所以我们需要在我们的服务中定义两个操作来表示需要执行的命令。在Android中,我们将这些操作定义为静态常量(final)字符串。然后,不同的活动可以使用这些字符串来创建Intent,我们的服务可以检查触发服务的Intent的当前操作是什么。看看操作是如何定义的,

// ACTIONS
public static final String ACTION_TURN_ON = "com.wordpress.afzaalahmadzeeshan.flashlight_app.TURN_ON";
public static final String ACTION_TURN_OFF = "com.wordpress.afzaalahmadzeeshan.flashlight_app.TURN_OFF";

这两个现在可以被任何活动访问,因为它们是公共的。我们可以创建我们的Intent,然后使用setAction(String)为该Intent设置一个操作。在Android编程中,操作定义了活动(或用户)实际想要做什么。在上面的主题中,这些操作被用来定义上面按钮的行为。

提示:有关IntentService与Service的更多信息,请阅读此Stack Overflow线程

建立到相机的“安全”连接

当服务启动时,我们还想建立与相机设备的连接。在Android中,在处理Android设备本身提供的设备时,我们可能需要考虑许多因素。例如相机,相机可能被另一个应用程序使用,也可能不可用,或者应用程序没有权限,等等。

在这些情况下,我们使用try...catch块来建立连接,并在建立连接时处理任何问题。最常见的问题是“未能连接到相机服务”。该问题意味着其他应用程序当前正在使用相机,你现在无法使用它。这也是“内存泄漏”的结果,因为应用程序可能已结束但忘记释放相机资源,因此,在结束应用程序的活动之前,请务必清理资源。

但是,以下函数可以做到,它尝试连接到服务,并在发生任何错误时通知用户。

private void fillFields() {
    // Initialize the fields
    canWork = this.getPackageManager()
                .hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
    // Get the IDs if any.
    ids = AppWidgetManager.getInstance(this)
                .getAppWidgetIds(new ComponentName(this, FlashLightWidget.class));

    // If flash feature is available
    if(canWork) {
        // Get the camera instance
        if(camera == null) {
            // Only if the camera object is null. Otherwise,
            // we may have captured the camera instance in last visit.
            try {
                camera = Camera.open(); // Try to open the camera

                // Set the properties.
                Camera.Parameters p = camera.getParameters();

                // Set it to flash mode
                p.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
                camera.setParameters(p);
            } catch (Exception ignored) {
                notifyUser();
            }
        }
    } else {
        // If flash is not available, notify the user
        notifyUser();
    }
}

有一个函数可能对你有用,notifyUser。这个函数更像是一个“紧急情况”函数。如果闪光灯不可用,或者相机无法连接到应用程序,它会向用户显示一个通知。在这些情况下,我们的应用程序会简单地通知用户有关问题。

private void notifyUser() {
    NotificationCompat.Builder mBuilder =
           new NotificationCompat.Builder(this)
                 .setSmallIcon(R.drawable.ic_flash_on_white_24dp)
                 .setContentTitle("Cannot access flashlight")
                 .setContentText("Camera application may be running in a background application.");

    int mId = 1234;
    NotificationManager mNotificationManager =
            (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    // mId allows you to update the notification later on.
    mNotificationManager.notify(mId, mBuilder.build());
}

我不想讨论这个通知构建器的事情,它非常简单,很容易从Android文档中理解。现在我们已经定义了需要执行的函数,它填充了我们的变量和对象,我们还查看了“方法”以避免在使用真实设备中的应用程序时引发任何不必要的异常。我们可能希望继续进行下一步,以实际处理来自用户的请求。在下一节中,你将看到如何实际执行操作。

处理Intent和Action

每个IntentService对象都有一个onHandleIntent(Intent)函数,它会触发一个后台工作线程,该线程负责在后台线程上执行操作。它节省了你的UI线程,并且不会冻结它。

我已经将所有内容都放在了自己的位置(函数!)上了,现在在这个函数中,我只能调用它们来为我做工作。然后我就可以管理什么时候做什么!很简单,对吧?看下面。

@Override
protected void onHandleIntent(Intent intent) {
    // Fill the fields
    fillFields();

    // Handle the intent
    if(intent.getAction().equals(ACTION_TURN_OFF)) {
        alter(false);
    } else if(intent.getAction().equals(ACTION_TURN_ON)) {
        alter(true);
    }
}

所以现在你看到了,我实际上是如何处理Intent的。Intent实际上被传递到一个工作线程,该线程现在将根据与相机的连接以及所有其他类似的东西来处理请求。否则,它只会显示一个错误消息。

在Intent处理程序中,你可以看到只有一个函数接受一个布尔值。然后我们的线程将处理它。该函数声明并定义如下,

private void alter(boolean on) {
    if(canWork) {
        if(on) {
            try {
                // Start the "flashlight"
                camera.startPreview();
                FlashLightWidget.lightOn = true;
                notifyWidgets();
            } catch (Exception e) {
                Toast.makeText(FlashlightIntentService.this, e.getMessage(), Toast.LENGTH_SHORT)
                        .show();
            }
        } else {
            try {
                // Stop everything and just release the resource
                camera.stopPreview();
                camera.release();
                camera = null;
                FlashLightWidget.lightOn = false;
                notifyWidgets();
            } catch (Exception e) {
                Toast.makeText(FlashlightIntentService.this, e.getMessage(), Toast.LENGTH_SHORT)
                        .show();
            }
        }
    } else {
        notifyUser();
    }
}

在上面的函数中,我要么触发相机激活,要么关闭它。这取决于参数“on”的值,该参数被传递给函数。

好吧,在这个阶段,应用程序就可以正常工作了。如果你点击按钮,它确实会打开闪光灯,依此类推。但是,我想要一些“更好的东西”。我想在我的设备上使用一个窗口小部件来使用它,我的意思是,我不想启动活动来打开闪光灯。我想使用一个窗口小部件,我可以点击它来启动灯。所以,下一节专门讲这个。


图 2:通过活动使用应用程序。


图 3:“关闭”按钮表示闪光灯当前处于活动状态。

活动部分就到这里。活动也依赖于服务。这就是为什么,当我们构建窗口小部件时,我们将不调用活动来启动,而是直接调用后台服务并触发它来为我们激活闪光灯。很简单。:-)

开发我们应用程序的Android窗口小部件

在本节中,你将学习如何创建Android窗口小部件。当然,标题只是一个幌子,用来教你Android基础知识,但现在我们先坚持“Android手电筒窗口小部件”。

Android窗口小部件是你设备主屏幕上的小屏幕或应用程序。你可以使用它们来触发任何服务、Intent、活动或Android能够做到的任何事情。窗口小部件是你的应用程序的快捷方式。我们将使用相同的良好概念来使用快捷方式并用它来触发服务。

构建窗口小部件的UI

与其他所有Android应用程序或活动一样,Android窗口小部件也有一个UI供我们的用户使用,他们可以通过它与你的应用程序进行交互。在很多方面,窗口小部件不一定需要启动你的应用程序,在那里你可以使用服务,在本例中是IntentService。

窗口小部件的布局也是XML文件,具有布局、一些Android控件,这些控件在屏幕上为用户渲染。如果你创建了一个新的窗口小部件,Android Studio可能已经为你创建了一个默认布局,如果没有,请创建一个并按以下XML标记编辑布局文件。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:padding="@dimen/widget_margin">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/button_switch_widget"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_margin="15dp"/>

</RelativeLayout>

上面的布局非常简单。它只包含一个ImageView控件,用于预览我们将用于让用户交互的图像。我还确保窗口小部件是“不可调整大小”的,因此它只跨越一个屏幕块网格。如果你要显示额外的信息,你可以考虑让你的窗口小部件可以调整大小到额外的网格。但我不想这样做,所以就这样了。

管理后端代码

在我们的窗口小部件后面工作的类扩展了AppWidgetProvider。这个类为我们提供了基本函数,我们可以使用它们来执行操作,例如管理何时创建新窗口小部件,或者何时删除窗口小部件等等。

我只尝试覆盖并提供一个函数的逻辑,即更新应用程序的函数。我没有费心去维护或继续处理其他函数,例如onEnabled(Context)onDisabled(Context)等。我还提供了一个自定义函数,它可以从外部对象(例如我们的服务)触发。只有当我们的对象有一个静态函数,并且可以在不拥有对象实例的情况下调用它时,才可能实现这一点。

以下函数是我们应用程序窗口小部件状态的代码后端处理程序。

static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
    // Construct the RemoteViews object
    RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.flash_light_widget);
    if(lightOn) {
        // Flash light active
        Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), 
                                            R.drawable.light_active_45);
        views.setImageViewBitmap(R.id.button_switch_widget, bitmap);

        // Create the intent, set the action
        Intent underlyingIntent = new Intent(context, FlashlightIntentService.class);
        underlyingIntent.setAction(FlashlightIntentService.ACTION_TURN_OFF);

        // Create the pending intent
        PendingIntent turnOffIntent = PendingIntent.getService(context, 1,
                                           underlyingIntent,
                                           PendingIntent.FLAG_UPDATE_CURRENT);
        views.setOnClickPendingIntent(R.id.button_switch_widget, turnOffIntent);

    } else {
        // Flash light off
        Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), 
                                            R.drawable.light_deactive_45);
        views.setImageViewBitmap(R.id.button_switch_widget, bitmap);

        // Create the intent, set the action
        Intent underlyingIntent = new Intent(context, FlashlightIntentService.class);
        underlyingIntent.setAction(FlashlightIntentService.ACTION_TURN_ON);

        // Create the pending intent
        PendingIntent turnOnIntent = PendingIntent.getService(context, 1,
                                          underlyingIntent,
                                          PendingIntent.FLAG_UPDATE_CURRENT);
        views.setOnClickPendingIntent(R.id.button_switch_widget, turnOnIntent);
    }

    // Instruct the widget manager to update the widget
    appWidgetManager.updateAppWidget(appWidgetId, views);
}

代码处理了我们窗口小部件的全部内容。窗口小部件依赖于此代码,它更新窗口小部件并允许用户与服务通信以触发闪光灯。我还使用了一些来自Google Material图标集的图标,并自己创建了一个,所有这些图标都可以从GitHub的存储库下载。这段代码的作用很简单。它只是允许用户使用窗口小部件并根据当前状态触发Intent。

如果灯当前是开着的,它将允许用户将其关闭,否则它将将其打开。这就是这段代码的作用。

我在家里的大约4台Android设备上使用了这个窗口小部件,它们都可以工作。我的设备上有一个这个窗口小部件的预览。



图 4:在我自己的设备上查看应用程序窗口小部件。闪光灯当前关闭。

一旦你使用这个窗口小部件触发了闪光灯,窗口小部件就会改变当前屏幕上使用的drawable,并且“看起来”闪光灯已激活。你也可以通过查看以下屏幕来判断。


图 5:闪光灯当前激活,窗口小部件通过更改drawable资源来体现这一点。

这展示了如何使用该应用程序。当然,你无法看到闪光灯,因为它没有包含在截图中,但它是有效的。这次就到这里。:-) 我希望你觉得这篇文章很有用。

关注点——和常用技巧

在本文中,你学会了如何创建一个可以触发应用程序中闪光灯活动的应用程序。我确保应用程序不会意外终止,但你还需要关注一些事情。首先,如果你正在使用任何资源,请记住在终止之前关闭流并释放资源。在上面的应用程序中,我在MainActivityonPause函数中做到了。

@Override
public void onPause() {
    if(FlashLightWidget.lightOn) {
        // Light is on, turn it off.
        Intent intent = new Intent(this, FlashlightIntentService.class);
        intent.setAction(FlashlightIntentService.ACTION_TURN_OFF);
        startService(intent);
    }
    super.onPause();
}

确定资源是否已被释放并不费时,但如果你确保清理资源,将会有良好的用户体验。

其余的只是基于原生Android API的简单Android应用程序。有一个常见问题,“如何增加或减小亮度?”嗯,Android原生不支持此功能,所以你需要编写一个原生应用程序来支持它,使用NDK。HTC智能手机为其自己的应用程序支持此功能,并要求root访问权限。大多数设备都不提供root访问权限,你不应该依赖root访问权限来提供服务。要么使用NDK,要么不提供此功能。在我看来,这个功能没用!

希望你喜欢这篇文章。如果你觉得有些地方困难或容易,请在评论中告诉我。:-)

© . All rights reserved.