Android学习笔记之——将程序运行到手机上

之前的博文一直是直接在模拟器上运行程序的,本博文试试在手机上测试程序

目录

将程序运行到手机上

通知(Notification)

通知的基本用法

Demo Test

通知的进阶技巧

通知的高级功能

调用摄像头和相册

从相册中选择照片


将程序运行到手机上

想要将程序运行到手机上,我们需要先通过数据线把手机连接到电脑上。然后进入到设置→开发者选项界面,并在这个界面中勾选中USB调试选项,如下图所示。

此时已经可以看到有两个选项,确定程序运行到哪

运行博文《 Android 学习笔记之——服务中的下载功能 》中的代码

然后类似于之前模拟器,直接把代码运行到手机上就ok了哈~

通知(Notification)

通知(Notification)是Android 系统中比较有特色的一个功能,当某个应用程序希望向用户发出一些提示信息,而该应用程序又不在前台运行时,就可以借助通知来实现。发出一条通知后,手机最上方的状态栏中会显示一个通知的图标,下拉状态栏后可以看到通知的详细内容。

一般程序进入后台后,才希望使用通知。

通知的基本用法

首先需要一个NotificationManager来对通知进行管理,可以调用Context的getSystemService() 方法获取到。getSystemService() 方法接收一个字符串参数用于确定获取系统的哪个服务,这里我们传入Context.NOTIFICATION_SERVICE 即可。

因此,获取NotificationManager的实例就可以写成:

NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

接下来需要使用一个Builder构造器来创建Notification 对象,

Notification notification = new NotificationCompat.Builder(context).build();

当然,上述代码只是创建了一个空的Notification 对象,并没有什么实际作用,我们可以在最终的build() 方法之前连缀任意多的设置方法来创建一个丰富的Notification 对象,先来看一些最基本的设置:

Notification notification = new NotificationCompat.Builder(context)
    .setContentTitle("This is content title")
    .setContentText("This is content text")
    .setWhen(System.currentTimeMillis())
    .setSmallIcon(R.drawable.small_icon)
    .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawable.large_icon))
    .build();

setContentTitle() 方法用于指定通知的标题内容,下拉系统状态栏就可以看到这部分内容。

setContentText() 方法用于指定通知的正文内容,同样下拉系统状态栏就可以看到这部分内容。

setWhen() 方法用于指定通知被创建的时间,以毫秒为单位,当下拉系统状态栏时,这里指定的时间会显示在相应的通知上。

setSmallIcon() 方法用于设置通知的小图标,注意只能使用纯alpha图层的图片进行设置,小图标会显示在系统状态栏上。

setLargeIcon() 方法用于设置通知的大图标,当下拉系统状态栏时,就可以看到设置的大图标了。

以上工作都完成之后,只需要调用NotificationManager的notify() 方法就可以让通知显示出来了。notify() 方法接收两个参数,第一个参数是id ,要保证为每个通知所指定的id 都是不同的。第二个参数则是Notification 对象,这里直接将我们刚刚创建好的Notification对象传入即可。因此,显示一个通知就可以写成:

manager.notify(1, notification);

Demo Test

新建一个NotificationTest项目,并修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/send_notice"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Send notice" />

</LinearLayout>

按照《第一行代码》中会报错入下:

修改代码为

package com.example.notificationtest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button sendNotice = (Button) findViewById(R.id.send_notice);
        sendNotice.setOnClickListener(this);
    }

    @Override
    public void onClick (View v) {
        switch (v.getId()) {
            case R.id.send_notice:
                NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                Notification.Builder builder= new Notification.Builder(this);
                builder.setContentTitle("This is content title")
                        .setContentText("This is content text")
                        .setWhen(System.currentTimeMillis())
                        .setSmallIcon(R.mipmap.ic_launcher)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(),
                                R.mipmap.ic_launcher));

                //如果需要发送属于某个自定义渠道的通知,你需要在发送通知前创建自定义通知渠道
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                    NotificationChannel channel = new NotificationChannel("1","my_channel", NotificationManager.IMPORTANCE_DEFAULT);
                    channel.enableLights(true);
                    channel.setLightColor(Color.green(1));
                    channel.setShowBadge(true);
                    manager.createNotificationChannel(channel);
                    builder.setChannelId("1");
                }

                Notification notification = builder.build();
                manager.notify(1, notification);
                break;
            default:
                break;
        }
    }



}

