引入布局文件
新建布局文件
view_camera_and_title.xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginRight="4dp">
<ImageView
android:id="@+id/crime_photo"
android:layout_width="80dp"
android:layout_height="80dp"
android:scaleType="centerInside"
android:background="@android:color/darker_gray"
android:cropToPadding="true" />
<ImageButton
android:id="@+id/crime_camera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@android:drawable/ic_menu_camera" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_weight="1">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/crime_title_label"
style="?android:listSeparatorTextViewStyle" />
<EditText
android:id="@+id/crime_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="16dp"
android:hint="@string/crime_title_hint" />
</LinearLayout>
</LinearLayout>
引入布局文件
fragment_crime.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/view_camera_and_title" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/crime_details_label"
style="?android:listSeparatorTextViewStyle" />
<!-- ... -->
</LinearLayout>
外部存储
指定照片存放位置
添加文件名获取方法
Crime.java
public String getPhotoFilename() {
return "IMG_" + getId().toString() + ".jpg";
}
Crime.getPhotoFilename() 方法不知道图片文件该存储在哪个目录。CrimeLab 负责 CriminalIntent 应用的数据持久工作,那么在 CrimeLab 类里添加 getPhotoFile(Crime) 方法再合适不过。
添加定位文件保存目录方法
CrimeLab.java
public File getPhotoFile(Crime crime) {
//获得存储图像文件的外部存储路径
File externalFileDir = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
if (externalFileDir == null) return null;
return new File(externalFileDir, crime.getPhotoFilename());
}
上述新增方法不会创建任何文件。它的作用就是返回指向某个具体位置的 File 对象。我们在这个方法里加了个校验:确认外部存储是否可用。如果不可用,getExternalFilesDir(String)方法会返回 null 值。
使用相机intent
获取图片文件路径
CrimeFragment.java
private File mPhotoFile; //图像文件(带路径)
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
UUID crimeId = (UUID) getArguments().getSerializable(ARG_CRIME_ID);
mCrime = CrimeLab.get(getActivity()).getCrime(crimeId);
//初始化图像文件路径
mPhotoFile = CrimeLab.get(getActivity()).getPhotoFile(mCrime);
}
申请外部存储读写权限
既然 Context.getExternalFilesDir(String) 方法会返回应用专用的文件目录,那么可以直接读写这个目录应该是理所当然的事。因此,对Android 4.4 (API 19) 及其之后的新版系统来说,应用就不用申请使用这个目录的权限了,但使用其他外部存储仍然需要。
AndroidManifest.xml
...
<!-- 只有在API级别小于19的设备上,应用才需要申请外部存储读权限 -->
<uses-feature
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="18" />
...
触发拍照
CrimeFragment.java
private static final int REQUEST_PHOTO = 4;
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//...
mPhotoButton = view.findViewById(R.id.crime_camera);
final Intent captureImage = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//文件存在且intent能被合适的activity解析时才可以拍照
boolean canTakePhoto = mPhotoFile != null && captureImage.resolveActivity(packageManager) != null;
mPhotoButton.setEnabled(canTakePhoto);
if (canTakePhoto) {
Uri uri = Uri.fromFile(mPhotoFile);
captureImage.putExtra(MediaStore.EXTRA_OUTPUT, uri);
}
mPhotoButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivityForResult(captureImage, REQUEST_PHOTO);
}
});
mPhotoView = view.findViewById(R.id.crime_photo);
updatePhotoView();
return view;
}
缩放和显示位图
缩放位图
手工缩放位图照片,首先确认文件到底有多大,然后考虑按照给定区域大小合理缩放文件。最后,重新读取缩放后的文件,创建 Bitmap对象。
PictureUtils.java
public class PictureUtils {
public static Bitmap getScaledBitmap(String path, int destWidth, int destHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
float srcWidth = options.outWidth;
float srcHeight = options.outHeight;
int inSampleSize = 1;
if (srcHeight > destHeight || srcWidth > destWidth) {
if (srcWidth > srcHeight) inSampleSize = Math.round(srcHeight / destHeight);
else inSampleSize = Math.round(srcWidth / destWidth);
}
options = new BitmapFactory.Options();
options.inSampleSize = inSampleSize;
return BitmapFactory.decodeFile(path, options);
}
}
上述方法中,inSampleSize 值很关键。它决定着缩略图像素的大小。假设这个值是1的话,就表明缩略图和原始照片的水平像素大小一样。如果是2的话,它们的水平像素比就是1∶2。因此,inSampleSize 值为2时,缩略图的像素数就是原始文件的四分之一。
编写合理的缩放方法
fragment刚启动时,PhotoView究竟有多大无人知道。 onCreate(…) 、 onStart() 和 onResume() 方法启动后,才会有首个实例化布局出现。也就在此时,显示在屏幕上的视图才会有大小尺寸。
解决方案有两个:要么等布局实例化完成并显示,要么干脆使用保守估算值。特定条件下,
尽管估算比较主观,但确实是唯一切实可行的办法。首先确认屏幕的尺寸,然后按此缩放图像。这样,就能保证载入的ImageView永远不会过大。
PictureUtils.java
//确认屏幕尺寸,然后根据此比例缩放图像
public static Bitmap getScaledBitmap(String path, Activity activity) {
Point size = new Point();
activity.getWindowManager().getDefaultDisplay().getSize(size);
return getScaledBitmap(path, size.x, size.y);
}
在ImageView显示图像
CrimeFragment.java
//若图像文件成功创建,就将图片显示在控件上
private void updatePhotoView() {
if (mPhotoFile == null || !mPhotoFile.exists()) mPhotoView.setImageDrawable(null);
else {
Bitmap bitmap = PictureUtils.getScaledBitmap(mPhotoFile.getPath(), getActivity());
mPhotoView.setImageBitmap(bitmap);
}
}
在初始化和返回结果时更新图像
CrimeFragment.onCreateView(LayoutInflater, ViewGroup, Bundle)
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_crime, container, false);
//...
mPhotoView = view.findViewById(R.id.crime_photo);
updatePhotoView();
return view;
}
CrimeFragment.onActivityResult(int, int, Intent)
if (requestCode == REQUEST_PHOTO) {
updatePhotoView();
}
实现点击缩略图显示照片
新建显示照片的布局
dialog_photo.xml
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dialog_photo_image_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</ImageView>
新建PhotoViewFragment类
PhotoViewFragment.java
public class PhotoViewFragment extends DialogFragment {
private static final String ARG_FILE = "file";
private ImageView mPhotoView;
public static PhotoViewFragment newInstance(File file) {
Bundle args = new Bundle();
args.putSerializable(ARG_FILE, file);
PhotoViewFragment photoViewFragment = new PhotoViewFragment();
photoViewFragment.setArguments(args);
return photoViewFragment;
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
File photo = (File) getArguments().getSerializable(ARG_FILE);
View view = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_photo, null);
mPhotoView = view.findViewById(R.id.dialog_photo_image_view);
//若图像文件成功创建,就将图片显示在控件上
if (photo == null || !photo.exists()) mPhotoView.setImageDrawable(null);
else {
Bitmap bitmap = PictureUtils.getScaledBitmap(photo.getPath(), getActivity());
mPhotoView.setImageBitmap(bitmap);
}
return new AlertDialog.Builder(getActivity())
.setView(view)
.create();
}
}
设置单击事件监听器
CrimeFragment.onCreateView(LayoutInflater, ViewGroup, Bundle)
//...
mPhotoView = view.findViewById(R.id.crime_photo);
updatePhotoView();
mPhotoView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "点击了图片...");
Log.d(TAG, "图片路径:" + mPhotoFile.getPath());
FragmentManager manager = getFragmentManager();
PhotoViewFragment dialog = PhotoViewFragment.newInstance(mPhotoFile);
dialog.show(manager, DIALOG_PHOTO);
}
});