android 拍照_android拍照 - CSDN
精华内容
参与话题
  • android拍照的两种方法

    千次阅读 2018-07-11 09:59:56
    调用系统摄像头来拍照首先,找到AndroidManifest.xml文件里加入用户权限<uses-permission android:name="android.permission.CAMERA"></uses-permission> <uses-...

    android系统的照相功能,已实现2种方法,可供大家参考:

    1.调用系统摄像头来拍照

    首先,找到AndroidManifest.xml文件里加入用户权限

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

     <uses-feature android:name="android.hardware.camera" />
     <uses-feature android:name="android.hardware.camera.autofocus" />

    其次,在主类java文件里加入2个控件(button和imageview),是用来触发按钮事件和显示图片的,纯是个人爱好

    final int TAKE_PICTURE = 1;//为了表示返回方法中辨识你的程序打开的相机

    关键是这里:startActivityForResult(new Intent("android.media.action.IMAGE_CAPTURE"), TAKE_PICTURE);

    是打开系统自带相机,以下是处理拍照得到的数据,将数据保存下来

        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            if (requestCode == TAKE_PICTURE) {
                if (resultCode == RESULT_OK) {
                    Bitmap bm = (Bitmap) data.getExtras().get("data");
                    img.setImageBitmap(bm);//想图像显示在ImageView视图上,private ImageView img;
                    File myCaptureFile = new File("sdcard/123456.jpg");
                    try {
         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(myCaptureFile));
          /* 采用压缩转档方法 */
               bm.compress(Bitmap.CompressFormat.JPEG, 80, bos);
              
               /* 调用flush()方法,更新BufferStream */
               bos.flush();
              
               /* 结束OutputStream */
               bos.close();
        } catch (FileNotFoundException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
         
        } catch (IOException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
        }
                }
            }
    }

    这样就能实现调用系统自带的摄像头了,很简单的操作。

    2.自己写程序来保存照片

    照片格局文件lay.xml里要先进行这些定义

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:orientation="vertical"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
    >
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="130px"
        android:paddingRight="200px"
      >
      <SurfaceView
        android:id="@+id/mSurfaceView1"
        android:visibility="visible"
        android:layout_width="320px"
        android:layout_height="240px">
      </SurfaceView>
      </LinearLayout>
      </LinearLayout>

     

    其中SurfaceView是用来进行预览的,

    在Oncreat函数里初始化一系列的值:

     requestWindowFeature(Window.FEATURE_NO_TITLE);
     setContentView(R.layout.lay);

     /* 取得屏幕解析像素 */
         DisplayMetrics dm = new DisplayMetrics();
         getWindowManager().getDefaultDisplay().getMetrics(dm);
        // mImageView01 = (ImageView) findViewById(R.id.myImageView1);
        
         /* 以SurfaceView作为相机Preview之用 */
         mSurfaceView01 = (SurfaceView) findViewById(R.id.mSurfaceView1);
        
         /* 绑定SurfaceView,取得SurfaceHolder对象 */
         mSurfaceHolder01 = mSurfaceView01.getHolder();
        
         /* Activity必须实现SurfaceHolder.Callback */
         mSurfaceHolder01.addCallback(takephoto.this);
        
         /*
          * 以SURFACE_TYPE_PUSH_BUFFERS(3)
          * 作为SurfaceHolder显示类型
          * */
         mSurfaceHolder01.setType
         (SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

     

    首先进行初始化照相机的功能函数和参数设置:

    private Camera mCamera01;

    mCamera01 = Camera.open();

     /* 创建Camera.Parameters对象 */
        Camera.Parameters parameters = mCamera01.getParameters();
            
        /* 设置相片格式为JPEG */
        parameters.setPictureFormat(PixelFormat.JPEG);
        Log.i(TAG, "pic is jpeg");
            
        /* 指定preview的屏幕大小 */
        parameters.setPreviewSize(320, 240);
        Log.i(TAG, "pic pingmu fenbianlv");
            
        /* 设置图片分辨率大小 */
        parameters.setPictureSize(1024, 768);
        Log.i(TAG, "pic tupian fenbianlv");
            
        /* 将Camera.Parameters设置予Camera */
        mCamera01.setParameters(parameters);
            
        /* setPreviewDisplay唯一的参数为SurfaceHolder */
        mCamera01.setPreviewDisplay(mSurfaceHolder01);
            
        /* 立即运行Preview */
        mCamera01.startPreview();

    初始化成功后就可以进行拍照了,拍照函数依然是通过调用camera类的函数来实现

    mCamera01.takePicture
           (shutterCallback, rawCallback, jpegCallback);

    只需实现jpegCallback这个回调函数来就行解码、保存即可,前2个参数可以直接设为null,不过系统一般会自动帮你把这些都写进来的

     private PictureCallback jpegCallback = new PictureCallback()
       {
         public void onPictureTaken(byte[] _data, Camera _camera)
         {
           // TODO Handle JPEG image data
          
           /* onPictureTaken传入的第一个参数即为相片的byte */
           Bitmap bm = BitmapFactory.decodeByteArray
                       (_data, 0, _data.length);
          
           /* 创建新文件 */
                  picname = "sdcard/1234566.jpg";//要保存在哪里,路径你自己设
           File myCaptureFile = new File(picname);
           try
           {
             BufferedOutputStream bos = new BufferedOutputStream
             (new FileOutputStream(myCaptureFile));
            
             /* 采用压缩转档方法 */
             bm.compress(Bitmap.CompressFormat.JPEG, 80, bos);
            
             /* 调用flush()方法,更新BufferStream */
             bos.flush();
            
             /* 结束OutputStream */
             bos.close();
            
             /* 将拍照下来且存储完毕的图文件,显示出来 */
             //mImageView01.setImageBitmap(bm);
            
             /* 显示完图文件,立即重置相机,并关闭预览 */
             resetCamera();
             
              }
           catch (Exception e)
           {
             Log.e(TAG, e.getMessage());
           }
         }
       };

     

    拍照完了要重置照相机,然后可以继续拍照

    /* 相机重置 */
       private void resetCamera()
       {
         if (mCamera01 != null && bIfPreview)
         {
           mCamera01.stopPreview();
           /* 扩展学习,释放Camera对象 */
           mCamera01.release();
           mCamera01 = null;
           bIfPreview = false;
         }
       }

     

    2种拍照方式的比较

    1.调用系统自带的照相机,照片格式大小只有几种选择,照片拍出来比较大,而自己程序实现的话可以调节照片大小为任意尺寸,图片的容量可以调节

    2.调用系统的简单,而且外观一般比自己设置的要好看

    3.调用系统的操作简单、方便,不易出错,自己编程的话需要注意,容易引起系统出错意外终止

    展开全文
  • 实现思路: 把预览的SurfaceView的宽高设置为肉眼看不出的值,例如0.1dp,其他的就是自定义相机的标准步骤了!   当然,网上自定义相机的文章多的去了,我是不可能再做老生常谈的事的,在这里我推荐一个很好用的...

    实现思路:

    把预览的SurfaceView的宽高设置为肉眼看不出的值,例如0.1dp,其他的就是自定义相机的标准步骤了!

     

    当然,网上自定义相机的文章多的去了,我是不可能再做老生常谈的事的,在这里我推荐一个很好用的第三方库,也许很多人已经了解过并且用过,没用过的同学可以试试,亲测好用。

    废话不多说,先上源码地址:https://github.com/CameraKit/camerakit-android

     

    这里简单介绍一下用法:

    1、在gradle里添加库的依赖:compile 'com.wonderkiln:camerakit:0.13.1'

    2、布局文件代码:

    <LinearLayout
                android:id="@+id/ll_content"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
     
                <Button
                    android:id="@+id/btn_test"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="测试"
                    android:layout_gravity="center_horizontal"/>
    
                <com.wonderkiln.camerakit.CameraView
                    android:id="@+id/camera"
                    android:layout_width="0.1dp"
                    android:layout_height="0.1dp"
                    android:adjustViewBounds="true"
                    camerakit:ckFacing="front" />
    
            </LinearLayout>

    camerakit:ckFacing="front" 表示用前置摄像头,其他属性请参照官方文档。

    注意:宽高不能设置为0,否则不能拍照。

    3、Java代码

    public class MainActivity extends BaseActivity {
        @BindView(R.id.btn_test)
        Button btnTest;
        @BindView(R.id.camera)
        CameraView cameraView;
        @BindView(R.id.ll_content)
        LinearLayout llContent;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ButterKnife.bind(this);
            initView();
        }    
    
        @Override
        protected void onResume() {
            super.onResume();
            cameraView.start();
        }
    
        @Override
        protected void onPause() {
            // TODO Auto-generated method stub
            cameraView.stop();
            super.onPause();
        }
    
        private void initView() {
            //设置回调
            cameraView.addCameraKitListener(new CameraKitEventListener() {
                @Override
                public void onEvent(CameraKitEvent cameraKitEvent) {
    
                }
    
                @Override
                public void onError(CameraKitError cameraKitError) {
    
                }
    
                @Override
                public void onImage(CameraKitImage cameraKitImage) {
                    ImageView imageView = new ImageView(MainActivity.this);
                    imageView.setImageBitmap(cameraKitImage.getBitmap());
                    llContent.addView(imageView);
                }
    
                @Override
                public void onVideo(CameraKitVideo cameraKitVideo) {
    
                }
            });
        }
    
        @OnClick(R.id.btn_test)
        public void onViewClicked() {
           //拍照
           cameraView.captureImage();
        }
    
    }

     

    展开全文
  • 现在一般的手机应用...调用系统相机拍照后crash,或者返回RESULT_CANCEL(0)3.选择相片后得到的Uri为空或者为Uri后半段为资源ID(%1234567这种)4.调用系统裁剪后crash5.小米手机的特别情况还有许多小问题,大多都...

    现在一般的手机应用都会有上传头像的功能,我在实现这个功能的时候遇到很多问题,这里专门记录一下。

    add 2018/5/10 21:05

    先列举一下我出现过的问题:

    1.运行时权限

    2.调用系统相机拍照后crash,或者返回RESULT_CANCEL(0)

    3.选择相片后得到的Uri为空或者为Uri后半段为资源ID(%1234567这种)

    4.调用系统裁剪后crash

    5.小米手机的特别情况

    还有许多小问题,大多都是上面问题引起的并发症,就不一一列举了。


    先上代码,慢慢讲。


    1.布局


    只关注头像那一栏就可以了,点击头像后会弹出选择页面。PopupWindow的实现如下:

    1.1    新建layout文件pop_item

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#66000000">
    
        <LinearLayout
            android:id="@+id/ll_pop"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="15dp"
            android:layout_marginRight="15dp"
            android:orientation="vertical"
            android:layout_alignParentBottom="true">
            <Button
                android:id="@+id/icon_btn_camera"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/white_btn_top"
                android:textColor="@color/colorMainGreen"
                android:text="拍照"/>
            <View
                android:layout_width="match_parent"
                android:layout_height="1dp"
                />
            <Button
                android:id="@+id/icon_btn_select"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/white_btn_bottom"
                android:textColor="@color/colorMainGreen"
                android:text="从相册选择"/>
            <Button
                android:id="@+id/icon_btn_cancel"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="10dp"
                android:layout_marginBottom="15dp"
                android:background="@drawable/white_btn"
                android:textColor="@color/colorMainGreen"
                android:text="取消"/>
        </LinearLayout>
    
    </RelativeLayout>

            三个Button分别对应三个按钮,中间的View是两个按钮之间的线,colorMainGreen是

    <color name="colorMainGreen">#40cab3</color>

    1.2    可以看到三个按钮分别是上圆角,下圆角,全圆角,在drawable中新建3个xml,绘制Button样式

    white_btn_top

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android">
        <solid android:color="@android:color/white" />
        <corners android:topLeftRadius="10dp"
            android:topRightRadius="10dp"
            android:bottomRightRadius="0dp"
            android:bottomLeftRadius="0dp"/>
        <stroke android:width="0dp" android:color="@android:color/white" />
    </shape>

    white_btn_bottom

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android">
        <solid android:color="@android:color/white" />
        <corners android:topLeftRadius="0dp"
            android:topRightRadius="0dp"
            android:bottomRightRadius="10dp"
            android:bottomLeftRadius="10dp"/>
        <stroke android:width="0dp" android:color="@android:color/white" />
    </shape>

    while_btn

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android">
        <solid android:color="@android:color/white"/>
        <corners android:radius="10dp"/>
        <stroke android:width="0dp" android:color="@android:color/white" />
    </shape>

            简单解释一下shape的用法,每一组< />中制定一个属性(不知道是不是这么叫,但是是这个意思),在每条属性中可以指定更多的细节。solid指定填充,corners指定圆角,在corners中的radius指定了圆角的半径,stroke用于描边。

    1.3    PhotoPopupWindow布局都写好了,现在我们要写自己的PhotoPopupWindow类加载它,同时给他添加点击事件。新建一个package,命名为popup(这样做的目的是使得代码结构清晰),在这个包下新建PhotoPopupWindow类,继承PopupWindow

    public class PhotoPopupWindow extends PopupWindow {
        private static final String TAG = "PhotoPopupWindow";
        private View mView; // PopupWindow 菜单布局
        private Context mContext; // 上下文参数
        private View.OnClickListener mSelectListener; // 相册选取的点击监听器
        private View.OnClickListener mCaptureListener; // 拍照的点击监听器
    
        public PhotoPopupWindow(Activity context, View.OnClickListener selectListener, View.OnClickListener captureListener) {
            super(context);
            this.mContext = context;
            this.mSelectListener = selectListener;
            this.mCaptureListener = captureListener;
            Init();
        }
    
        /**
         * 设置布局以及点击事件
         */
        private void Init() {
            LayoutInflater inflater = (LayoutInflater) mContext
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            assert inflater != null;
            mView = inflater.inflate(R.layout.pop_item, null);
            Button btn_camera = (Button) mView.findViewById(R.id.icon_btn_camera);
            Button btn_select = (Button) mView.findViewById(R.id.icon_btn_select);
            Button btn_cancel = (Button) mView.findViewById(R.id.icon_btn_cancel);
    
            btn_select.setOnClickListener(mSelectListener);
            btn_camera.setOnClickListener(mCaptureListener);
            btn_cancel.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    dismiss();
                }
            });
    
            // 导入布局
            this.setContentView(mView);
            // 设置动画效果
            this.setAnimationStyle(R.style.popwindow_anim_style);
            this.setWidth(WindowManager.LayoutParams.MATCH_PARENT);
            this.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
            // 设置可触
            this.setFocusable(true);
            ColorDrawable dw = new ColorDrawable(0x0000000);
            this.setBackgroundDrawable(dw);
            // 单击弹出窗以外处 关闭弹出窗
            mView.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    int height = mView.findViewById(R.id.ll_pop).getTop();
                    int y = (int) event.getY();
                    if (event.getAction() == MotionEvent.ACTION_UP) {
                        if (y < height) {
                            dismiss();
                        }
                    }
                    return true;
                }
            });
        }
    }

            代码是很主流的写法,没什么特别的。TAG常量会出现在每一个JAVA类中,即使我用不到他。这样写便于区别Log发生的位置。两个OnClickListener需要在实例化的时候实现。

    1.4    弹出框写好了,接下来是头像页面的布局文件

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/backgroundWhite"
        tools:context=".activity.UserInfoActivity">
    
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar_userinfo"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:title="用户信息"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
        </android.support.v7.widget.Toolbar>
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            >
            <RelativeLayout
                android:id="@+id/user_head"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_marginTop="1dp"
                android:background="@drawable/bg_info_rl">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginLeft="15dp"
                    android:textSize="16sp"
                    android:text="头像 "/>
                <de.hdodenhof.circleimageview.CircleImageView
                    android:id="@+id/user_head_iv"
                    android:layout_width="40dp"
                    android:layout_height="40dp"
                    android:layout_alignParentEnd="true"
                    android:layout_alignParentRight="true"
                    android:layout_centerVertical="true"
                    android:layout_marginRight="25dp"
                    app:civ_border_color="#F3F3F3"
                    app:civ_border_width="1dp"/>
            </RelativeLayout>
    
            <RelativeLayout
                android:id="@+id/user_nick_name"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:layout_marginTop="1dp"
                android:background="@drawable/bg_info_rl">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginLeft="15dp"
                    android:textSize="16sp"
                    android:text="昵称"/>
                <TextView
                    android:id="@+id/user_nick_name_TV"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginRight="10dp"
                    android:textSize="16sp"
                    android:layout_toLeftOf="@id/user_nick_name_IV" />
                <ImageView
                    android:id="@+id/user_nick_name_IV"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:src="@drawable/ic_chevron_right_black_24dp"
                    android:layout_alignParentRight="true"
                    android:layout_marginRight="15dp"
                    />
            </RelativeLayout>
    
            <RelativeLayout
                android:id="@+id/user_gender"
                android:layout_width="match_parent"
                android:layout_height="60dp"
                android:layout_marginTop="1dp"
                android:background="@drawable/bg_info_rl">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginLeft="15dp"
                    android:textSize="16sp"
                    android:text="性别"/>
                <TextView
                    android:id="@+id/user_gender_TV"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:gravity="center_vertical"
                    android:layout_marginRight="10dp"
                    android:textSize="16sp"
                    android:layout_toLeftOf="@id/user_gender_IV" />
                <ImageView
                    android:id="@+id/user_gender_IV"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:src="@drawable/ic_chevron_right_black_24dp"
                    android:layout_marginRight="15dp"
                    android:layout_alignParentRight="true"
                    />
            </RelativeLayout>
    
        </LinearLayout>
    </LinearLayout>
            我解释一下有疑问的地方。Toolbar是谷歌推荐的标题栏,比Actionbar具有更好的拓展性。CircleImageView是一个开源库,自行百度GitHub地址,用于产生圆形图片。这一部分的设计可以参考郭霖大神的《第一行代码》或者他的博客,有关Material design的内容(在十一章好像),他讲解的很详细。backgroundWhite是
    <color name="backgroundWhite">#EBEBEB</color>
    tools:context指定了该layout将在哪个activiy展示,指定了tools:context的layout可以动态预览布局,也就是说我在acticity的onCreate里改变了某个控件,我可以在不run的情况下预览这个改变的效果。tools还有一些别的功能,我懂的也不多,就不介绍了。bg_info_rl是可变背景,这里我只是简单设置了点击和松开时候的颜色,代码这段结束后贴,ic_chevron_right_black_24dp是Material design提供的小图标,样式是“>”,大家可以自行搜索,这是谷歌官方提供的免费素材包,可以在谷歌MD的官网或者GitHub上获取。
    bg_info_rl
    <?xml version="1.0" encoding="UTF-8"?>
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:state_window_focused="false" android:drawable="@drawable/bg_info_rl_normal" />
        <item android:state_pressed="true" android:drawable="@drawable/bg_info_rl_pressed" />
    </selector>
    bg_info_rl_normal
    <?xml version="1.0" encoding="UTF-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android">
        <solid android:color="@color/white" />
    </shape>
    bg_info_rl_pressed
    <?xml version="1.0" encoding="UTF-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android">
        <solid android:color="@color/light_grey" />
    </shape>
    color
    <color name="white">#FFFFFF</color>
    <color name="light_grey">#EAEAEA</color> <!--浅灰色-->

    2.运行时权限的问题

    add 2018/5/11 10:18

            android 6.0引入了运行时权限,用户不必在安装时授予所有权限,可以在使用到相关功能时再授权。普通权限不需要运行时申请,危险权限则必须,否则程序会crash掉。

            头像功能需要以下三个权限

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

            读写SD卡,使用相机,他们都是危险权限,接下来新建UserInfoAcitivity,继承AppCompatActivity 继承View.OnClickListener接口。

    变量和常量

    private static final String TAG = "UserInfoActivity";
        private static final int REQUEST_IMAGE_GET = 0;
        private static final int REQUEST_IMAGE_CAPTURE = 1;
        private static final int REQUEST_SMALL_IMAGE_CUTTING = 2;
        private static final int REQUEST_CHANGE_USER_NICK_NAME = 10;
        private static final String IMAGE_FILE_NAME = "user_head_icon.jpg";
    
        PhotoPopupWindow mPhotoPopupWindow;
        TextView textView_user_nick_name;
        TextView textView_user_gender;
        CircleImageView circleImageView_user_head;
    初始化布局
    @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_userinfo);
    
            textView_user_nick_name = findViewById(R.id.user_nick_name_TV);
            textView_user_gender = findViewById(R.id.user_gender_TV);
            circleImageView_user_head = findViewById(R.id.user_head_iv);
            //InfoPrefs自己封装的一个 SharedPreferences 工具类
            //init()指定文件名,getData(String key)获取key对应的字符串,getIntData(int key)获取key对应的int
            InfoPrefs.init("user_info");
            refresh();
    
            RelativeLayout relativeLayout_user_nick_name = findViewById(R.id.user_nick_name);
            relativeLayout_user_nick_name.setOnClickListener(this);
    
            RelativeLayout relativeLayout_user_gender = findViewById(R.id.user_gender);
            relativeLayout_user_gender.setOnClickListener(this);
    
            RelativeLayout relativeLayout_user_head = findViewById(R.id.user_head);
            relativeLayout_user_head.setOnClickListener(this);
            //初始化 toolbar
            Toolbar toolbar = findViewById(R.id.toolbar_userinfo);
            setSupportActionBar(toolbar);
            ActionBar actionBar = getSupportActionBar();
    
            if (actionBar != null) {
                //指定toolbar左上角的返回按钮,这个按钮的id是home(无法更改)
                actionBar.setDisplayHomeAsUpEnabled(true);
                //actionBar.setHomeAsUpIndicator();
            }
        }


    public void refresh(){
            textView_user_nick_name.setText(InfoPrefs.getData(Constants.UserInfo.NAME));
            textView_user_gender.setText(InfoPrefs.getData(Constants.UserInfo.GENDER));
            showHeadImage();
            //circleImageView_user_head.setImageURI();
        }

            为每一个RelativeLayout都添加了点击事件,这里我们只关注头像的点击事件。出现的工具类我只解释功能,完整代码可以在文末我的GitHub获取,这个项目是一个手机桌面宠物的demo,包括悬浮窗、蓝牙、闹钟等,合作写的,代码的风格不太一致,见谅。

    @Override
        public void onClick(View v) {
            switch(v.getId()){
                case R.id.user_head:
                    //创建存放头像的文件夹
                    PictureUtil.mkdirMyPetRootDirectory();
                    mPhotoPopupWindow = new PhotoPopupWindow(UserInfoActivity.this, new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            // 文件权限申请
                            if (ContextCompat.checkSelfPermission(UserInfoActivity.this,
                                    Manifest.permission.WRITE_EXTERNAL_STORAGE)
                                    != PackageManager.PERMISSION_GRANTED) {
                                // 权限还没有授予,进行申请
                                ActivityCompat.requestPermissions(UserInfoActivity.this,
                                        new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 200); // 申请的 requestCode 为 200
                            } else {
                                // 如果权限已经申请过,直接进行图片选择
                                mPhotoPopupWindow.dismiss();
                                Intent intent = new Intent(Intent.ACTION_PICK);
                                intent.setType("image/*");
                                // 判断系统中是否有处理该 Intent 的 Activity
                                if (intent.resolveActivity(getPackageManager()) != null) {
                                    startActivityForResult(intent, REQUEST_IMAGE_GET);
                                } else {
                                    Toast.makeText(UserInfoActivity.this, "未找到图片查看器", Toast.LENGTH_SHORT).show();
                                }
                            }
                        }
                    }, new View.OnClickListener()
                    {
                        @Override
                        public void onClick (View v){
                            // 拍照及文件权限申请
                            if (ContextCompat.checkSelfPermission(UserInfoActivity.this,
                                    Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
                                    || ContextCompat.checkSelfPermission(UserInfoActivity.this,
                                    Manifest.permission.WRITE_EXTERNAL_STORAGE)
                                    != PackageManager.PERMISSION_GRANTED) {
                                // 权限还没有授予,进行申请
                                ActivityCompat.requestPermissions(UserInfoActivity.this,
                                        new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 300); // 申请的 requestCode 为 300
                            } else {
                                // 权限已经申请,直接拍照
                                mPhotoPopupWindow.dismiss();
                                imageCapture();
                            }
                        }
                    });
                    View rootView = LayoutInflater.from(UserInfoActivity.this).inflate(R.layout.activity_userinfo, null);
                    mPhotoPopupWindow.showAtLocation(rootView,
                            Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);
                    break;
    
                case R.id.user_nick_name:
                    ChangeInfoBean bean = new ChangeInfoBean();
                    bean.setTitle("修改昵称");
                    bean.setInfo(InfoPrefs.getData(Constants.UserInfo.NAME));
                    Intent intent = new Intent(UserInfoActivity.this,ChangeInfoActivity.class);
                    intent.putExtra("data", bean);
                    startActivityForResult(intent,REQUEST_CHANGE_USER_NICK_NAME);
                    break;
    
                case R.id.user_gender:
                    new ItemsAlertDialogUtil(UserInfoActivity.this).setItems(Constants.GENDER_ITEMS).
                            setListener(new ItemsAlertDialogUtil.OnSelectFinishedListener() {
                                @Override
                                public void SelectFinished(int which) {
                                    InfoPrefs.setData(Constants.UserInfo.GENDER,Constants.GENDER_ITEMS[which]);
                                    textView_user_gender.setText(InfoPrefs.getData(Constants.UserInfo.GENDER));
                                }
                            }).showDialog();
                    break;
                default:
            }
        }
    

            运行时权限的逻辑很简单,先判断是否已经授权过,如果已经授权,则直接进行操作,否则请求授权,根据请求结果处理。请求结果在onRequestPermissonsResult里处理。有一点要提一下,运行时权限申请是按照组来处理的,也就是说同属一个组的权限的请求是一样的,而用户只要授权一个,同组的权限也会同时被授权。SD卡的读写全是同属STORAGE组,所以我在申请SD卡读写权限的时候只申请读权限或者写权限就可以了。

    @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            switch (requestCode) {
                case 200:
                    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        mPhotoPopupWindow.dismiss();
                        Intent intent = new Intent(Intent.ACTION_PICK);
                        intent.setType("image/*");
                        // 判断系统中是否有处理该 Intent 的 Activity
                        if (intent.resolveActivity(getPackageManager()) != null) {
                            startActivityForResult(intent, REQUEST_IMAGE_GET);
                        } else {
                            Toast.makeText(UserInfoActivity.this, "未找到图片查看器", Toast.LENGTH_SHORT).show();
                        }
                    } else {
                        mPhotoPopupWindow.dismiss();
                    }
                    break;
                case 300:
                    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        mPhotoPopupWindow.dismiss();
                        imageCapture();
                    } else {
                        mPhotoPopupWindow.dismiss();
                    }
                    break;
            }
            //super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }

            到这里,运行时权限就已经处理好了。

    3.拍照和选图

    add 2018/5/11 11:30

            从这里开始就可能会NullPointerException,SecurityException等问题。

    3.1    拍照

    private void imageCapture() {
            Intent intent;
            Uri pictureUri;
            //getMyPetRootDirectory()得到的是Environment.getExternalStorageDirectory() + File.separator+"MyPet"
            //也就是我之前创建的存放头像的文件夹(目录)
            File pictureFile = new File(PictureUtil.getMyPetRootDirectory(), IMAGE_FILE_NAME);
            // 判断当前系统
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                //这一句非常重要
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                //""中的内容是随意的,但最好用package名.provider名的形式,清晰明了
                pictureUri = FileProvider.getUriForFile(this,
                        "com.example.mypet.fileprovider", pictureFile);
            } else {
                intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
                pictureUri = Uri.fromFile(pictureFile);
            }
            // 去拍照,拍照的结果存到pictureUri对应的路径中
            intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri);
            Log.e(TAG,"before take photo"+pictureUri.toString());
            startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
        }

            从android7.0(SDK>=24)开始,直接使用本地真实路径的Uri被认为是不安全的,会抛出一个FileUri什么什么Exception(记不清了),必须使用FileProvider封装过的Uri,感兴趣可以自己看下,7.0y以上得到的uri是content开头,以下以file开头。那么FileProvider怎么使用呢?

            首先,在res->xml文件文件夹下新建provider_paths.xml

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

            paths指定要用到的目录,external-path是SD卡根目录,所以我的第一条的路径是SD卡根目录下新建的MyPet文件夹,取名为MyPetRoot,这个虚拟目录名可以随意取,最终的Uri会是这样 content://com.example.mypet.fileprovider/MyPetRoot/......。第二条是将SD卡共享,这是用于相册选图。

            接下来,在AndroidManifest.xml中注册这个provider

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

            有几点要注意:authorities要与之前使用FileProvider时""中的内容相同(实际上的逻辑是使用FileProvider时要与声明的authorites相同)。exported必须要并且只能设为false,否则会报错,其实也很好理解,我们将sd卡共享了,如果再设置成可以被外界访问,那么权限就没有用了。grantUriPermissions要设置为true,这样才能让其他程序(系统相机等)临时使用这个provider。最后在<meta-data />指定使用的resource,也就是我们刚才写的xml。

            现在我们回到imageCapture,还有两句没有解释。addflags的作用是给Intent添加一个标记(我不知道应该怎么叫,看我后面的解释),这里我们添加的是Intent.FLAG_GRANT_READ_URI_PERMISSION,他的作用是临时授权Intent启动的Activity使用我们Rrovider封装的Uri。最后一行启动拍照Activity,请求码是REQUEST_IMAGE_CAPTURE,这个值自己设置,用于区分回调结果来自哪个请求。这样,我们就完成了拍照请求,会跳转到系统拍照界面,接下来就要处理拍照得到的照片了。

            重写onActicityResult方法,我直接把所有请求先全贴了,拍照后的回调是case REQUEST_IMAGE_CAPTURE下的内容,逻辑很简单,拍照后需要裁剪,裁剪需要用到我们拍照得到的图片的Uri。

    @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            // 回调成功
            if (resultCode == RESULT_OK) {
                switch (requestCode) {
    
                    // 切割
                    case REQUEST_SMALL_IMAGE_CUTTING:
                        Log.e(TAG,"before show");
                        File cropFile=new File(PictureUtil.getMyPetRootDirectory(),"crop.jpg");
                        Uri cropUri = Uri.fromFile(cropFile);
                        setPicToView(cropUri);
                        break;
    
                    // 相册选取
                    case REQUEST_IMAGE_GET:
                        Uri uri= PictureUtil.getImageUri(this,data);
                        startPhotoZoom(uri);
                        break;
    
                    // 拍照
                    case REQUEST_IMAGE_CAPTURE:
                        File pictureFile = new File(PictureUtil.getMyPetRootDirectory(), IMAGE_FILE_NAME);
                        Uri pictureUri;
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                            pictureUri = FileProvider.getUriForFile(this,
                                    "com.example.mypet.fileprovider", pictureFile);
                            Log.e(TAG,"picURI="+pictureUri.toString());
                        } else {
                            pictureUri = Uri.fromFile(pictureFile);
                        }
                        startPhotoZoom(pictureUri);
                        break;
                    // 获取changeinfo销毁 后 回传的数据
                    case REQUEST_CHANGE_USER_NICK_NAME:
                        String returnData = data.getStringExtra("data_return");
                        InfoPrefs.setData(Constants.UserInfo.NAME,returnData);
                        textView_user_nick_name.setText(InfoPrefs.getData(Constants.UserInfo.NAME));
                        break;
                    default:
                }
            }else{
                Log.e(TAG,"result = "+resultCode+",request = "+requestCode);
            }
        }
            到这里,拍照的功能已经实现了,如果不想裁剪,可以直接将  sd卡根目录/MyPet/user_head_icon.jpg这张图片显示,接下来讲一下相册选取的实现。

    3.2    相册选取

            其实相册选取的代码我在上面已经完全贴过了,权限申请的时候已经调用了相册,OnActivityResult中处理回调,这里我要说一下我踩的一个大坑

    Uri uri= PictureUtil.getImageUri(this,data);

            具体的各种问题我已经记不清了,没办法进行梳理,我只能笼统的说一下了(本来想说的很多,结果解决后把坑玩的差不多了,嘤嘤嘤,以后要一边踩坑一边写)。总体上是因为android4.4开始相册中返回的图片不再是图片的真是Uri了,而是封装过的。我测试的时候返回的Uri五花八门,有的是可以正常处理的,有的需要进行解析,其中以小米最乱(不得不说MIUI对开发者各种不友好,为了用户体验各种不遵守规则)。最终在各种博客(可能是我用法的原因,有不少博客的方法我用了还是会报错)的洗礼下终于解决了,我把他封装在了PictureUtil里面,过程是    乱七八糟的uri ->真实路径 ->统一的uri 。

            部分 50% 来自《第一行代码》,部分 25% 来自他人博客,部分 25% 是我自己写的

    public class PictureUtil {
        private static final String TAG = "PictureUtil";
        private static final String MyPetRootDirectory = Environment.getExternalStorageDirectory() + File.separator+"MyPet";
    
        public static String getMyPetRootDirectory(){
            return MyPetRootDirectory;
        }
    
        public static Uri getImageUri(Context context,Intent data){
            String imagePath = null;
            Uri uri = data.getData();
            if(Build.VERSION.SDK_INT >= 19){
                if(DocumentsContract.isDocumentUri(context,uri)){
                    String docId = DocumentsContract.getDocumentId(uri);
                    if("com.android.providers.media.documents".equals(uri.getAuthority())){
                        String id = docId.split(":")[1];
                        String selection = MediaStore.Images.Media._ID+"="+id;
                        imagePath = getImagePath(context,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(context,contentUri,null);
                    }
                }else if("content".equalsIgnoreCase(uri.getScheme())){
                    imagePath = getImagePath(context,uri,null);
                }else if("file".equalsIgnoreCase(uri.getScheme())){
                    imagePath = uri.getPath();
                }
            }else{
                uri= data.getData();
                imagePath = getImagePath(context,uri,null);
            }
            File file = new File(imagePath);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                uri = FileProvider.getUriForFile(context,
                      "com.example.mypet.fileprovider", file);
            } else {
                uri = Uri.fromFile(file);
            }
    
            return uri;
        }
    
        private static String getImagePath(Context context,Uri uri, String selection) {
            String path = null;
            Cursor cursor = context.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;
        }
    
        public static void mkdirMyPetRootDirectory(){
            boolean isSdCardExist = Environment.getExternalStorageState().equals(
                    Environment.MEDIA_MOUNTED);// 判断sdcard是否存在
            if (isSdCardExist) {
                File MyPetRoot = new File(getMyPetRootDirectory());
                if (!MyPetRoot.exists()) {
                    try {
                        MyPetRoot.mkdir();
                        Log.d(TAG, "mkdir success");
                    } catch (Exception e) {
                        Log.e(TAG, "exception->" + e.toString());
                    }
                }
            }
        }
    }

            到这里,有关图片获取的内容就都结束了

    4.图片裁剪

            前面传入的Uri处理好了,一般裁剪不会出什么问题,只有一个可能出现OOM(out of memory)问题,先上代码

    private void startPhotoZoom(Uri uri) {
            Log.d(TAG,"Uri = "+uri.toString());
            //保存裁剪后的图片
            File cropFile=new File(PictureUtil.getMyPetRootDirectory(),"crop.jpg");
            try{
                if(cropFile.exists()){
                    cropFile.delete();
                    Log.e(TAG,"delete");
                }
            }catch(Exception e){
                e.printStackTrace();
            }
            Uri cropUri;
            cropUri = Uri.fromFile(cropFile);
    
            Intent intent = new Intent("com.android.camera.action.CROP");
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                //添加这一句表示对目标应用临时授权该Uri所代表的文件
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            }
            intent.setDataAndType(uri, "image/*");
            intent.putExtra("crop", "true");
            intent.putExtra("aspectX", 1); // 裁剪框比例
            intent.putExtra("aspectY", 1);
            intent.putExtra("outputX", 300); // 输出图片大小
            intent.putExtra("outputY", 300);
            intent.putExtra("scale", true);
            intent.putExtra("return-data", false);
    
            Log.e(TAG,"cropUri = "+cropUri.toString());
    
            intent.putExtra(MediaStore.EXTRA_OUTPUT, cropUri);
            intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
            intent.putExtra("noFaceDetection", true); // no face detection
            startActivityForResult(intent, REQUEST_SMALL_IMAGE_CUTTING);
        }

    主要问题在这一句

    intent.putExtra("return-data", false);

    如果设置成true,那么裁剪得到的图片将会直接以Bitmap的形式缓存到内存中,Bitmap是相当大的,如果手机内存不足,或者手机分配的内存不足会导致OOM问题而闪退。当然,实际上现在的手机300X300一般不会OOM,但为了保险起见和可能需要更大图片的需求,最好将“return-data”置为false。

    5.保存并显示图片

            回调的代码在上面已经贴过了,这里可能会有一个疑问

    case REQUEST_SMALL_IMAGE_CUTTING:
                        Log.e(TAG,"before show");
                        File cropFile=new File(PictureUtil.getMyPetRootDirectory(),"crop.jpg");
                        Uri cropUri = Uri.fromFile(cropFile);
                        setPicToView(cropUri);
                        break;

            按理说应该分版本获取Uri,否则会抛出异常。实际上并非如此,我之前没有搞明白7.0的Uri保护到底是做什么的,后来看了文档才明白 : 

    一个应用提供自身文件给其它应用使用时,如果给出一个file://格式的URI的话,应用会抛出FileUriExposedException

            也就是说提供给外界时才需要fileprovider,也就是调用相机、相册、裁剪的时候才需要,自己使用是不需要的。谷歌做出这个改动的原因是 

    谷歌认为目标app可能不具有文件权限,会造成潜在的问题。所以让这一行为快速失败。

    好了,基本上所有的坑都踩完了,下面补上其他的代码

    public void setPicToView(Uri uri)  {
            if (uri != null) {
                Bitmap photo = null;
                try {
                    photo = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
                }catch (FileNotFoundException e){
                    e.printStackTrace();
                }
                // 创建 Icon 文件夹
                if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                    //String storage = Environment.getExternalStorageDirectory().getPath();
                    File dirFile = new File(PictureUtil.getMyPetRootDirectory(),  "Icon");
                    if (!dirFile.exists()) {
                        if (!dirFile.mkdirs()) {
                            Log.d(TAG, "in setPicToView->文件夹创建失败");
                        } else {
                            Log.d(TAG, "in setPicToView->文件夹创建成功");
                        }
                    }
                    File file = new File(dirFile, IMAGE_FILE_NAME);
                    InfoPrefs.setData(Constants.UserInfo.HEAD_IMAGE,file.getPath());
                    //Log.d("result",file.getPath());
                    // Log.d("result",file.getAbsolutePath());
                    // 保存图片
                    FileOutputStream outputStream = null;
                    try {
                        outputStream = new FileOutputStream(file);
                        photo.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
                        outputStream.flush();
                        outputStream.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                // 在视图中显示图片
                showHeadImage();
                //circleImageView_user_head.setImageBitmap(InfoPrefs.getData(Constants.UserInfo.GEAD_IMAGE));
            }
        }
    private void showHeadImage() {
            boolean isSdCardExist = Environment.getExternalStorageState().equals(
                    Environment.MEDIA_MOUNTED);// 判断sdcard是否存在
            if (isSdCardExist) {
    
                String path = InfoPrefs.getData(Constants.UserInfo.HEAD_IMAGE);// 获取图片路径
    
                File file = new File(path);
                if (file.exists()) {
                    Bitmap bm = BitmapFactory.decodeFile(path);
                    // 将图片显示到ImageView中
                    circleImageView_user_head.setImageBitmap(bm);
                }else{
                    Log.e(TAG,"no file");
                    circleImageView_user_head.setImageResource(R.drawable.huaji);
                }
            } else {
                Log.e(TAG,"no SD card");
                circleImageView_user_head.setImageResource(R.drawable.huaji);
            }
        }

    huaji是一张在没有头像情况下的默认头像

    6.总结

            最后做一个总结,如果按照以上代码执行的话,会在SD卡根目录下创建一下文件和文件夹

            dir:MyPet

            file:user_head_icon.jpg

            file:crop.jpg

                    dir:Icon

            file:user_head_icon.jpg


        MyPet/user_head_icon.jpg是拍照得到的原图,MyPet/crop.jpg是裁剪得到的图片,我们最后显示的是MyPet/Icon/user_head_icon.jpg,所以前两个如果不需要可以在最后删掉,删掉的代码我就不写了。


            最后的最后,UserInfoActivity和整个项目的完整代码可在我的GitHub获取:NeedKwok

           之后我也可能写一下如何实现一个闹钟




    展开全文
  • 更换头像或者上传图片功能已基本是每个 APP 所具备的基础功能了,但这对于开发者来说是一个很麻烦的事情,除机型之外,适配版本就至少要考虑这几种情况(6.0以下版本、6.0的动态权限、7.0的FileProvider、8.0的特殊...

    更换头像或者上传图片功能已基本是每个 APP 所具备的基础功能了,但这对于开发者来说是一个很麻烦的事情,除机型之外,适配版本就至少要考虑这几种情况(6.0以下版本、6.0的动态权限、7.0的FileProvider、8.0的特殊情况)。

    今天来个总结,方便自己也方便同行。


    功能说明

    本文的示例以下图为准:
    选择图片

    界面只有一个 ImageView,点击 ImageView 弹出 DialogFragment,分别是拍照和相册选择图片功能,其中都带有系统的裁剪功能,将裁剪后的图片显示在 ImageView 上。如果不需要裁剪功能,只需要将代码中的裁剪方法注释掉即可。

    6.0 以下版本

    1. 权限

    <uses-feature android:name="android.hardware.camera" />
        <!--相机权限-->
        <uses-permission android:name="android.permission.CAMERA" />
        <!--写入SD卡的权限:如果你希望保存相机拍照后的照片-->
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
        <!--读取SD卡的权限:打开相册选取图片所必须的权限-->
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    2. 拍照代码

    /**
         * 打开系统相机
         */
        private void openSysCamera() {
            Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
                    new File(Environment.getExternalStorageDirectory(), imgName)));
            startActivityForResult(cameraIntent, CAMERA_RESULT_CODE);
        }

    其中 imgName 是拍照图片的名字,一般以时间戳再加上自定义字符串命名;CAMERA_RESULT_CODE 是自定义的一个常量,作为拍照的请求码。

    onActivityResult 处理拍照后的图片

    case CAMERA_RESULT_CODE:
                    tempFile = new File(Environment.getExternalStorageDirectory(), imgName);
                    cropPic(Uri.fromFile(tempFile));
                    break;

    将拍照后的图片创建成一个 File 对象,用来裁剪,裁剪的功能代码是和相册选取图片通用的。

    裁剪代码

    /**
         * 裁剪图片
         *
         * @param data
         */
        private void cropPic(Uri data) {
            if (data == null) {
                return;
            }
            Intent cropIntent = new Intent("com.android.camera.action.CROP");
            cropIntent.setDataAndType(data, "image/*");
    
            // 开启裁剪:打开的Intent所显示的View可裁剪
            cropIntent.putExtra("crop", "true");
            // 裁剪宽高比
            cropIntent.putExtra("aspectX", 1);
            cropIntent.putExtra("aspectY", 1);
            // 裁剪输出大小
            cropIntent.putExtra("outputX", 320);
            cropIntent.putExtra("outputY", 320);
            cropIntent.putExtra("scale", true);
            /**
             * return-data
             * 这个属性决定我们在 onActivityResult 中接收到的是什么数据,
             * 如果设置为true 那么data将会返回一个bitmap
             * 如果设置为false,则会将图片保存到本地并将对应的uri返回,当然这个uri得有我们自己设定。
             * 系统裁剪完成后将会将裁剪完成的图片保存在我们所这设定这个uri地址上。我们只需要在裁剪完成后直接调用该uri来设置图片,就可以了。
             */
            cropIntent.putExtra("return-data", true);
            // 当 return-data 为 false 的时候需要设置这句
    //        cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
            // 图片输出格式
    //        cropIntent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
            // 头像识别 会启动系统的拍照时人脸识别
    //        cropIntent.putExtra("noFaceDetection", true);
            startActivityForResult(cropIntent, CROP_RESULT_CODE);
        }

    其中的 CROP_RESULT_CODE 也是定义的静态常量,同拍照时定义的常量相同的作用。

    如果裁剪时将 return-data 设置为 false,那么需要定义一个 Uri 来保存裁剪后的图片路径。

     // 裁剪属性 cropIntent.putExtra("return-data", false); 时,使用自定义接收图片的Uri
        private static final String IMAGE_FILE_LOCATION = "file:///" + Environment.getExternalStorageDirectory().getPath() + "/temp.jpg";
        private Uri imageUri = Uri.parse(IMAGE_FILE_LOCATION);

    图片裁剪完成回调

     case CROP_RESULT_CODE:
                    // 裁剪时,这样设置 cropIntent.putExtra("return-data", true); 处理方案如下
                    if (data != null) {
                        Bundle bundle = data.getExtras();
                        if (bundle != null) {
                            Bitmap bitmap = bundle.getParcelable("data");
                            imageView.setImageBitmap(bitmap);
                            // 把裁剪后的图片保存至本地 返回路径
                            String urlpath = FileUtilcll.saveFile(this, "crop.jpg", bitmap);
                            L.e("裁剪图片地址->" + urlpath);
                        }
                    }
    
                    // 裁剪时,这样设置 cropIntent.putExtra("return-data", false); 处理方案如下
    //                try {
    //                    ivHead.setImageBitmap(BitmapFactory.decodeStream(
    // getActivity().getContentResolver().openInputStream(imageUri)));
    //                } catch (FileNotFoundException e) {
    //                    e.printStackTrace();
    //                }
                    break;

    FileUtilcll 代码

    /**
     * 图片文件操作
     */
    public class FileUtilcll {
    
        /**
         * 将Bitmap 图片保存到本地路径,并返回路径
         *
         * @param fileName 文件名称
         * @param bitmap   图片
         * @param资源类型,参照 MultimediaContentType 枚举,根据此类型,保存时可自动归类
         */
        public static String saveFile(Context c, String fileName, Bitmap bitmap) {
            return saveFile(c, "", fileName, bitmap);
        }
    
        public static String saveFile(Context c, String filePath, String fileName, Bitmap bitmap) {
            byte[] bytes = bitmapToBytes(bitmap);
            return saveFile(c, filePath, fileName, bytes);
        }
    
        /**
         * Bitmap 转 字节数组
         *
         * @param bm
         * @return
         */
        public static byte[] bitmapToBytes(Bitmap bm) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            bm.compress(CompressFormat.JPEG, 100, baos);
            return baos.toByteArray();
        }
    
        public static String saveFile(Context c, String filePath, String fileName, byte[] bytes) {
            String fileFullName = "";
            FileOutputStream fos = null;
            String dateFolder = new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA)
                    .format(new Date());
            try {
                String suffix = "";
                if (filePath == null || filePath.trim().length() == 0) {
                    filePath = Environment.getExternalStorageDirectory() + "/cxs/" + dateFolder + "/";
                }
                File file = new File(filePath);
                if (!file.exists()) {
                    file.mkdirs();
                }
                File fullFile = new File(filePath, fileName + suffix);
                fileFullName = fullFile.getPath();
                fos = new FileOutputStream(new File(filePath, fileName + suffix));
                fos.write(bytes);
            } catch (Exception e) {
                fileFullName = "";
            } finally {
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        fileFullName = "";
                    }
                }
            }
            return fileFullName;
        }
    
    }

    到此,拍照功能就完成了,附带了裁剪。剩下的就是相册选取照片,这个也不难,固定代码,其他功能和拍照时相同的。

     /**
         * 打开系统相册
         */
        private void openSysAlbum() {
            Intent albumIntent = new Intent(Intent.ACTION_PICK);
            albumIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
            startActivityForResult(albumIntent, ALBUM_RESULT_CODE);
        }

    其中的 ALBUM_RESULT_CODE 同样是定义的常量。用来在 onActivityResult 中处理返回结果。

    case ALBUM_RESULT_CODE:
                    // 相册
                    cropPic(data.getData());
                    break;

    和拍照公用裁剪代码,这里直接调用即可,其他逻辑不变。


    6.0动态权限适配

    除了上述功能保持不变之外,需要在代码中动态申请文件读写权限和相机权限。这里我将申请的时机写在了刚进入页面,而不是刚打开 APP,也不是点击按钮的时候。

    逻辑

    • 如果用户点击了拒绝,但没有点击“不再询问”,这个时候再次进入界面继续弹框;
    • 如果用户点击了拒绝,且选择了“不再询问”,那么再次进入此界面将会弹框提示打开 APP 的详情界面,手动开启对应权限。

    权限申请代码

    /**
         * 初始化相机相关权限
         * 适配6.0+手机的运行时权限
         */
        private void initPermission() {
            String[] permissions = new String[]{Manifest.permission.CAMERA,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.READ_EXTERNAL_STORAGE};
            //检查权限
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                    != PackageManager.PERMISSION_GRANTED) {
                // 之前拒绝了权限,但没有点击 不再询问 这个时候让它继续请求权限
                if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                        Manifest.permission.CAMERA)) {
                    Toast.makeText(this, "用户曾拒绝打开相机权限", Toast.LENGTH_SHORT).show();
                    ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
                } else {
                    //注册相机权限
                    ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
                }
            }
        }

    这里我将需要的权限统一写在了一个数组里面。其中的 REQUEST_PERMISSIONS 和上面拍照定义的常量功能相同。

    权限申请回调

    @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                               @NonNull int[] grantResults) {
            switch (requestCode) {
                case REQUEST_PERMISSIONS:
                    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        //成功
                        Toast.makeText(this, "用户授权相机权限", Toast.LENGTH_SHORT).show();
                    } else {
                        // 勾选了不再询问
                        Toast.makeText(this, "用户拒绝相机权限", Toast.LENGTH_SHORT).show();
                        /**
                         * 跳转到 APP 详情的权限设置页
                         *
                         * 可根据自己的需求定制对话框,点击某个按钮在执行下面的代码
                         */
                        Intent intent = Util.getAppDetailSettingIntent(PhotoFromSysActivity.this);
                        startActivity(intent);
                    }
                    break;
            }
        }

    其中的 getAppDetailSettingIntent()方法代码如下:

    /**
         * 获取 APP 详情页面intent
         *
         * @return
         */
        public static Intent getAppDetailSettingIntent(Context context) {
            Intent localIntent = new Intent();
            localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            if (Build.VERSION.SDK_INT >= 9) {
                localIntent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
                localIntent.setData(Uri.fromParts("package", context.getPackageName(), null));
            } else if (Build.VERSION.SDK_INT <= 8) {
                localIntent.setAction(Intent.ACTION_VIEW);
                localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails");
                localIntent.putExtra("com.android.settings.ApplicationPkgName", context.getPackageName());
            }
            return localIntent;
        }

    到这,权限这个适配问题就解决了,接下来该是 Android 7.0 的适配了,这里我们只需要修改拍照的功能即可,相册是没有问题的。


    Android7.0 适配

    Android 7.0 就是 File 路径的变更,需要使用 FileProvider 来做,下面看拍照的代码。

    拍照代码修改

    /**
         * 打开系统相机
         */
        private void openSysCamera() {
            Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    //        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(
    //                new File(Environment.getExternalStorageDirectory(), imgName)));
    //        File file = new File(Environment.getExternalStorageDirectory(), imgName);
            try {
                file = createOriImageFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            if (file != null) {
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
                    imgUriOri = Uri.fromFile(file);
                } else {
                    imgUriOri = FileProvider.getUriForFile(this, getPackageName() + ".provider", file);
                }
                cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imgUriOri);
                startActivityForResult(cameraIntent, CAMERA_RESULT_CODE);
            }
        }

    File 对象的创建和 拍照图片的 Uri 对象创建方式更改。创建原图像保存的代码如下:

    /**
         * 创建原图像保存的文件
         *
         * @return
         * @throws IOException
         */
        private File createOriImageFile() throws IOException {
            String imgNameOri = "HomePic_" + new SimpleDateFormat(
                    "yyyyMMdd_HHmmss").format(new Date());
            File pictureDirOri = new File(getExternalFilesDir(
                    Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/OriPicture");
            if (!pictureDirOri.exists()) {
                pictureDirOri.mkdirs();
            }
            File image = File.createTempFile(
                    imgNameOri,         /* prefix */
                    ".jpg",             /* suffix */
                    pictureDirOri       /* directory */
            );
            imgPathOri = image.getAbsolutePath();
            return image;
        }

    拍照回调代码修改

     case CAMERA_RESULT_CODE:
    //                tempFile = new File(Environment.getExternalStorageDirectory(), imgName);
    //                cropPic(Uri.fromFile(tempFile));
    
                    // 适配 Android7.0+
                    cropPic(getImageContentUri(file));
                    break;

    getImageContentUri() 代码如下:

    /**
         * 7.0以上获取裁剪 Uri
         *
         * @param imageFile
         * @return
         */
        private Uri getImageContentUri(File imageFile) {
            String filePath = imageFile.getAbsolutePath();
            Cursor cursor = getContentResolver().query(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                    new String[]{MediaStore.Images.Media._ID},
                    MediaStore.Images.Media.DATA + "=? ",
                    new String[]{filePath}, null);
    
            if (cursor != null && cursor.moveToFirst()) {
                int id = cursor.getInt(cursor
                        .getColumnIndex(MediaStore.MediaColumns._ID));
                Uri baseUri = Uri.parse("content://media/external/images/media");
                return Uri.withAppendedPath(baseUri, "" + id);
            } else {
                if (imageFile.exists()) {
                    ContentValues values = new ContentValues();
                    values.put(MediaStore.Images.Media.DATA, filePath);
                    return getContentResolver().insert(
                            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
                } else {
                    return null;
                }
            }
        }

    代码适配解决完了,还有一个 FileProvider 问题需要做如下配置。

    1. 在 res 目录下创建一个名为 xml 的文件夹,并在其下创建一个名为 file_paths.xml 文件,其内容如下:
    <?xml version="1.0" encoding="utf-8"?>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
        <files-path
            name="images"
            path="Android/data/com.example.package.name/files/Pictures/OriPicture/" />
        <external-path
            name="images"
            path="Android/data/com.example.package.name/files/Pictures/OriPicture/" />
        <external-files-path
            name="images"
            path="files/Pictures/OriPicture" />
        <root-path
            name="images"
            path="" />
        <root-path
            name="images"
            path="" />
    </paths>
    2. 在 AndroidMainfest.xml 中的 application 节点下做如下配置:
    
    <!--FileProvider共享文件、缓存-->
            <provider
                android:name="android.support.v4.content.FileProvider"
                android:authorities="com.cxs.yukumenu.provider"
                android:exported="false"
                android:grantUriPermissions="true">
                <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/file_paths" />
            </provider>

    其中这里的 android:authorities 属性值要和代码中 FileProvider.getUriForFile()的第二个参数值保持一致。

    注意

    • 代码已全部给出,粘贴即可使用
    • 本次测试真机:华为5.1.1,一加8.0.0

    总结

    1. 机型适配是个细活,但能锻炼排查问题和解决问题能力。
    2. 本文的代码有很多是可以抽取的,如果是在项目中使用,那么建议提取出特定功能的代码。
    展开全文
  • Android开发之调用摄像头拍照

    万次阅读 多人点赞 2019-12-05 15:01:40
    现在很多应用中都会要求用户上传一张图片来作为头像,首先我在这接收使用相机拍照和在相册中选择图片。接下来先上效果图: 接下来看代码: 1,布局文件: <LinearLayout xmlns:android=...
  • android拍照或从相册选取图片

    千次阅读 2020-04-23 10:06:28
    从相册或拍照更换图片功能的实现:(取图无裁剪功能) 获取图片方式:(类似更换头像的效果) 手机拍照 选择图片; 相册选取图片; 本文只是简单实现该功能,页面展示有些简陋,运行效果图如下: 创建xml布局...
  • 7.0以上版本(包括7.0)实现拍照功能有两个需要注意的地方 拍照权限的动态获取 内容提供器的使用 动态权限获取 在6.0系统之前,需要指定的权限可以在AndroidManifest.xml文件中进行权限声明 例如: <user-...
  • 拍照前(预览): 拍照后(显示):  从上图可以看出拍照前跟拍照后清晰度完全不一致。当我把他保存为图片文件结果这个文件的大小只有10几KB,然后我使用其他机型的手机来测试结果出乎我的意料居然没问题。百度、谷歌...
  • import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.Calendar; import java.util.Locale;...import android.annotation.Su
  • android拍照开发android开发实现拍照功能主要有两种方法: 直接调用系统照相机API实现拍照,拍完后,图片会保存在相册中,返回保存照片的路径,从而获取图片。 自己写SurfaceView调用camera来实现拍照,该方法触发一...
  • Android 实现 拍照测距 的APP

    万次阅读 热门讨论 2016-05-09 16:54:19
    应朋友需求做了款拍照测距的APP,可以测出你到目标物体的距离。 源码发到了Github上,欢迎star、下载 github地址:点击打开链接 主要难点: 1.自定义相机 2.SurfaceView双缓冲问题 3.别的也没啥了  README.md...
  • android camera无预览拍照 后台拍照

    万次阅读 热门讨论 2013-10-03 20:28:21
    前言:相信有许多人和我一样,希望在不让用户知道的情况下,使用后台Server调用摄像头拍照, 在网上找了不少资料,大致都讲到不预览无法实现拍照,涉及到用户隐私,属于非法调用摄像头...怎么办!!! 曾经看到一篇...
  • 拍照时弹出新的界面,像二维码那样有个框,需要按下拍照按钮才拍照拍照获取的只有被框住的图像? 是否有demo? 或者麻烦给个大概的代码
  • Android调用系统相机拍照并保存到指定位置 @Click(R.id.btn_takePhoto) void onclick() { Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); File file = new File(imagePath, Utils.getCurrentDa
  • TakePhoto https://github.com/crazycodeboy/TakePhoto/blob/master/README.md
  • 对于Android 6.0之前,想要使用系统的相机进行拍照,那么只要在AndroidManifedt.xml中进行添加相应的权限,主要是两个: uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> uses-...
  • Android静默拍照(无感知拍照)

    万次阅读 2019-10-31 18:27:42
    在安卓系统下,实现拍照主要有两种方式:第一种就是直接调用系统的相机实现拍照,简单快捷。但是弊端就是不能自定义拍照界面。第二种就是根据Android系统提供的相关API进行自定义拍照,这样就是可以根据具体需求实现...
  • Android-UploadMultipartImageAndroid 仿照微信发说说,既能实现拍照,选图库,多图案上传 使用Retrofit技术。使用方法:详见博客http://blog.csdn.net/u010046908/article/details/50767904项目的运行效果:服务器...
  • Android 7.0调用相机拍照,返回后显示拍照照片

    万次阅读 多人点赞 2017-11-29 08:33:02
    Android 7.0调用相机拍照,返回后显示拍照照片,并显示到手机相册中
  • 在我的程序里面,有个Activity是锁定竖屏的,点击拍照按钮,将会调用系统的相机进行拍照,现在遇到的问题是:如果这个系统相机是默认横屏拍照的,那么在程序跳转会发生横竖屏切换,在拍照返回结果的时候,我的...
1 2 3 4 5 ... 20
收藏数 33,246
精华内容 13,298
关键字:

android 拍照