参考资料(https://blog.csdn.net/XingTina/article/details/102658408

查阅资料得知,NotificationChannel是Android O新增的通知渠道,其允许您为要显示的每种通知类型创建用户可自定义的渠道

如果需要发送属于某个自定义渠道的通知,你需要在发送通知前创建自定义通知渠道

效果如下图

发现一个现象就是,这条消息,是不可以点击的。要想实现通知的点击效果,我们还需要在代码中进行相应的设置,这就涉及了一个新的概念:PendingIntent。

PendingIntent从名字上看起来就和Intent有些类似,它们之间也确实存在着不少共同点。比如它们都可以去指明某一个“意图”,都可以用于启动活动、启动服务以及发送广播等。不同的是,Intent更加倾向于去立即执行某个动作,而PendingIntent更加倾向于在某个合适的时机去执行某个动作。所以,也可以把PendingIntent简单地理解为延迟执行的Intent。

PendingIntent的用法同样很简单,它主要提供了几个静态方法用于获取PendingIntent的实例,可以根据需求来选择是使用getActivity() 方法、getBroadcast() 方法,还是getService() 方法。这几个方法所接收的参数都是相同的:

  • 第一个参数依旧是Context,不用多做解释。
  • 第二个参数一般用不到,通常都是传入0即可。
  • 第三个参数是一个Intent 对象,我们可以通过这个对象构建出PendingIntent的“意图”。
  • 第四个参数用于确定PendingIntent的行为,有LAG_ONE_SHOT 、FLAG_NO_CREATE 、FLAG_CANCEL_CURRENT 和FLAG_UPDATE_CURRENT 这4种值可选,每种值的具体含义你可以查看文档,通常情况下这个参数传入0就可以了。

NotificationCompat.Builder。这个构造器还可以再连缀一个setContentIntent() 方法,接收的参数正是一个PendingIntent 对象。因此,这里就可以通过PendingIntent构建出一个延迟执行的“意图”,当用户点击这条通知时就会执行相应的逻辑。

首先需要准备好另一个活动,右击com.example.notificationtest包→New→Activity→EmptyActivity,新建NotificationActivity,布局起名为notification_layout。

然后修改notification_layout.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textSize="24sp"
        android:text="This is notification layout"
        />
</RelativeLayout>

修改mainactivity.java如下

package com.example.notificationtest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button sendNotice = (Button) findViewById(R.id.send_notice);
        sendNotice.setOnClickListener(this);
    }

    @Override
    public void onClick (View v) {
        switch (v.getId()) {
            case R.id.send_notice:
                //给通知加入点击功能
                Intent intent=new Intent(this, NotificationActivity.class);//启动NotificationActivity的“意图”
                //将构建好的Intent 对象传入到PendingIntent的getActivity() 方法里,
                PendingIntent pi=PendingIntent.getActivity(this, 0,intent,0);

                NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                Notification.Builder builder= new Notification.Builder(this);
                builder.setContentTitle("This is content title")
                        .setContentText("This is content text")
                        .setWhen(System.currentTimeMillis())
                        .setSmallIcon(R.mipmap.ic_launcher)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
                        .setContentIntent(pi);//调用.setContentIntent方法,作为参数传入

                //如果需要发送属于某个自定义渠道的通知,你需要在发送通知前创建自定义通知渠道
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                    NotificationChannel channel = new NotificationChannel("1","my_channel", NotificationManager.IMPORTANCE_DEFAULT);
                    channel.enableLights(true);
                    channel.setLightColor(Color.green(1));
                    channel.setShowBadge(true);
                    manager.createNotificationChannel(channel);
                    builder.setChannelId("1");
                }

                Notification notification = builder.build();
                manager.notify(1, notification);
                break;
            default:
                break;
        }
    }



}

