Android开发中用相机拍照后图片裁剪的实例及源码
其实Android提供Intent让我们打开系统的相机,但是系统相机跟自己app风格不搭,而且用起来体验不好。所以我使用了SDK提供的camera API自定义了一个相机,并且在相机界面上面添加了参考线,有助于用户将题目拍正,提高ocr的识别率。
1、绘制参考线的代码
public class ReferenceLine extends View {
private Paint mLinePaint;
public ReferenceLine(Context context) {
super(context);
init();
}
public ReferenceLine(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ReferenceLine(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mLinePaint = new Paint();
mLinePaint.setAntiAlias(true);
mLinePaint.setColor(Color.parseColor("#45e0e0e0"));
mLinePaint.setStrokeWidth(1);
}
@Override
protected void onDraw(Canvas canvas) {
int screenWidth = Utils.getScreenWH(getContext()).widthPixels;
int screenHeight = Utils.getScreenWH(getContext()).heightPixels;
int width = screenWidth/3;
int height = screenHeight/3;
for (int i = width, j = 0;i < screenWidth && j<2;i += width, j++) {
canvas.drawLine(i, 0, i, screenHeight, mLinePaint);
}
for (int j = height,i = 0;j < screenHeight && i < 2;j += height,i++) {
canvas.drawLine(0, j, screenWidth, j, mLinePaint);
}
}
}
2、自定义相机代码
这里主要是要创建一个SurfaceView,将摄像头的预览界面放到SurfaceView中显示。
package com.bbk.lling.camerademo.camare;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.PictureCallback;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.Toast;
import com.bbk.lling.camerademo.utils.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @Class: CameraPreview
* @Description: 自定义相机
* @author: lling(www.cnblogs.com/liuling)
* @Date: 2015/10/25
*/
public class CameraPreview extends SurfaceView implements
SurfaceHolder.Callback, AutoFocusCallback {
private static final String TAG = "CameraPreview";
private int viewWidth = 0;
private int viewHeight = 0;
/** 监听接口 */
private OnCameraStatusListener listener;
private SurfaceHolder holder;
private Camera camera;
private FocusView mFocusView;
//创建一个PictureCallback对象,并实现其中的onPictureTaken方法
private PictureCallback pictureCallback = new PictureCallback() {
// 该方法用于处理拍摄后的照片数据
@Override
public void onPictureTaken(byte[] data, Camera camera) {
// 停止照片拍摄
try {
camera.stopPreview();
} catch (Exception e) {
}
// 调用结束事件
if (null != listener) {
listener.onCameraStopped(data);
}
}
};
// Preview类的构造方法
public CameraPreview(Context context, AttributeSet attrs) {
super(context, attrs);
// 获得SurfaceHolder对象
holder = getHolder();
// 指定用于捕捉拍照事件的SurfaceHolder.Callback对象
holder.addCallback(this);
// 设置SurfaceHolder对象的类型
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
setOnTouchListener(onTouchListener);
}
// 在surface创建时激发
public void surfaceCreated(SurfaceHolder holder) {
Log.e(TAG, "==surfaceCreated==");
if(!Utils.checkCameraHardware(getContext())) {
Toast.makeText(getContext(), "摄像头打开失败!", Toast.LENGTH_SHORT).show();
return;
}
// 获得Camera对象
camera = getCameraInstance();
try {
// 设置用于显示拍照摄像的SurfaceHolder对象
camera.setPreviewDisplay(holder);
} catch (IOException e) {
e.printStackTrace();
// 释放手机摄像头
camera.release();
camera = null;
}
updateCameraParameters();
if (camera != null) {
camera.startPreview();
}
setFocus();
}
// 在surface销毁时激发
public void surfaceDestroyed(SurfaceHolder holder) {
Log.e(TAG, "==surfaceDestroyed==");
// 释放手机摄像头
camera.release();
camera = null;
}
// 在surface的大小发生改变时激发
public void surfaceChanged(final SurfaceHolder holder, int format, int w,
int h) {
// stop preview before making changes
try {
camera.stopPreview();
} catch (Exception e){
// ignore: tried to stop a non-existent preview
}
// set preview size and make any resize, rotate or
// reformatting changes here
updateCameraParameters();
// start preview with new settings
try {
camera.setPreviewDisplay(holder);
camera.startPreview();
} catch (Exception e){
Log.d(TAG, "Error starting camera preview: " + e.getMessage());
}
setFocus();
}
/**
* 点击显示焦点区域
*/
OnTouchListener onTouchListener = new OnTouchListener() {
@SuppressWarnings("deprecation")
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
int width = mFocusView.getWidth();
int height = mFocusView.getHeight();
mFocusView.setX(event.getX() - (width / 2));
mFocusView.setY(event.getY() - (height / 2));
mFocusView.beginFocus();
} else if (event.getAction() == MotionEvent.ACTION_UP) {
focusOnTouch(event);
}
return true;
}
};
/**
* 获取摄像头实例
* @return
*/
private Camera getCameraInstance() {
Camera c = null;
try {
int cameraCount = 0;
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
cameraCount = Camera.getNumberOfCameras(); // get cameras number
for (int camIdx = 0; camIdx < cameraCount; camIdx++) {
Camera.getCameraInfo(camIdx, cameraInfo); // get camerainfo
// 代表摄像头的方位,目前有定义值两个分别为CAMERA_FACING_FRONT前置和CAMERA_FACING_BACK后置
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
try {
c = Camera.open(camIdx); //打开后置摄像头
} catch (RuntimeException e) {
Toast.makeText(getContext(), "摄像头打开失败!", Toast.LENGTH_SHORT).show();
}
}
}
if (c == null) {
c = Camera.open(0); // attempt to get a Camera instance
}
} catch (Exception e) {
Toast.makeText(getContext(), "摄像头打开失败!", Toast.LENGTH_SHORT).show();
}
return c;
}
private void updateCameraParameters() {
if (camera != null) {
Camera.Parameters p = camera.getParameters();
setParameters(p);
try {
camera.setParameters(p);
} catch (Exception e) {
Camera.Size previewSize = findBestPreviewSize(p);
p.setPreviewSize(previewSize.width, previewSize.height);
p.setPictureSize(previewSize.width, previewSize.height);
camera.setParameters(p);
}
}
}
/**
* @param p
*/
private void setParameters(Camera.Parameters p) {
List<String> focusModes = p.getSupportedFocusModes();
if (focusModes
.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
p.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
}
long time = new Date().getTime();
p.setGpsTimestamp(time);
// 设置照片格式
p.setPictureFormat(PixelFormat.JPEG);
Camera.Size previewSize = findPreviewSizeByScreen(p);
p.setPreviewSize(previewSize.width, previewSize.height);
p.setPictureSize(previewSize.width, previewSize.height);
p.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
if (getContext().getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) {
camera.setDisplayOrientation(90);
p.setRotation(90);
}
}
// 进行拍照,并将拍摄的照片传入PictureCallback接口的onPictureTaken方法
public void takePicture() {
if (camera != null) {
try {
camera.takePicture(null, null, pictureCallback);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 设置监听事件
public void setOnCameraStatusListener(OnCameraStatusListener listener) {
this.listener = listener;
}
@Override
public void onAutoFocus(boolean success, Camera camera) {
}
public void start() {
if (camera != null) {
camera.startPreview();
}
}
public void stop() {
if (camera != null) {
camera.stopPreview();
}
}
/**
* 相机拍照监听接口
*/
public interface OnCameraStatusListener {
// 相机拍照结束事件
void onCameraStopped(byte[] data);
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
viewWidth = MeasureSpec.getSize(widthSpec);
viewHeight = MeasureSpec.getSize(heightSpec);
super.onMeasure(
MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY));
}
/**
* 将预览大小设置为屏幕大小
* @param parameters
* @return
*/
private Camera.Size findPreviewSizeByScreen(Camera.Parameters parameters) {
if (viewWidth != 0 && viewHeight != 0) {
return camera.new Size(Math.max(viewWidth, viewHeight),
Math.min(viewWidth, viewHeight));
} else {
return camera.new Size(Utils.getScreenWH(getContext()).heightPixels,
Utils.getScreenWH(getContext()).widthPixels);
}
}
/**
* 找到最合适的显示分辨率 (防止预览图像变形)
* @param parameters
* @return
*/
private Camera.Size findBestPreviewSize(Camera.Parameters parameters) {
// 系统支持的所有预览分辨率
String previewSizeValueString = null;
previewSizeValueString = parameters.get("preview-size-values");
if (previewSizeValueString == null) {
previewSizeValueString = parameters.get("preview-size-value");
}
if (previewSizeValueString == null) { // 有些手机例如m9获取不到支持的预览大小 就直接返回屏幕大小
return camera.new Size(Utils.getScreenWH(getContext()).widthPixels,
Utils.getScreenWH(getContext()).heightPixels);
}
float bestX = 0;
float bestY = 0;
float tmpRadio = 0;
float viewRadio = 0;
if (viewWidth != 0 && viewHeight != 0) {
viewRadio = Math.min((float) viewWidth, (float) viewHeight)
/ Math.max((float) viewWidth, (float) viewHeight);
}
String[] COMMA_PATTERN = previewSizeValueString.split(",");
for (String prewsizeString : COMMA_PATTERN) {
prewsizeString = prewsizeString.trim();
int dimPosition = prewsizeString.indexOf('x');
if (dimPosition == -1) {
continue;
}
float newX = 0;
float newY = 0;
try {
newX = Float.parseFloat(prewsizeString.substring(0, dimPosition));
newY = Float.parseFloat(prewsizeString.substring(dimPosition + 1));
} catch (NumberFormatException e) {
continue;
}
float radio = Math.min(newX, newY) / Math.max(newX, newY);
if (tmpRadio == 0) {
tmpRadio = radio;
bestX = newX;
bestY = newY;
} else if (tmpRadio != 0 && (Math.abs(radio - viewRadio)) < (Math.abs(tmpRadio - viewRadio))) {
tmpRadio = radio;
bestX = newX;
bestY = newY;
}
}
if (bestX > 0 && bestY > 0) {
return camera.new Size((int) bestX, (int) bestY);
}
return null;
}
/**
* 设置焦点和测光区域
*
* @param event
*/
public void focusOnTouch(MotionEvent event) {
int[] location = new int[2];
RelativeLayout relativeLayout = (RelativeLayout)getParent();
relativeLayout.getLocationOnScreen(location);
Rect focusRect = Utils.calculateTapArea(mFocusView.getWidth(),
mFocusView.getHeight(), 1f, event.getRawX(), event.getRawY(),
location[0], location[0] + relativeLayout.getWidth(), location[1],
location[1] + relativeLayout.getHeight());
Rect meteringRect = Utils.calculateTapArea(mFocusView.getWidth(),
mFocusView.getHeight(), 1.5f, event.getRawX(), event.getRawY(),
location[0], location[0] + relativeLayout.getWidth(), location[1],
location[1] + relativeLayout.getHeight());
Camera.Parameters parameters = camera.getParameters();
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
if (parameters.getMaxNumFocusAreas() > 0) {
List<Camera.Area> focusAreas = new ArrayList<Camera.Area>();
focusAreas.add(new Camera.Area(focusRect, 1000));
parameters.setFocusAreas(focusAreas);
}
if (parameters.getMaxNumMeteringAreas() > 0) {
List<Camera.Area> meteringAreas = new ArrayList<Camera.Area>();
meteringAreas.add(new Camera.Area(meteringRect, 1000));
parameters.setMeteringAreas(meteringAreas);
}
try {
camera.setParameters(parameters);
} catch (Exception e) {
}
camera.autoFocus(this);
}
/**
* 设置聚焦的图片
* @param focusView
*/
public void setFocusView(FocusView focusView) {
this.mFocusView = focusView;
}
/**
* 设置自动聚焦,并且聚焦的圈圈显示在屏幕中间位置
*/
public void setFocus() {
if(!mFocusView.isFocusing()) {
try {
camera.autoFocus(this);
mFocusView.setX((Utils.getWidthInPx(getContext())-mFocusView.getWidth()) / 2);
mFocusView.setY((Utils.getHeightInPx(getContext())-mFocusView.getHeight()) / 2);
mFocusView.beginFocus();
} catch (Exception e) {
}
}
}
}
3、Activity中使用自定义相机
public class TakePhoteActivity extends Activity implements CameraPreview.OnCameraStatusListener,
SensorEventListener {
private static final String TAG = "TakePhoteActivity";
public static final Uri IMAGE_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
public static final String PATH = Environment.getExternalStorageDirectory()
.toString() + "/AndroidMedia/";
CameraPreview mCameraPreview;
CropImageView mCropImageView;
RelativeLayout mTakePhotoLayout;
LinearLayout mCropperLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置横屏
// setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
// 设置全屏
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_take_phote);
// Initialize components of the app
mCropImageView = (CropImageView) findViewById(R.id.CropImageView);
mCameraPreview = (CameraPreview) findViewById(R.id.cameraPreview);
FocusView focusView = (FocusView) findViewById(R.id.view_focus);
mTakePhotoLayout = (RelativeLayout) findViewById(R.id.take_photo_layout);
mCropperLayout = (LinearLayout) findViewById(R.id.cropper_layout);
mCameraPreview.setFocusView(focusView);
mCameraPreview.setOnCameraStatusListener(this);
mCropImageView.setGuidelines(2);
mSensorManager = (SensorManager) getSystemService(Context.
SENSOR_SERVICE);
mAccel = mSensorManager.getDefaultSensor(Sensor.
TYPE_ACCELEROMETER);
}
boolean isRotated = false;
@Override
protected void onResume() {
super.onResume();
if(!isRotated) {
TextView hint_tv = (TextView) findViewById(R.id.hint);
ObjectAnimator animator = ObjectAnimator.ofFloat(hint_tv, "rotation", 0f, 90f);
animator.setStartDelay(800);
animator.setDuration(1000);
animator.setInterpolator(new LinearInterpolator());
animator.start();
View view = findViewById(R.id.crop_hint);
AnimatorSet animSet = new AnimatorSet();
ObjectAnimator animator1 = ObjectAnimator.ofFloat(view, "rotation", 0f, 90f);
ObjectAnimator moveIn = ObjectAnimator.ofFloat(view, "translationX", 0f, -50f);
animSet.play(animator1).before(moveIn);
animSet.setDuration(10);
animSet.start();
isRotated = true;
}
mSensorManager.registerListener(this, mAccel, SensorManager.SENSOR_DELAY_UI);
}
@Override
protected void onPause() {
super.onPause();
mSensorManager.unregisterListener(this);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
Log.e(TAG, "onConfigurationChanged");
super.onConfigurationChanged(newConfig);
}
public void takePhoto(View view) {
if(mCameraPreview != null) {
mCameraPreview.takePicture();
}
}
public void close(View view) {
finish();
}
/**
* 关闭截图界面
* @param view
*/
public void closeCropper(View view) {
showTakePhotoLayout();
}
/**
* 开始截图,并保存图片
* @param view
*/
public void startCropper(View view) {
//获取截图并旋转90度
CropperImage cropperImage = mCropImageView.getCroppedImage();
Log.e(TAG, cropperImage.getX() + "," + cropperImage.getY());
Log.e(TAG, cropperImage.getWidth() + "," + cropperImage.getHeight());
Bitmap bitmap = Utils.rotate(cropperImage.getBitmap(), -90);
// Bitmap bitmap = mCropImageView.getCroppedImage();
// 系统时间
long dateTaken = System.currentTimeMillis();
// 图像名称
String filename = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken)
.toString() + ".jpg";
Uri uri = insertImage(getContentResolver(), filename, dateTaken, PATH,
filename, bitmap, null);
cropperImage.getBitmap().recycle();
cropperImage.setBitmap(null);
Intent intent = new Intent(this, ShowCropperedActivity.class);
intent.setData(uri);
intent.putExtra("path", PATH + filename);
intent.putExtra("width", bitmap.getWidth());
intent.putExtra("height", bitmap.getHeight());
intent.putExtra("cropperImage", cropperImage);
startActivity(intent);
bitmap.recycle();
finish();
super.overridePendingTransition(R.anim.fade_in,
R.anim.fade_out);
// doAnimation(cropperImage);
}
private void doAnimation(CropperImage cropperImage) {
ImageView imageView = new ImageView(this);
View view = LayoutInflater.from(this).inflate(
R.layout.image_view_layout, null);
((RelativeLayout) view.findViewById(R.id.root_layout)).addView(imageView);
RelativeLayout relativeLayout = ((RelativeLayout) findViewById(R.id.root_layout));
// relativeLayout.addView(imageView);
imageView.setX(cropperImage.getX());
imageView.setY(cropperImage.getY());
ViewGroup.LayoutParams lp = imageView.getLayoutParams();
lp.width = (int)cropperImage.getWidth();
lp.height = (int) cropperImage.getHeight();
imageView.setLayoutParams(lp);
imageView.setImageBitmap(cropperImage.getBitmap());
try {
getWindow().addContentView(view, lp);
} catch (Exception e) {
e.printStackTrace();
}
/*AnimatorSet animSet = new AnimatorSet();
ObjectAnimator translationX = ObjectAnimator.ofFloat(this, "translationX", cropperImage.getX(), 0);
ObjectAnimator translationY = ObjectAnimator.ofFloat(this, "translationY", cropperImage.getY(), 0);*/
TranslateAnimation translateAnimation = new TranslateAnimation(
0, -cropperImage.getX(), 0, -(Math.abs(cropperImage.getHeight() - cropperImage.getY())));// 当前位置移动到指定位置
RotateAnimation rotateAnimation = new RotateAnimation(0, -90,
Animation.ABSOLUTE, cropperImage.getX() ,Animation.ABSOLUTE, cropperImage.getY());
AnimationSet animationSet = new AnimationSet(true);
animationSet.addAnimation(translateAnimation);
animationSet.addAnimation(rotateAnimation);
animationSet.setFillAfter(true);
animationSet.setDuration(2000L);
imageView.startAnimation(animationSet);
// finish();
}
/**
* 拍照成功后回调
* 存储图片并显示截图界面
* @param data
*/
@Override
public void onCameraStopped(byte[] data) {
Log.i("TAG", "==onCameraStopped==");
// 创建图像
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
// 系统时间
long dateTaken = System.currentTimeMillis();
// 图像名称
String filename = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken)
.toString() + ".jpg";
// 存储图像(PATH目录)
Uri source = insertImage(getContentResolver(), filename, dateTaken, PATH,
filename, bitmap, data);
//准备截图
try {
mCropImageView.setImageBitmap(MediaStore.Images.Media.getBitmap(this.getContentResolver(), source));
// mCropImageView.rotateImage(90);
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
showCropperLayout();
}
/**
* 存储图像并将信息添加入媒体数据库
*/
private Uri insertImage(ContentResolver cr, String name, long dateTaken,
String directory, String filename, Bitmap source, byte[] jpegData) {
OutputStream outputStream = null;
String filePath = directory + filename;
try {
File dir = new File(directory);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(directory, filename);
if (file.createNewFile()) {
outputStream = new FileOutputStream(file);
if (source != null) {
source.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
} else {
outputStream.write(jpegData);
}
}
} catch (FileNotFoundException e) {
Log.e(TAG, e.getMessage());
return null;
} catch (IOException e) {
Log.e(TAG, e.getMessage());
return null;
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (Throwable t) {
}
}
}
ContentValues values = new ContentValues(7);
values.put(MediaStore.Images.Media.TITLE, name);
values.put(MediaStore.Images.Media.DISPLAY_NAME, filename);
values.put(MediaStore.Images.Media.DATE_TAKEN, dateTaken);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.DATA, filePath);
return cr.insert(IMAGE_URI, values);
}
private void showTakePhotoLayout() {
mTakePhotoLayout.setVisibility(View.VISIBLE);
mCropperLayout.setVisibility(View.GONE);
}
private void showCropperLayout() {
mTakePhotoLayout.setVisibility(View.GONE);
mCropperLayout.setVisibility(View.VISIBLE);
mCameraPreview.start(); //继续启动摄像头
}
private float mLastX = 0;
private float mLastY = 0;
private float mLastZ = 0;
private boolean mInitialized = false;
private SensorManager mSensorManager;
private Sensor mAccel;
@Override
public void onSensorChanged(SensorEvent event) {
float x = event.values[0];
float y = event.values[1];
float z = event.values[2];
if (!mInitialized){
mLastX = x;
mLastY = y;
mLastZ = z;
mInitialized = true;
}
float deltaX = Math.abs(mLastX - x);
float deltaY = Math.abs(mLastY - y);
float deltaZ = Math.abs(mLastZ - z);
if(deltaX > 0.8 || deltaY > 0.8 || deltaZ > 0.8){
mCameraPreview.setFocus();
}
mLastX = x;
mLastY = y;
mLastZ = z;
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
}
actiity中注册了SensorEventListener,也就是使用传感器监听用户手机的移动,如果有一定距离的移动,则自动聚焦,这样体验好一点。
我对比了一下小猿搜题和学霸君两款app的拍照功能,个人感觉小猿搜题的体验要好一些,因为从主界面进入拍照界面,连个界面没有一个旋转的过渡,而学霸君就有一个过渡,有一丝丝的影响体验。也就是说学霸君的拍照界面是横屏的,在activity的onCreate方法里面调用了setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)来设置全屏,而切换界面的时候又从竖屏切换为横屏,就会有个过渡的效果,影响了体验。
个人猜测小猿搜题是将拍照界面的activity设置为竖屏,而将摄像头直接旋转90度,这样就强制用户横屏拍摄,当然,拍完之后还要将图片旋转回来。所以我参考小猿搜题来实现的,毕竟体验为王嘛。
如上图(其实是竖屏),红色圈起来的其实是放到底部,然后将屏幕中间的文字旋转90度(带有动画,起了提示用户横屏拍照的作用),就给人的感觉是横屏的。了。
还有一点就是小猿搜题拍完照到截图过渡的很自然,感觉很流畅,估计是拍照和截图放在同一个activity中的,如果是两个activty,涉及到界面切换,肯定不会那么自然。所以我也将拍照和截图放在一个界面,拍照完就将自定义相机隐藏,将截图界面显示出来,这样切换就很流畅了。
项目中截图的功能我是从github上面找的一个开源库cropper:https://github.com/edmodo/cropper
因为ocr图片识别的代码是公司的,所以识别的功能没有添加到demo里面去。
SharedPreferences简介
SharedPreferences是Android平台上一个轻量级的存储类,用来保存应用的一些常用配置,比如Activity状态,Activity暂停时,将此activity的状态保存到SharedPereferences中;当Activity重载,系统回调方法onSaveInstanceState时,再从SharedPreferences中将值取出。
SharedPreferences提供了java常规的Long、Int、String等类型数据的保存接口。
SharedPreferences类似过去Windows系统上的ini配置文件,但是它分为多种权限,可以全局共享访问。
提示最终是以xml方式来保存,整体效率来看不是特别的高,对于常规的轻量级而言比SQLite要好不少,如果真的存储量不大可以考虑自己定义文件格式。xml处理时Dalvik会通过自带底层的本地XML Parser解析,比如XMLpull方式,这样对于内存资源占用比较好。
操作模式
SharedPreferences数据的四种操作模式
Context.MODE_PRIVATE
Context.MODE_APPEND
Context.MODE_WORLD_READABLE
Context.MODE_WORLD_WRITEABLE
Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容
Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件.
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件.
MODE_WORLD_READABLE:表示当前文件可以被其他应用读取.
MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入
SharedPreferences是Android中最容易理解的数据存储技术,实际上SharedPreferences处理的就是一个key-value(键值对)SharedPreferences常用来存储一些轻量级的数据。这类似于C++中Map的数据存储方式(实际上在最后生成的.xml文件内,就是以Map格式存储的)。
获取SharedPreferences的两种方式:
1、调用Context对象的getSharedPreferences()方法
2、调用Activity对象的getPreferences()方法
两种方式的区别:
调用Context对象的getSharedPreferences()方法获得的SharedPreferences对象可以被同一应用程序下的其他组件共享。
调用Activity对象的getPreferences()方法获得的SharedPreferences对象只能在该Activity中使用。
其中,getSharedPreferences()的方法原型为:
getSharedPreferences(String name, int mode);
// name: 生成xml文件的名称
// MODE_PRIVATE: 为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容
// MODE_APPEND: 模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件.
// MODE_WORLD_READABLE: 表示当前文件可以被其他应用读取,不推荐使用
// MODE_WORLD_WRITEABLE: 表示当前文件可以被其他应用写入,不推荐使用
使用SharedPreferences存储数据的方法如下:
//实例化SharedPreferences对象(第一步)
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
//实例化SharedPreferences.Editor对象(第二步)
SharedPreferences.Editor editor = mySharedPreferences.edit();
//用putString的方法保存数据
editor.putString("UserName", etName.getText().toString());
editor.putString("Password", etPassword.getText().toString());
//提交当前数据
//editor.apply();
editor.commit();
//使用toast信息提示框提示成功写入数据
Toast.makeText(MainActivity.this, "注册成功", Toast.LENGTH_LONG).show();
使用SharedPreferences读取数据的方法如下:
SharedPreferences sp = getSharedPreferences(strLogInfo, MODE_APPEND);
String name = sp.getString("UserName", "");
String passwd = sp.getString("Password","");
if(etName.getText().toString().equals(name) && etPassword.getText().toString().equals(passwd)){
Toast.makeText(MainActivity.this, "登陆成功", Toast.LENGTH_LONG).show();
} else{
Toast.makeText(MainActivity.this, "登录失败", Toast.LENGTH_LONG).show();
}
在使用SharedPreferences之后,程序会在“/data/data/包名/shared_prefs/xxx.xml”生成的一个XML文件。文件名取决于getSharedPreferences的第一个参数名。
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="UserName">Name</string>
<string name="Password">Password</string>
</map>
完整代码如下:
import android.content.SharedPreferences;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private EditText etName;
private EditText etPassword;
private Button btnLogin;
private Button btnLogup;
private String strLogInfo = "test";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etName = (EditText)findViewById(R.id.etName);
etPassword = (EditText)findViewById(R.id.etPassword);
btnLogin = (Button)findViewById(R.id.btnLogin);
btnLogup = (Button)findViewById(R.id.btnLogup);
btnLogup.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SharedPreferences sp = getSharedPreferences(strLogInfo, MODE_APPEND);
SharedPreferences.Editor editor = sp.edit();
editor.putString("UserName", etName.getText().toString());
editor.putString("Password", etPassword.getText().toString());
editor.commit();
Toast.makeText(MainActivity.this, "注册成功", Toast.LENGTH_LONG).show();
}
});
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SharedPreferences sp = getSharedPreferences(strLogInfo, MODE_APPEND);
String name = sp.getString("UserName", "");
String passwd = sp.getString("Password","");
if(etName.getText().toString().equals(name) && etPassword.getText().toString().equals(passwd)){
Toast.makeText(MainActivity.this, "登陆成功", Toast.LENGTH_LONG).show();
} else{
Toast.makeText(MainActivity.this, "登录失败", Toast.LENGTH_LONG).show();
}
}
});
}
}
帧动画
一开始我的想法是直接用帧动画来做,可是我太天真了,当帧数放到 50 几张的时候,已经在有些机器上奔溃了!所以这个方案否决!
GIF动图
虽然可以显示,但是已经卡的我,已经不想看了,直接放弃
视频
在这里,我突然想到我可以直接把他做成一个小视频啊,而且可以极限压缩视频。最终,视频大小被压缩到 500K 左右。此时已经基本可以满足需求了,但是我们有好多类似的动画,要求在每个动画切换的时候要有衔接感,不能有突兀的感觉,所有在这里视频就不能很好的完成任务了,所有再次放弃,已经泪牛满面了!!!!
SurfaceView + BitmapRegionDecoder +缓存
首先回答一下:为什么会想到这个解决方案?
首先在做帧动画的时候,大约每帧之间的时间差值是 40ms 可以说速度非常快了,在如此快速的图片切换上,自然而然的想到来了使用SurfaceView。
现在再来说说为什么想到要使用这个类 BitmapRegionDecoder .这个也是从我司游戏开发人员那儿得到的经验?他们在做游戏的时候,游戏中的切图都是放在一张大图上的,然后在根据对应的 xml,json 文件,获取相应的图片,接着再来切图。对此,我想能不能把所有的动图都放到同一张的图片上呢,之后在根据对应的描述文件,裁剪出我想要的图片呢!所以就用到了 BitmapRegionDecoder. 它的作用是:于显示图片的某一块矩形区域!之后,我在找设计人员商量一一下,把图片在尽量的压缩。之后从美工那儿获取的信息是这样的:
json格式的描述文件:
{"frames": [
{
"filename": "kidbot-正常闭眼0000",
"frame": {"x":0,"y":0,"w":360,"h":300},
"rotated": false,
"trimmed": false,
"spriteSourceSize": {"x":0,"y":0,"w":360,"h":300},
"sourceSize": {"w":360,"h":300}
}
.....
}
png图片:
接下来就好做了,解析 json 格式的文件,裁剪图片。
最后说一下为什么使用缓存,其实很简单,因为切换的频率实在太高了,没有必要每次都从图片中裁剪,这里就把裁剪出来的 bitmap 缓存起来在用。从而介绍内存开销!
最后给出代码:
public class AnimView extends SurfaceView implements SurfaceHolder.Callback { private BitmapRegionDecoder bitmapRegionDecoder; private SurfaceHolder mHolder; private boolean isrunning = true; private AnimThread thread; private Paint mPaint; private int WIDTH = 0; private int HEIGHT = 0; private int state = -1; private boolean isstart = false; private boolean isblinkfirst = false; private int rate = 40; private int index = 0; private Matrix matrix; private Random rand; private Handler handler = new Handler() { public void handleMessage(android.os.Message msg) { isblinkfirst = true; }; }; private SparseArray<WeakReference<Bitmap>> weakBitmaps; private SparseArray<WeakReference<Bitmap>> cweakBitmaps; private BitmapFactory.Options options; public AnimView(Context context) { super(context); init(); } public AnimView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public AnimView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } @SuppressLint("NewApi") private void init() { weakBitmaps = new SparseArray<WeakReference<Bitmap>>(); cweakBitmaps = new SparseArray<WeakReference<Bitmap>>(); mHolder = getHolder(); mHolder.addCallback(this); mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); setState(FaceBean.BLINK); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); matrix = new Matrix(); float[] values = { -1f, 0.0f, 0.0f, 0.0f, 1f, 0.0f, 0.0f, 0.0f, 1.0f }; matrix.setValues(values); WindowManager manger = (WindowManager) getContext().getSystemService( Context.WINDOW_SERVICE); DisplayMetrics displayMetrics = new DisplayMetrics(); manger.getDefaultDisplay().getMetrics(displayMetrics); WIDTH = displayMetrics.widthPixels / 2; HEIGHT = displayMetrics.heightPixels / 2; rand = new Random(); options = new Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; } @Override public void surfaceCreated(SurfaceHolder holder) { handler.sendEmptyMessageDelayed(0, 1000 * (4 + rand.nextInt(4))); thread = new AnimThread(); thread.start(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { if (thread != null) { thread.stopThread(); } } public class AnimThread extends Thread { @Override public void run() { super.run(); SurfaceHolder holder = mHolder; while (isrunning) { Canvas canvas = holder.lockCanvas(); if (canvas == null) continue; synchronized (AnimThread.class) { AnimBean.Frames frames; switch (state) { case FaceBean.BLINK: frames = KidbotRobotApplication.animBlink.getFrames() .get(index); if (frames.getFrame().getW() <= 0) { } else { Rect rect = new Rect(frames.getFrame().getX(), frames.getFrame().getY(), frames.getFrame() .getX() + frames.getSourceSize().getW(), frames.getFrame().getY() + frames.getSourceSize().getH()); WeakReference<Bitmap> weakBitmap = weakBitmaps .get(index); Bitmap map = null; if (weakBitmap == null) { map = bitmapRegionDecoder.decodeRegion(rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } else { map=weakBitmap.get(); if (map == null) { map = bitmapRegionDecoder.decodeRegion( rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } } if (map == null) { holder.unlockCanvasAndPost(canvas); continue; } mPaint.setXfermode(new PorterDuffXfermode( Mode.CLEAR)); canvas.drawPaint(mPaint); mPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); canvas.drawBitmap(map, (int) (WIDTH - (map.getWidth() * 1) - 150), (int) (HEIGHT - (map.getHeight() / 2)), mPaint); canvas.drawBitmap(map, (int) (WIDTH + 150), (int) (HEIGHT - (map.getHeight() / 2)), mPaint); if (index == 0) { } if (map.isRecycled()) { map.recycle(); } } if (!isstart) { if (index < KidbotRobotApplication.animBlink .getFrames().size()) { index++; if (index == KidbotRobotApplication.animBlink .getFrames().size()) { index--; isstart = true; if (rand.nextInt(10) <= 2) { index = 1; } } } else { index--; isstart = true; } } else { if (index > 0) { index--; if (index == 0) { isstart = false; } } else { index++; isstart = false; } } if (!isblinkfirst) { index = 0; } else { if (index == KidbotRobotApplication.animBlink .getFrames().size() - 1) { isblinkfirst = false; index = 0; handler.sendEmptyMessageDelayed(0, 1000 * (4 + rand.nextInt(4))); } } break; case FaceBean.ANGRY: frames = KidbotRobotApplication.animAngry.getFrames() .get(index); if (frames.getFrame().getW() <= 0) { } else { Rect rect = new Rect(frames.getFrame().getX(), frames.getFrame().getY(), frames.getFrame() .getX() + frames.getFrame().getW(), frames.getFrame().getH() + frames.getFrame().getX()); WeakReference<Bitmap> weakBitmap = weakBitmaps .get(index); Bitmap map = null; if (weakBitmap == null) { map = bitmapRegionDecoder.decodeRegion(rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } else { map=weakBitmap.get(); if (map == null) { map = bitmapRegionDecoder.decodeRegion( rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } } if (map == null) { holder.unlockCanvasAndPost(canvas); continue; } mPaint.setXfermode(new PorterDuffXfermode( Mode.CLEAR)); canvas.drawPaint(mPaint); mPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); Bitmap dstbmp =null; weakBitmap=cweakBitmaps.get(index); if(weakBitmap==null){ dstbmp = Bitmap.createBitmap(map, 0, 0, map.getWidth(), map.getHeight(), matrix, true); cweakBitmaps.put(index, new WeakReference<Bitmap>(dstbmp)); }else{ dstbmp=weakBitmap.get(); if(dstbmp==null){ dstbmp = Bitmap.createBitmap(map, 0, 0, map.getWidth(), map.getHeight(), matrix, true); cweakBitmaps.put(index, new WeakReference<Bitmap>(dstbmp)); } } canvas.drawBitmap( map, frames.getSpriteSourceSize().getX() + (int) (WIDTH - (map.getWidth() * 1) - 150), frames.getSpriteSourceSize().getY() + (int) (HEIGHT - (map.getHeight() / 2)), mPaint); canvas.drawBitmap(dstbmp, frames .getSpriteSourceSize().getX() + (int) (WIDTH + 150), frames .getSpriteSourceSize().getY() + (int) (HEIGHT - (map.getHeight() / 2)), mPaint); if (dstbmp.isRecycled()) { dstbmp.recycle(); } if (map.isRecycled()) { map.recycle(); } } if (!isstart) { if (index < KidbotRobotApplication.animAngry .getFrames().size()) { index++; if (index == KidbotRobotApplication.animAngry .getFrames().size()) { index--; isstart = true; } } else { index--; isstart = true; } } else { if (index > 0) { index--; if (index == 0) { isstart = false; } } else { index++; isstart = false; } } break; case FaceBean.HAPPY: frames = KidbotRobotApplication.animHappy.getFrames() .get(index); if (frames.getFrame().getW() <= 0) { } else { Rect rect = new Rect(frames.getFrame().getX(), frames.getFrame().getY(), frames.getFrame() .getX() + frames.getSourceSize().getW(), frames.getFrame().getY() + frames.getSourceSize().getH()); WeakReference<Bitmap> weakBitmap = weakBitmaps .get(index); Bitmap map = null; if (weakBitmap == null) { map = bitmapRegionDecoder.decodeRegion(rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } else { map=weakBitmap.get(); if (map == null) { map = bitmapRegionDecoder.decodeRegion( rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } } if (map == null) { holder.unlockCanvasAndPost(canvas); continue; } mPaint.setXfermode(new PorterDuffXfermode( Mode.CLEAR)); canvas.drawPaint(mPaint); mPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); Bitmap dstbmp =null; weakBitmap=cweakBitmaps.get(index); if(weakBitmap==null){ dstbmp = Bitmap.createBitmap(map, 0, 0, map.getWidth(), map.getHeight(), matrix, true); cweakBitmaps.put(index, new WeakReference<Bitmap>(dstbmp)); }else{ dstbmp=weakBitmap.get(); if(dstbmp==null){ dstbmp = Bitmap.createBitmap(map, 0, 0, map.getWidth(), map.getHeight(), matrix, true); cweakBitmaps.put(index, new WeakReference<Bitmap>(dstbmp)); } } canvas.drawBitmap( map, frames.getSpriteSourceSize().getX() + (int) (WIDTH - (map.getWidth() * 1) - 150), frames.getSpriteSourceSize().getY() + (int) (HEIGHT - (map.getHeight() / 2)), mPaint); canvas.drawBitmap(dstbmp, frames .getSpriteSourceSize().getX() + (int) (WIDTH + 150), frames .getSpriteSourceSize().getY() + (int) (HEIGHT - (map.getHeight() / 2)), mPaint); // if (dstbmp.isRecycled()) { // dstbmp.recycle(); // } // if (map.isRecycled()) { // map.recycle(); // } } if (!isstart) { if (index < KidbotRobotApplication.animHappy .getFrames().size()) { index++; if (index == KidbotRobotApplication.animHappy .getFrames().size()) { index--; isstart = true; } } else { index--; isstart = true; } } else { if (index > 0) { index--; if (index == 0) { isstart = false; } } else { index++; isstart = false; } } break; case FaceBean.RESOLVE: break; case FaceBean.RISUS: break; case FaceBean.SEERIGHT: break; case FaceBean.SAD: frames = KidbotRobotApplication.animSad.getFrames() .get(index); if (frames.getFrame().getW() <= 0) { } else { Rect rect = new Rect(frames.getFrame().getX(), frames.getFrame().getY(), frames.getFrame() .getX() + frames.getSourceSize().getW(), frames.getFrame().getY() + frames.getSourceSize().getH()); WeakReference<Bitmap> weakBitmap = weakBitmaps .get(index); Bitmap map = null; if (weakBitmap == null) { map = bitmapRegionDecoder.decodeRegion(rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } else { map=weakBitmap.get(); if (map == null) { map = bitmapRegionDecoder.decodeRegion( rect, options); weakBitmaps.put(index, new WeakReference<Bitmap>(map)); } } if (map == null) { holder.unlockCanvasAndPost(canvas); continue; } mPaint.setXfermode(new PorterDuffXfermode( Mode.CLEAR)); canvas.drawPaint(mPaint); mPaint.setXfermode(new PorterDuffXfermode(Mode.SRC)); Bitmap dstbmp =null; weakBitmap=cweakBitmaps.get(index); if(weakBitmap==null){ dstbmp = Bitmap.createBitmap(map, 0, 0, map.getWidth(), map.getHeight(), matrix, true); cweakBitmaps.put(index, new WeakReference<Bitmap>(dstbmp)); }else{ dstbmp=weakBitmap.get(); if(dstbmp==null){ dstbmp = Bitmap.createBitmap(map, 0, 0, map.getWidth(), map.getHeight(), matrix, true); cweakBitmaps.put(index, new WeakReference<Bitmap>(dstbmp)); } } canvas.drawBitmap( map, frames.getSpriteSourceSize().getX() + (int) (WIDTH - (map.getWidth() * 1) - 150), frames.getSpriteSourceSize().getY() + (int) (HEIGHT - (map.getHeight() / 2)), mPaint); canvas.drawBitmap(dstbmp, frames .getSpriteSourceSize().getX() + (int) (WIDTH + 150), frames .getSpriteSourceSize().getY() + (int) (HEIGHT - (map.getHeight() / 2)), mPaint); if (dstbmp.isRecycled()) { dstbmp.recycle(); } if (map.isRecycled()) { map.recycle(); } } if (!isstart) { if (index < KidbotRobotApplication.animSad .getFrames().size()) { index++; if (index == KidbotRobotApplication.animSad .getFrames().size()) { index--; isstart = true; } } else { index--; isstart = true; } } else { if (index > 0) { index--; if (index == 0) { isstart = false; } } else { index++; isstart = false; } } break; default: break; } } holder.unlockCanvasAndPost(canvas); try { Thread.sleep(rate); } catch (InterruptedException e) { e.printStackTrace(); } } } public void stopThread() { isrunning = false; try { join(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void setRate(int rate) { this.rate = rate; } public int getState() { return this.state; } public synchronized void setState(int state) { // if (FaceBean.BLINK == this.state) { // while ((index != KidbotRobotApplication.animBlink.getFrames() // .size() - 1)) { // continue; // } // } cweakBitmaps.clear(); weakBitmaps.clear(); this.state = state; this.index = 0; switch (state) { case FaceBean.BLINK: try { bitmapRegionDecoder = BitmapRegionDecoder.newInstance( getContext().getAssets().open("kidbot_blink.png"), false); } catch (IOException e) { e.printStackTrace(); } break; case FaceBean.ANGRY: try { bitmapRegionDecoder = BitmapRegionDecoder.newInstance( getContext().getAssets().open("kidbot_angry.png"), false); } catch (IOException e) { e.printStackTrace(); } break; case FaceBean.HAPPY: try { bitmapRegionDecoder = BitmapRegionDecoder.newInstance( getContext().getAssets().open("kidbot_happy.png"), false); } catch (IOException e) { e.printStackTrace(); } break; case FaceBean.RESOLVE: try { bitmapRegionDecoder = BitmapRegionDecoder.newInstance( getContext().getAssets().open("kidbot_blink.png"), false); } catch (IOException e) { e.printStackTrace(); } break; case FaceBean.RISUS: try { bitmapRegionDecoder = BitmapRegionDecoder.newInstance( getContext().getAssets().open("kidbot_blink.png"), false); } catch (IOException e) { e.printStackTrace(); } break; case FaceBean.SEERIGHT: break; case FaceBean.SAD: try { bitmapRegionDecoder = BitmapRegionDecoder.newInstance( getContext().getAssets().open("kidbot_sad.png"), false); } catch (IOException e) { e.printStackTrace(); } break; } } public synchronized void setRunning(boolean isrunning) { this.isrunning = isrunning; } public synchronized void addIndex() { this.index++; } }
Android高效加载大图、多图解决方案,有效避免程序OOM
比如说系统图片库里展示的图片大都是用手机摄像头拍出来的,这些图片的分辨率会比我们手机屏幕的分辨率高得多。大家应该知道,我们编写的应用程序都是有一定内存限制的,程序占用了过高的内存就容易出现OOM(OutOfMemory)异常。
我们可以通过下面的代码看出每个应用程序最高可用内存是多少。
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
Log.d("TAG", "Max memory is " + maxMemory + "KB");
因此在展示高分辨率图片的时候,最好先将图片进行压缩。压缩后的图片大小应该和用来展示它的控件大小相近,在一个很小的ImageView上显示一张超大的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存,而且在性能上还可能会带来负面影响。下面我们就来看一看,如何对一张大图片进行适当的压缩,让它能够以最佳大小显示的同时,还能防止OOM的出现。
BitmapFactory这个类提供了多个解析方法(decodeByteArray, decodeFile, decodeResource等)用于创建Bitmap对象,我们应该根据图片的来源选择合适的方法。比如SD卡中的图片可以使用decodeFile方法,网络上的图片可以使用decodeStream方法,资源文件中的图片可以使用decodeResource方法。这些方法会尝试为已经构建的bitmap分配内存,这时就会很容易导致OOM出现。为此每一种解析方法都提供了一个可选的BitmapFactory.Options参数,将这个参数的inJustDecodeBounds属性设置为true就可以让解析方法禁止为bitmap分配内存,返回值也不再是一个Bitmap对象,而是null。虽然Bitmap是null了,但是BitmapFactory.Options的outWidth、outHeight和outMimeType属性都会被赋值。这个技巧让我们可以在加载图片之前就获取到图片的长宽值和MIME类型,从而根据情况对图片进行压缩。如下代码所示:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
为了避免OOM异常,最好在解析每张图片的时候都先检查一下图片的大小,除非你非常信任图片的来源,保证这些图片都不会超出你程序的可用内存。
现在图片的大小已经知道了,我们就可以决定是把整张图片加载到内存中还是加载一个压缩版的图片到内存中。以下几个因素是我们需要考虑的:
预估一下加载整张图片所需占用的内存。
为了加载这一张图片你所愿意提供多少内存。
用于展示这张图片的控件的实际大小。
当前设备的屏幕尺寸和分辨率。
比如,你的ImageView只有128*96像素的大小,只是为了显示一张缩略图,这时候把一张1024*768像素的图片完全加载到内存中显然是不值得的。
那我们怎样才能对图片进行压缩呢?
通过设置BitmapFactory.Options中inSampleSize的值就可以实现。比如我们有一张2048*1536像素的图片,将inSampleSize的值设置为4,就可以把这张图片压缩成512*384像素。原本加载这张图片需要占用13M的内存,压缩后就只需要占用0.75M了(假设图片是ARGB_8888类型,即每个像素点占用4个字节)。下面的方法可以根据传入的宽和高,计算出合适的inSampleSize值:
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // 源图片的高度和宽度 final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { // 计算出实际宽高和目标宽高的比率 final int heightRatio = Math.round((float) height / (float) reqHeight); final int widthRatio = Math.round((float) width / (float) reqWidth); // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高 // 一定都会大于等于目标的宽和高。 inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; } return inSampleSize; }
使用这个方法,首先你要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片了。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // 调用上面定义的方法计算inSampleSize值 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 使用获取到的inSampleSize值再次解析图片 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); }
下面的代码非常简单地将任意一张图片压缩成100*100的缩略图,并在ImageView上展示。
mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
使用图片缓存技术
在你应用程序的UI界面加载一张图片是一件很简单的事情,但是当你需要在界面上加载一大堆图片的时候,情况就变得复杂起来。在很多情况下,(比如使用ListView, GridView 或者 ViewPager 这样的组件),屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,最终导致OOM。
为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC操作。用这种思路来解决问题是非常好的,可是为了能让程序快速运行,在界面上迅速地加载图片,你又必须要考虑到某些图片被回收之后,用户又将它重新滑入屏幕这种情况。这时重新去加载一遍刚刚加载过的图片无疑是性能的瓶颈,你需要想办法去避免这个情况的发生。
这个时候,使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。
内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
为了能够选择一个合适的缓存大小给LruCache, 有以下多个因素应该放入考虑范围内,例如:
你的设备可以为每个应用程序分配多大的内存?
设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
你的设备的屏幕大小和分辨率分别是多少?一个超高分辨率的设备(例如 Galaxy Nexus) 比起一个较低分辨率的设备(例如 Nexus S),在持有相同数量图片的时候,需要更大的缓存空间。
图片的尺寸和大小,还有每张图片会占据多少内存空间。
图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个LruCache 对象来区分不同组的图片。
你能维持好数量和质量之间的平衡吗?有些时候,存储多个低像素的图片,而在后台去开线程加载高像素的图片会更加的有效。
并没有一个指定的缓存大小可以满足所有的应用程序,这是由你决定的。你应该去分析程序内存的使用情况,然后制定出一个合适的解决方案。一个太小的缓存空间,有可能造成图片频繁地被释放和重新加载,这并没有好处。而一个太大的缓存空间,则有可能还是会引起 java.lang.OutOfMemory 的异常。
下面是一个使用 LruCache 来缓存图片的例子:
private LruCache<String, Bitmap> mMemoryCache; @Override protected void onCreate(Bundle savedInstanceState) { // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 // LruCache通过构造函数传入缓存值,以KB为单位。 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // 使用最大可用内存值的1/8作为缓存的大小。 int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // 重写此方法来衡量每张图片的大小,默认返回图片数量。 return bitmap.getByteCount() / 1024; } }; } public void addBitmapToMemoryCache(String key, Bitmap bitmap) { if (getBitmapFromMemCache(key) == null) { mMemoryCache.put(key, bitmap); } } public Bitmap getBitmapFromMemCache(String key) { return mMemoryCache.get(key); }
在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4兆(32/8)的缓存空间。一个全屏幕的 GridView 使用4张 800x480分辨率的图片来填充,则大概会占用1.5兆的空间(800*480*4)。因此,这个缓存大小可以存储2.5页的图片。
当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。如果找到了相应的键值,则会立刻更新ImageView ,否则开启一个后台线程来加载这张图片。
public void loadBitmap(int resId, ImageView imageView) { final String imageKey = String.valueOf(resId); final Bitmap bitmap = getBitmapFromMemCache(imageKey); if (bitmap != null) { imageView.setImageBitmap(bitmap); } else { imageView.setImageResource(R.drawable.image_placeholder); BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId); } }
BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { // 在后台加载图片。 @Override protected Bitmap doInBackground(Integer... params) { final Bitmap bitmap = decodeSampledBitmapFromResource( getResources(), params[0], 100, 100); addBitmapToMemoryCache(String.valueOf(params[0]), bitmap); return bitmap; } }
掌握了以上两种方法,不管是要在程序中加载超大图片,还是要加载大量图片,都不用担心OOM的问题了!不过仅仅是理论地介绍不知道大家能不能完全理解,在后面的文章中我会演示如何在实际程序中灵活运用上述技巧来避免程序OOM,敬请期待。
如何实现两个TextView的跑马灯效果,如果使用常规的做法,只能够使一个TextView起作用。 现在方法具体如下:
1.为TextView增加四个属性
android:ellipsize="marquee" android:focusable="true" android:focusableInTouchMode="true" android:singleLine="true"
实现TextView类,实现三个构造函数并重载 isFocused方法。
public class MarqueeText extends TextView { public MarqueeText(Context context) { super(context); } public MarqueeText(Context context, AttributeSet attrs) { super(context, attrs); } public MarqueeText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public boolean isFocused() { return true; } }
在main.xml文件中使用自己实现的TextView类。
这样就实现了两个跑马灯的效果了。 这里主要是重写了isFocuse方法,这样默认两个TextView都被Focuse了,所以这两个TextView都可以跑马灯了。
Android实现多个TextView同时显示跑马灯效果
如果在一个较小的android页面中,需要显示较长的文案时无法显示完全,于是很自然地想到了TextView中的marquee —— 跑马灯效果,可是Android执行跑马灯效果需要控件获取焦点,当某一控件requestFocus()时,会将其他控件的焦点抢去,这就导致了同一时间只能有一个控件获取焦点。那么,如果让多个控件同时获取并持有焦点呢?或者说,“欺骗”Android系统,让它以为多个控件都持有焦点,即每个控件都在焦点状态。
通过上述分析,问题转化为如何让多个控件同时处于焦点状态,这就需要重写TextView的部分方法,达到“欺骗”Android系统的目的,这样每个控件requestFocus()之后,均让自己处于焦点状态,并且不可被剥夺焦点,就可以达到多个控件同时“持有”焦点了,代码如下:
public class MarqueeTextView extends TextView { public MarqueeTextView(Context context) { this(context, null); } public MarqueeTextView(Context context, AttributeSet attrs) { super(context, attrs); setFocusable(true); setFocusableInTouchMode(true); setSingleLine(); setEllipsize(TextUtils.TruncateAt.MARQUEE); setMarqueeRepeatLimit(-1); } public MarqueeTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setFocusable(true); setFocusableInTouchMode(true); setSingleLine(); setEllipsize(TextUtils.TruncateAt.MARQUEE); setMarqueeRepeatLimit(-1); } @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { if (focused) { super.onFocusChanged(focused, direction, previouslyFocusedRect); } } @Override public void onWindowFocusChanged(boolean focused) { if (focused) { super.onWindowFocusChanged(focused); } } @Override public boolean isFocused() { return true; } }
由于常规执行跑马灯的属性:
android:focusable="true"、android:focusableInTouchMode="true"、android:singleLine="true"、android:ellipsize="marquee"
在代码中均已设置,故引用此控件时,无需添加上述4个属性。
学习内容:
例一、使用StringRequest实现获取服务器的字符串响应...
Android的Volley中到底实现了哪些请求才是我们在开发中需要进行使用的...Volley实现的东西其实并不是很多,它的主要功能是实现异步进行网络请求和图片加载,其实就是异步加载解析Json数据,异步获取服务器的字符串数据,异步实现网络图片的动态加载,还有一个请求就是清空缓存的请求,不过使用的地方不是很多,主要还是前面三个方面,因此Volley相对于AndBase来说,其实还算是一个轻量级的框架,而AndBase涉及到的东西就更加的广泛,全面,但是网络请求这一部分使用Volley基本算是够用了...
1.StringRequest.java
package com.android.volley.toolbox; import com.android.volley.NetworkResponse; import com.android.volley.Request; import com.android.volley.Response; import com.android.volley.Response.ErrorListener; import com.android.volley.Response.Listener; import java.io.UnsupportedEncodingException; public class StringRequest extends Request<String> { private final Listener<String> mListener; //请求成功的监听... //根据指定的请求方式和url创建一个StringRquest对象... public StringRequest(int method, String url, Listener<String> listener, ErrorListener errorListener) { super(method, url, errorListener); //设置请求方式,url,以及错误监听.. mListener = listener; //设置成功监听... } //根据指定的url来创建一个StringRequest对象,请求方式默认为GET.. public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) { this(Method.GET, url, listener, errorListener); } //这里涉及到发送响应的过程了...表示整个请求的响应已经返回... @Override protected void deliverResponse(String response) { mListener.onResponse(response); } //对响应的解析过程... @Override protected Response<String> parseNetworkResponse(NetworkResponse response) { String parsed; try { parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); //对响应数据封装,解析字符集... } catch (UnsupportedEncodingException e) { parsed = new String(response.data); } return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));//返回请求成功... } }
上面只是StringRequest的源码实现,非常的简单...那么我们来具体的看看怎么用...
一般使用在简单的响应方式,返回一些基本的数据信息,比如说用户登录中,在发送Post请求发送用户的账号信息和密码的时候,需要服务器调取数据库进行相关查找...在完成这个响应之后需要为服务端返回响应信息,一般就是以字符串的形式进行发送的...
package com.example.oop; import com.android.volley.RequestQueue; import com.android.volley.toolbox.ImageLoader; import com.android.volley.toolbox.ImageLoader.ImageCache; import com.android.volley.toolbox.NetworkImageView; import com.android.volley.toolbox.Volley; import android.os.Bundle; import android.app.Activity; import android.graphics.Bitmap; import android.support.v4.util.LruCache; import android.view.Menu; import android.view.View; import android.view.View.OnClickListener; public class MainActivity extends Activity implements OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } public void init(){ RequestQueue queue=Volley.newRequestQueue(MainActivity.this); //首先创建一个请求队列... //然后需要向请求队列中添加相关请求... queue.add(new StringRequest("http://www.baidu.com/",new Listener <StringRequest>(){ //请求成功,接收请求方法的重写... @Override public void onResponse(String response){ System.out.println(response.toString()); } },new ErrorListener(){ //请求失败,对错误的获取... @Override public void onErrorResponse(VolleyError error){ System.out.println(error.toString()); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } }
这里我们想百度页面发送了相关的请求,那么毫无疑问,请求成功是必然的,那么返回的东西就是百度页面的原生数据,其实就是Html页面代码...那么我们无法去解析这个页面,但是浏览器却是可以的,我们可以通过浏览器去加载这个页面...这只是一个简单的小例子而已,目的是我们需要清楚,服务器返回给我们的是什么数据...
第二个例子:
这是一个中间用于过程处理的JSP方法...用于处理账户和密码,只是一个简单的方式,我们当然也可以通过它去连接数据库,完善化这个函数...这里只是一个简单的小例子...
<% String name=request.getParameter("name"); String password=request.getParameter("password"); if("darker".equals(name)&& "49681888".equals(password)){ out.println("Receive name is:"+name); out.println("Receive password is:"+password);%> Your Message are right! <%}else{ out.println("Receive name is:"+name); out.println("Receive password is:"+password);%> Your Message are wrong! <%}%>
那么Activity中需要通过Post请求发送请求参数,才能够通过这个函数来进行下一步的判断...由于Post请求中没有传递参数的方法...但是我们可以通过重写getParam()方法...来指定相关参数,服务端会自动调用getParam()中的参数....
package com.example.oop; //有一部分包没有引用,在编写的时候会自动引用的... import com.android.volley.RequestQueue; import com.android.volley.toolbox.Volley; import android.os.Bundle; import android.app.Activity; import android.view.Menu; import android.view.View; public class MainActivity extends Activity { TextView tv; String url="192.168.19.172:8080/JSP/post.jsp" @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv=(TextView)findViewById(R.id.tv_1); init(); } public void init(){ RequestQueue queue=Volley.newRequestQueue(MainActivity.this); //首先创建一个请求队列... queue.add(new StringRequest(Method.POST, url, new Listener<String>() { @Override public void onResponse(String response) { // TODO Auto-generated method stub System.out.println(response.toString()); tv.setText(response.toString()); //对获取的数据进行显示... } }, new ErrorListener() { @Override public void onErrorResponse(VolleyError error) { // TODO Auto-generated method stub System.out.println(error.toString()); } }){ //在这个方法里完成参数的相关传递.... @Override protected Map<String, String>getParams() throws AuthFailureError{ Map<String, String>map=new HashMap<String, String>(); map.put("name", "darker"); map.put("password", "49681888"); return map; } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } }
使用Post请求来完成验证,毫无疑问,这里由于我们传递的参数时正确的,因此客户端会获取到Receive name is:darker,Receive password is:49681888,Your Message are right!这段信息...
StringRequest请求非常的简单,涉及的东西也并不是很多,适合于发送网络请求来获取相应的字符串数据,呈递给客户端
相关文章
使用PHP+JavaScript将HTML页面转换为图片的实例分享
这篇文章主要介绍了使用PHP+JavaScript将HTML元素转换为图片的实例分享,文后结果的截图只能体现出替换的字体,也不能说将静态页面转为图片可以加快加载,只是这种做法比较interesting XD需要的朋友可以参考下...2016-04-19- php如何实现抓取网页图片,相较于手动的粘贴复制,使用小程序要方便快捷多了,喜欢编程的人总会喜欢制作一些简单有用的小软件,最近就参考了网上一个php抓取图片代码,封装了一个php远程抓取图片的类,测试了一下,效果还不错分享...2015-10-30
- 这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
- 今天小编在这里就来给各位Photoshop的这一款软件的使用者们来说说把古装美女图片转为细腻的工笔画效果的制作教程,各位想知道方法的使用者们,那么下面就快来跟着小编一...2016-09-14
- 这篇文章主要介绍了Python 图片转数组,二进制互转操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-09
- 下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
- 这篇文章主要介绍了源码分析系列之json_encode()如何转化一个对象,对json_encode()感兴趣的同学,可以参考下...2021-04-22
- 下面小编就为大家带来一篇利用JS实现点击按钮后图片自动切换的简单方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2016-10-25
jquery左右滚动焦点图banner图片鼠标经过显示上下页按钮
jquery左右滚动焦点图banner图片鼠标经过显示上下页按钮...2013-10-13- 这篇文章主要为大家详细介绍了js实现上传图片及时预览的相关资料,具有一定的参考价值,感兴趣的朋友可以参考一下...2016-05-09
- 拜读一个开源框架,最想学到的就是设计的思想和实现的技巧。废话不多说,jquery这么多年了分析都写烂了,老早以前就拜读过,不过这几年都是做移动端,一直御用zepto, 最近抽出点时间把jquery又给扫一遍我也不会照本宣科的翻译...2014-05-31
- Photoshop的这一款软件小编相信很多的人都已经是使用过了吧,那么今天小编在这里就给大家带来了用Photoshop软件制作枪战电影海报的教程,想知道制作步骤的玩家们,那么下面...2016-09-14
Android开发中findViewById()函数用法与简化
findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20- 如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
- 夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
- 为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
- 如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
- 图片剪裁是常用的方法,那么如何通过4坐标剪裁图片,本文就详细的来介绍一下,感兴趣的小伙伴们可以参考一下...2021-06-04
- 深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
- 下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02