此处有一点就是,点开后, 系统状态上的通知图标还没有消失。实际上,如果我们没有在代码中对该通知进行取消,它就会一直显示在系统的状态栏上。解决的方法有两种,一种是在NotificationCompat.Builder 中再连缀一个setAutoCancel() 方法,一种是显式地调用NotificationManager的cancel() 方法将它取消,接下来两种方法我们都来测试一下:

采用setAutoCancel() 方法。setAutoCancel() 方法传入true ,就表示当点击了这个通知的时候,通知会自动取消掉。

显式地调用NotificationManager的cancel() 方法将它取消

package com.example.notificationtest;

import androidx.appcompat.app.AppCompatActivity;

import android.app.NotificationManager;
import android.os.Bundle;

public class NotificationActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.notification_layout);

        NotificationManager manager=(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        manager.cancel(1);//前面的ID是1
    }
}

通知的进阶技巧

NotificationCompat.Builder 中提供了非常丰富的API来让我们创建出更加多样的通知效果。

通知的高级功能

setStyle() 方法,这个方法允许我们构建出富文本的通知内容。也就是说通知中不光可以有文字和图标,还可以包含更多的东西。setStyle() 方法接收一个NotificationCompat.Style 参数,这个参数就是用来构建具体的富文本信息的,如长文字、图片等。

若将显示的内容设置得比较的长,效果如下:

                        .setContentText("Learn how to build notifications, send and sync data, and use\n" +
                                "voice actions. Get the official Android IDE and developer tools to build\n" +
                                "apps for Android")

可以看到,通知内容是无法显示完整的,多余的部分会用省略号来代替。其实这也很正常,因为通知的内容本来就应该言简意赅,详细内容放到点击后打开的活动当中会更加合适。

此部分的内容参考《第一行代码》里面的代码不能直接运行,再加上这个部分不是特别的重要,所以就不纠结在这了哈~

调用摄像头和相册

首先新建一个CameraAlbumTest项目。然后修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/take_photo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Take Phote"
        />


    <ImageView
        android:id="@+id/picture"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        />

</LinearLayout>

定义完布局后,定义mainactivity

package com.example.cameraalbumtest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;

import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

public class MainActivity extends AppCompatActivity {

    public static final int TAKE_PHOTO=1;

    private ImageView picture;

    private Uri imageUri;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //获取实例
        Button takePhoto=(Button) findViewById(R.id.take_photo);
        picture =(ImageView) findViewById(R.id.picture);

        takePhoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //创建file对象,用于存储拍照后的图片
                File outputImage=new File(getExternalCacheDir(),"output_image.jpg");//把图片进行命名
                //调用getExternalCacheDir()可以得到手机SD卡的应用关联缓存目录
                //所谓的应用关联缓存目录,就是指SD卡中专门用于存放当前应用缓存数据的位置
                //具体的路径是/sdcard/Android/data/<package name>/cache

                //因为从Android 6.0系统开始,读写SD卡被列为了危险权限,
                // 如果将图片存放在SD卡的任何其他目录,都要进行运行时权限处理才行,而使用应用关联目录则可以跳过这一步。
                try {
                    if (outputImage.exists()) {//如果已经存在了图片,则删掉,
                        outputImage.delete();
                    }
                    outputImage.createNewFile();//将图片放入
                }catch (IOException e){
                    e.printStackTrace();
                }

                //获取Uri对象
                //这个Uri对象标识着output_image.jpg这张图片的本地真实路径。
                if (Build.VERSION.SDK_INT >= 24) {
                    //调用FileProvider的getUriForFile() 方法将File 对象转换成一个封装过的Uri对象
                    imageUri = FileProvider.getUriForFile(MainActivity.this,"com.example.cameraalbumtest.fileprovider", outputImage);
                    //FileProvider则是一种特殊的内容提供器,它使用了和内容提供器类似的机制来对数据进行保护,
                    // 可以选择性地将封装过的Uri共享给外部,从而提高了应用的安全性。
                    //第一个参数要求传入Context 对象
                    //第二个参数可以是任意唯一的字符串 (需要在AndroidManifest.xml中声明)
                    //第三个参数则是我们刚刚创建的File 对象

                } else {//若系统的版本低于Android7.0,则调用下面的方法将File对象转换为Uri对象
                    imageUri = Uri.fromFile(outputImage);
                }

                //启动相机程序
                Intent intent= new Intent("android.media.action.IMAGE_CAPTURE");
                intent.putExtra(MediaStore.EXTRA_OUTPUT,imageUri);//指定图片的输出地址
                startActivityForResult(intent,TAKE_PHOTO);//调用startActivityForResult() 来启动活动。
            }
        });

    }

    //使用startActivityForResult() 来启动活动的,
    // 因此拍完照后会有结果返回到onActivityResult() 方法中。
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case TAKE_PHOTO:
                if (resultCode == RESULT_OK) {//如果拍照成功
                    try {
                        // 将拍摄的照片显示出来
                        //可以调用BitmapFactory的decodeStream() 方法将output_image.jpg这张照片解析成Bitmap 对象
                        Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
                        picture.setImageBitmap(bitmap);//将Bitmap对象,设置到ImageView中显示出来。
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
                break;
            default:
                break;
        }
    }
}

由于采用到了内容提供器(FileProvider),那么我们自然要在AndroidManifest.xml中对内容提供器进行注册了,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.cameraalbumtest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.example.cameraalbumtest.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

其中,android:name 属性的值是固定的,android:authorities 属性的值必须要和刚才FileProvider.getUriForFile() 方法中的第二个参数一致。另外,这里还在<provider> 标签的内部使用<meta-data> 来指定Uri 的共享路径,并引用了一个@xml/file_paths 资源。当然,这个资源现在还是不存在的,下面我们就来创建它。

右击res 目录→New→Directory,创建一个xml目录,接着右击xml目录→New→File,创建一个file_paths.xml文件。然后修改file_paths.xml文件中的内容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images" path="" />
</paths>

其中,external-path 就是用来指定Uri 共享的,name 属性的值可以随便填,path 属性的值表示共享的具体路径。这里设置空值就表示将整个SD卡进行共享。

同时声明一下访问SD卡的权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

运行会发现报错。感觉第一行代码里面的程序的版本还是有点低哎。比如这里,必须填路径,但是里面就说空都可以(已经遇到过好多次里面代码运行不了的情况了~)

修改AndroidManifest.xml中的fileprovider后有

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.cameraalbumtest">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="***.fileProvider"
            android:exported="false"
            android:grantUriPermissions="true"
            xmlns:tools="http://schemas.android.com/tools"
            tools:replace="android:authorities">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>


    </application>

</manifest>

再修改

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images" path="/" />
</paths>

参考资料https://www.jianshu.com/p/8af9a3790858

代码就没有问题了哈~

然而点击take photo却奔溃。。。。

好了,改为就没有问题了

android:authorities="com.example.cameraalbumtest.fileprovider"
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.cameraalbumtest">

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.example.cameraalbumtest.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true"
            xmlns:tools="http://schemas.android.com/tools"
            tools:replace="android:authorities">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>


    </application>

</manifest>

从相册中选择照片

虽然调用摄像头拍照既方便又快捷,但我们并不是每次都需要去当场拍一张照片的。因为每个人的手机相册里应该都会存有许许多多张照片,直接从相册里选取一张现有的照片会比打开相机拍一张照片更加常用。一个优秀的应用程序应该将这两种选择方式都提供给用户,由用户来决定使用哪一种。下面我们就来看一下,如何才能实现从相册中选择照片的功能。

还是在CameraAlbumTest项目的基础上进行修改,编辑activity_main.xml文件,在布局中添加一个按钮用于从相册中选择照片,代码如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/take_photo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Take Photo"
        />

    <ImageView
        android:id="@+id/picture"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        />

    <Button
        android:id="@+id/choose_from_album"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Choose from Album"
        />


</LinearLayout>
package com.example.cameraalbumtest;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;

import android.Manifest;
import android.annotation.TargetApi;
import android.content.ContentUris;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

public class MainActivity extends AppCompatActivity {

    public static final int TAKE_PHOTO=1;//拍照
    public static final int CHOOSE_PHOTO=2;//从相册取照片

    private ImageView picture;

    private Uri imageUri;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //获取实例
        Button takePhoto=(Button) findViewById(R.id.take_photo);
        picture =(ImageView) findViewById(R.id.picture);

        takePhoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //创建file对象,用于存储拍照后的图片
                File outputImage=new File(getExternalCacheDir(),"output_image.jpg");//把图片进行命名
                //调用getExternalCacheDir()可以得到手机SD卡的应用关联缓存目录
                //所谓的应用关联缓存目录,就是指SD卡中专门用于存放当前应用缓存数据的位置
                //具体的路径是/sdcard/Android/data/<package name>/cache

                //因为从Android 6.0系统开始,读写SD卡被列为了危险权限,
                // 如果将图片存放在SD卡的任何其他目录,都要进行运行时权限处理才行,而使用应用关联目录则可以跳过这一步。
                try {
                    if (outputImage.exists()) {//如果已经存在了图片,则删掉,
                        outputImage.delete();
                    }
                    outputImage.createNewFile();//将图片放入
                }catch (IOException e){
                    e.printStackTrace();
                }

                //获取Uri对象
                //这个Uri对象标识着output_image.jpg这张图片的本地真实路径。
                if (Build.VERSION.SDK_INT >= 24) {
                    //调用FileProvider的getUriForFile() 方法将File 对象转换成一个封装过的Uri对象
                    imageUri = FileProvider.getUriForFile(MainActivity.this,"com.example.cameraalbumtest.fileprovider", outputImage);
                    //FileProvider则是一种特殊的内容提供器,它使用了和内容提供器类似的机制来对数据进行保护,
                    // 可以选择性地将封装过的Uri共享给外部,从而提高了应用的安全性。
                    //第一个参数要求传入Context 对象
                    //第二个参数可以是任意唯一的字符串 (需要在AndroidManifest.xml中声明)
                    //第三个参数则是我们刚刚创建的File 对象
                } else {//若系统的版本低于Android7.0,则调用下面的方法将File对象转换为Uri对象
                    imageUri = Uri.fromFile(outputImage);
                }


                //启动相机程序
                Intent intent= new Intent("android.media.action.IMAGE_CAPTURE");
                intent.putExtra(MediaStore.EXTRA_OUTPUT,imageUri);//指定图片的输出地址
                startActivityForResult(intent,TAKE_PHOTO);//调用startActivityForResult() 来启动活动。
            }
        });

        //从相册中取图片
        Button chooseFromAlbum=(Button) findViewById(R.id.choose_from_album);
        chooseFromAlbum.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {//定义该按钮点击事件
                //申请一个运行时权限处理
                //权限WRITE_EXTERNAL_STORAGE表示同时授予程序对SD卡读和写的能力。
                if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED)
                {
                    ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
                }else {//授予了权限后,则调用openAlbum方法来获取图片
                    openAlbum();
                }

            }
        });

    }

    //定义openAlbum()方法来获取图片
    private void openAlbum(){
        Intent intent =new Intent("android.intent.action.GET_CONTENT");//构建一个intent对象,并将它的action指定
        intent.setType("image/*");
        startActivityForResult(intent, CHOOSE_PHOTO);//打开相册程序,选择照片
        //给第二个参数传入的值变成了CHOOSE_PHOTO
        // 这样当从相册选择完图片回到onActivityResult() 方法时
        // 就会进入CHOOSE_PHOTO 的case 来处理图片。
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions,int[] grantResults){
        switch (requestCode){
            case 1:
                if (grantResults.length>0 && grantResults[0]==PackageManager.PERMISSION_GRANTED){
                    openAlbum();
                }else {
                    Toast.makeText(this,"You denied the permission",Toast.LENGTH_SHORT).show();
                }
                break;
            default:
                break;
        }
    }


    //使用startActivityForResult() 来启动活动的,
    // 因此拍完照后会有结果返回到onActivityResult() 方法中。
    //在此函数中显示图像
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case TAKE_PHOTO://拍照
                if (resultCode == RESULT_OK) {//如果拍照成功
                    try {
                        // 将拍摄的照片显示出来
                        //可以调用BitmapFactory的decodeStream() 方法将output_image.jpg这张照片解析成Bitmap 对象
                        Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
                        picture.setImageBitmap(bitmap);//将Bitmap对象,设置到ImageView中显示出来。
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case CHOOSE_PHOTO://打开相册
                if (resultCode == RESULT_OK){
                    //判断手机的系统的版本号
                    if (Build.VERSION.SDK_INT>=19){
                        //4.4及以上系统使用这个方法处理图片
                        handleImageOnKitKat(data);
                    } else {
                        // 4.4以下系统使用这个方法处理图片
                        handleImageBeforeKitKat(data);
                    }
                }
            default:
                break;
        }
    }

    //因为Android系统从4.4版本开始,选取相册中的图片不再返回图片真实的Uri了,而是一个封装过的Uri
    // 因此如果是4.4版本以上的手机就需要对这个Uri进行解析才行。
    @TargetApi(19)
    private void handleImageOnKitKat(Intent data) {//用于解析Android4.4版本以上的封装过的Uri
        String imagePath = null;
        Uri uri = data.getData();

        if (DocumentsContract.isDocumentUri(this, uri)) {
            // 如果是document类型的Uri,则通过document id处理
            String docId = DocumentsContract.getDocumentId(uri);
            if("com.android.providers.media.documents".equals(uri.getAuthority())) {
                //如果Uri的authority是media格式的话,document id 还需要再进行一次解析
                //要通过字符串分割的方式取出后半部分才能得到真正的数字id
                String id = docId.split(":")[1]; // 解析出数字格式的id
                String selection = MediaStore.Images.Media._ID + "=" + id;
                imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection);
            } else if ("com.android.providers.downloads.documents".equals(uri. getAuthority())) {
                Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
                imagePath = getImagePath(contentUri, null);
            }
        } else if ("content".equalsIgnoreCase(uri.getScheme())) {
            // 如果是content类型的Uri,则使用普通方式处理
            imagePath = getImagePath(uri, null);
        } else if ("file".equalsIgnoreCase(uri.getScheme())) {
            // 如果是file类型的Uri,直接获取图片路径即可
            imagePath = uri.getPath();
        }
        displayImage(imagePath); // 根据图片路径显示图片
    }

    //它的Uri是没有封装过的,不需要任何解析
    private void handleImageBeforeKitKat(Intent data) {
        Uri uri = data.getData();
        String imagePath = getImagePath(uri, null);//直接将Uri传入到getImagePath() 方法当中就能获取到图片的真实路径了
        displayImage(imagePath);//让图片显示到界面上
    }

    //获取到图片的真实路径了
    private String getImagePath(Uri uri, String selection) {
        String path = null;
        // 通过Uri和selection来获取真实的图片路径
        Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
        if (cursor != null) {
            if (cursor.moveToFirst()) {
                path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
            }
            cursor.close();
        }
        return path;
    }


    //将图片显示到界面上
    private void displayImage(String imagePath) {
        if (imagePath != null) {
            Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
            picture.setImageBitmap(bitmap);
        } else {
            Toast.makeText(this, "failed to get image", Toast.LENGTH_SHORT).show();
        }
    }
}

运行结果如下图所示

猜你喜欢

转载自blog.csdn.net/gwplovekimi/article/details/105950207