1.公司项目要求,拍照时要显示自己的位置;排出的照片也要在照片上能看到位置;
大致两种思路:自定义拍照页面后,点击拍照的时候,不拍照而是截屏;第二种是拍照后,把文字等等ui要求的东西通过bitmap代码以水印的方式加上去。
截屏的话ui可以随意布局,都能得到想要的照片;但Android方面截屏参差不齐,有的还要root;我还没有细看,所以暂时用第二种方式实现。
2.先看大概的样子
这是打开相机和拍完照的样子。
3.我这里使用的是camera1;对camera2来说,各个手机厂家支持情况不太一样,项目赶时间就没用。
3.1调用相机的代码网上很多这里就不说了;文末有我的完整demo;
3.2定位使用的是高德地图,官方demo代码拷贝过来就好。
3.3说一下下面的位置信息布局跟随屏幕转换的代码
private void initOrientate(){
if(mOrientationEventListener == null){
mOrientationEventListener = new OrientationEventListener(getActivity()) {
@Override
public void onOrientationChanged(int orientation) {
// i的范围是0-359
// 屏幕左边在顶部的时候 i = 90;
// 屏幕顶部在底部的时候 i = 180;
// 屏幕右边在底部的时候 i = 270;
// 正常的情况默认i = 0;
if(45 <= orientation && orientation < 135){
takePhotoOrientation = 180;
} else if(135 <= orientation && orientation < 225){
takePhotoOrientation = 270;
} else if(225 <= orientation && orientation < 315){
takePhotoOrientation = 0;
} else {
takePhotoOrientation = 90;
}
// 只检测是否有四个角度的改变
if (orientation < 60 && orientation > 30) { // 动画0度与接口360度相反,增加下限抵消0度影响
orientation = 360;
} else if (orientation > 70 && orientation < 110) { // 动画90度与接口270度相反
orientation = 270;
} else if (orientation > 160 && orientation < 200) { // 180度
orientation = 180;
} else if (orientation > 240 && orientation < 300) {
orientation = 90;
} else if (orientation > 320 && orientation < 340) {// 减少上限减少360度的影响
orientation = 0;
} else {
return;
}
if (oldOrientation != orientation) {
ObjectAnimator rotation = ObjectAnimator
.ofFloat(ll_photo_message, "Rotation", oldOrientation,
orientation).setDuration(0);
int photoMessageHeight = ll_photo_message.getHeight();
System.out.println("ll_photo_message--h---->"+photoMessageHeight+"--w---->"+screenWidth);
System.out.println("orientation------>"+orientation);
if (orientation == 270) {
ll_photo_message.setPivotX(screenWidth/2);
ll_photo_message.setPivotY(-(screenWidth/2-photoMessageHeight));
}else if(orientation == 90){
ll_photo_message.setPivotX(screenWidth/2);
ll_photo_message.setPivotY(-(screenWidth/2-photoMessageHeight));
}else if(orientation == 180){
ll_photo_message.setPivotX(screenWidth/2);
ll_photo_message.setPivotY(-(screenWidth/2-photoMessageHeight));
}else {
ll_photo_message.setPivotX(0);
ll_photo_message.setPivotY(0);
}
rotation.start();
if (orientation == 270 || orientation == 0){
ObjectAnimator translationY = ObjectAnimator.ofFloat(ll_photo_message, "translationY",
-(cameraPreview.getHeight() - screenWidth), 0);
translationY.setDuration(0);
translationY.start();
} else if(orientation == 90 || orientation == 180){
ObjectAnimator translationY = ObjectAnimator.ofFloat(ll_photo_message, "translationY",
0,-(cameraPreview.getHeight() - screenWidth));
translationY.setDuration(0);
translationY.start();
}
ll_photo_message.clearAnimation();
oldOrientation = orientation;
}
}
};
}
mOrientationEventListener.enable();
}
首先,最上面是通过手机方向确定相机的方向,保证拍的照片是正向的;关键在下面给view做属性动画;因为相机的框是长方形的,只做旋转是不能达到效果的;所以当做不同位置旋转时,就要再加上平移的操作了;这样就能保证文字提示永远在屏幕的左下角了。
可以卡到手机在四个方向旋转时,文字会始终在屏幕的左下角显示。至于里面具体的宽度高度,就是相机显示的ui宽高和屏幕的宽高来做旋转和平移。
ObjectAnimator rotation = ObjectAnimator
.ofFloat(ll_photo_message, "Rotation", oldOrientation,
orientation).setDuration(0);
rotation.start();
以上是旋转的代码,我们都知道;
那么怎么设置旋转的圆心呢?
ll_photo_message.setPivotX(screenWidth/2);
ll_photo_message.setPivotY(-(screenWidth/2-photoMessageHeight));
setPivotX和setPivotY就是设置的旋转的中心点了;但这个数是 要旋转的控件的左上角那个点的相对位置。找好旋转中心点后,算出中心点距离旋转控件的左上角的距离,向上,向左是负值,向下向右是正值。
3.4拍完照片后,要把水印加上去
package com.example.android.camera2basic.camera1;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.RectF;
import android.hardware.Camera;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.Layout;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import com.example.android.camera2basic.R;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import androidx.core.content.FileProvider;
public class BitmapManager {
private Context context;
public BitmapManager(Context context) {
this.context = context;
}
public File createFile(String fileName, String dirName) {
String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
.getAbsoluteFile() + File.separator + dirName;
File mIVMSFolder = new File(path);
if (!mIVMSFolder.exists()) {
mIVMSFolder.mkdirs();
}
return new File(mIVMSFolder.getAbsolutePath(), fileName);
}
public Uri getUriFromFile(Context context, File file){
if (Build.VERSION.SDK_INT >= 24) {
return FileProvider.getUriForFile(context,context.getPackageName()+".provider", file);
} else {
return Uri.fromFile(file);
}
}
public void getPhoto(File mFile, byte[] imageData, int mCameraId, int takePhotoOrientation,String position, String project) {
FileOutputStream fos = null;
try {
fos = new FileOutputStream(mFile);
fos.write(imageData);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
// 获得图片
Bitmap mBitmap = BitmapFactory.decodeFile(mFile.getPath());
//添加时间水印
Bitmap newBitmap = AddTimeWatermark(mBitmap,mCameraId, takePhotoOrientation, position, project);
if (onBitmapCompleteListener != null)
onBitmapCompleteListener.OnBitmapComplete(newBitmap);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public Bitmap AddTimeWatermark(Bitmap mBitmap, int mCameraId, int takePhotoOrientation,String position, String project) {
Matrix matrix = new Matrix();
matrix.postRotate(Float.valueOf(takePhotoOrientation));
if(mCameraId == 1){
if(takePhotoOrientation == 90){
matrix.postRotate(180f);
}
}
//获取原始图片与水印图片的宽与高
Bitmap mNewBitmap = Bitmap.createBitmap(mBitmap, 0, 0,
mBitmap.getWidth(), mBitmap.getHeight(), matrix, true);
//新增 如果是前置 需要镜面翻转处理
if(mCameraId == 1){
Matrix matrix1 = new Matrix();
matrix1.postScale(-1f,1f);
mNewBitmap = Bitmap.createBitmap(mNewBitmap, 0, 0,
mNewBitmap.getWidth(), mNewBitmap.getHeight(), matrix1, true);
}
Bitmap mNewBitmap2 = Bitmap.createBitmap(mNewBitmap.getWidth(), mNewBitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas mCanvas = new Canvas(mNewBitmap2);
//向位图中开始画入MBitmap原始图片
mCanvas.drawBitmap(mNewBitmap,0,0,null);
//添加文字
Paint mPaint = new Paint();
mPaint.setColor(Color.WHITE);
mPaint.setTextSize(SystemUtil.dp2px(context,40));
//根据路径得到Typeface
// Typeface typeface=Typeface.createFromAsset(context.getAssets(), "fonts/xs.ttf");
// mPaint.setTypeface(typeface);
// mCanvas.rotate(-45, (mNewBitmap2.getWidth() * 1) / 2, (mNewBitmap2.getHeight() * 1) / 2);
boolean isShuPing = takePhotoOrientation == 0 || takePhotoOrientation == 90;
//矩形背景
Paint bgRect=new Paint();
bgRect.setStyle(Paint.Style.FILL);
bgRect.setColor(context.getResources().getColor(R.color.gray));
RectF rectF=new RectF(SystemUtil.dp2px(context,10),
mNewBitmap.getHeight()-SystemUtil.dp2px(context,isShuPing?120:90),
mNewBitmap.getWidth()-SystemUtil.dp2px(context,50),
mNewBitmap.getHeight());
mCanvas.drawRect(rectF, bgRect);
//画时间
drawPosition(mNewBitmap, mCanvas,
new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date()),
R.drawable.line,
SystemUtil.dp2px(context,20),
mNewBitmap.getHeight()-SystemUtil.dp2px(context,isShuPing?120:90),
0);
//画位置
drawPosition(mNewBitmap, mCanvas,
position,
R.drawable.location,
SystemUtil.dp2px(context,20),
mNewBitmap.getHeight()-SystemUtil.dp2px(context,isShuPing?90:60),
1);
//画项目名称
drawPosition(mNewBitmap, mCanvas,
project,
R.drawable.dot,
SystemUtil.dp2px(context,20),
mNewBitmap.getHeight()-SystemUtil.dp2px(context,isShuPing?50:30),
2);
mCanvas.save();
mCanvas.restore();
return mNewBitmap2;
}
public void drawPosition(Bitmap mNewBitmap, Canvas mCanvas, String mFormat, int drawable,
int x, int y, int type) {
if (TextUtils.isEmpty(mFormat)) return;
Bitmap resource = BitmapFactory.decodeResource(context.getResources(), drawable);
int width = resource.getWidth();
int height = resource.getHeight();
// 设置想要的大小
int newWidth = 0;
int newHeight = 0;
int disX = 0;
int disY = 0;
switch (type){
case 0:
newWidth = SystemUtil.dp2px(context,2);
newHeight = SystemUtil.dp2px(context,12);
disX = SystemUtil.dp2px(context,4);
disY = SystemUtil.dp2px(context,5);
break;
case 1:
newWidth = SystemUtil.dp2px(context,10);
newHeight = SystemUtil.dp2px(context,10);
disX = SystemUtil.dp2px(context,0);
disY = SystemUtil.dp2px(context,4);
break;
case 2:
newWidth = SystemUtil.dp2px(context,3);
newHeight = SystemUtil.dp2px(context,3);
disX = SystemUtil.dp2px(context,3.5f);
disY = SystemUtil.dp2px(context,5);
break;
}
// 计算缩放比例
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// 取得想要缩放的matrix参数
Matrix matrix2 = new Matrix();
matrix2.postScale(scaleWidth, scaleHeight);
// 得到新的图片
resource = Bitmap.createBitmap(resource, 0, 0, width, height, matrix2, true);
mCanvas.drawBitmap(resource, x+disX, y+disY, null);
TextPaint textPaint = new TextPaint();
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(type == 0 ?
SystemUtil.dp2px(context,16):SystemUtil.dp2px(context,12));
StaticLayout staticLayout = new StaticLayout(mFormat, textPaint,
mNewBitmap.getWidth()-SystemUtil.dp2px(context,100),
Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
mCanvas.save();
mCanvas.translate(2*x, y);
staticLayout.draw(mCanvas);
mCanvas.restore();
// for (int i = 0; i < Layout.getLineCount(); i++) {
// Rect rect = new Rect();
// Paint bgRect=new Paint();
// bgRect.setStyle(Paint.Style.FILL);
// bgRect.setColor(Color.YELLOW);
// Layout.getLineBounds(i, rect);
// mCanvas.drawRect(rect, bgRect);
// }
// Rect rect = new Rect();
// float v = textPaint.measureText(mFormat, 0, mFormat.length() - 1);
Paint bgRect=new Paint();
// textPaint.setStyle(Paint.Style.FILL);
// textPaint.setColor(Color.parseColor("#66FFFF00"));
// RectF rectF=new RectF(3*x, y, x+v, y+(isTime?SystemUtil.dp2px(context,40):SystemUtil.dp2px(context,30))+SystemUtil.dp2px(context,10));
// mCanvas.drawRect(rectF, textPaint);
}
public void saveBitmapFile(File mFile, Bitmap bitmap){
if (null == bitmap) return;
try {
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(mFile));
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
bos.flush();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public interface OnBitmapCompleteListener{
void OnBitmapComplete(Bitmap bitmap);
}
public OnBitmapCompleteListener onBitmapCompleteListener;
public void setOnBitmapCompleteListener(OnBitmapCompleteListener onBitmapCompleteListener) {
this.onBitmapCompleteListener = onBitmapCompleteListener;
}
}
这个类中就是把拍好的照片转换成bitmap;然后通过代码画文字,画图片到bitmap上;这样就实现了水印。
里面就是一些canvas的drowText,drowBitmap的方法;
TextPaint textPaint = new TextPaint();
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(type == 0 ?
SystemUtil.dp2px(context,16):SystemUtil.dp2px(context,12));
StaticLayout staticLayout = new StaticLayout(mFormat, textPaint,
mNewBitmap.getWidth()-SystemUtil.dp2px(context,100),
Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
mCanvas.save();
mCanvas.translate(2*x, y);
staticLayout.draw(mCanvas);
mCanvas.restore();
上面这段代码可以画多行的文字;其实就是StaticLayout的作用,view画图中都有详细介绍。
3.5选择位置
这部分内容就是高德地图,先定位到当前的位置,如果不太准确就点击右侧的搜索按钮,搜索周边的poi;然后列表形式展示出来。
package com.example.android.camera2basic.camera1;
import android.content.Context;
import com.amap.api.location.AMapLocation;
import com.amap.api.location.AMapLocationClient;
import com.amap.api.location.AMapLocationClientOption;
import com.amap.api.location.AMapLocationClientOption.AMapLocationMode;
import com.amap.api.location.AMapLocationClientOption.AMapLocationProtocol;
import com.amap.api.location.AMapLocationListener;
import com.amap.api.location.AMapLocationQualityReport;
import com.amap.api.services.core.LatLonPoint;
import com.amap.api.services.core.PoiItem;
import com.amap.api.services.geocoder.GeocodeResult;
import com.amap.api.services.geocoder.GeocodeSearch;
import com.amap.api.services.geocoder.GeocodeSearch.OnGeocodeSearchListener;
import com.amap.api.services.geocoder.RegeocodeQuery;
import com.amap.api.services.geocoder.RegeocodeResult;
import com.amap.api.services.poisearch.PoiResult;
import com.amap.api.services.poisearch.PoiSearch;
import com.amap.api.services.poisearch.PoiSearch.OnPoiSearchListener;
import com.amap.api.services.poisearch.PoiSearch.SearchBound;
import java.text.SimpleDateFormat;
import java.util.Date;
public class LocationManager {
private AMapLocationClient locationClient = null;
private AMapLocationClientOption locationOption = null;
private GeocodeSearch geocoderSearch;
private PoiSearch.Query query;
private PoiSearch poiSearch;
public Context context;
public OnPoiSearchListener onPoiSearchListener;
public AMapLocationListener aMapLocationListener;
public void setOnPoiSearchListener(OnPoiSearchListener onPoiSearchListener) {
this.onPoiSearchListener = onPoiSearchListener;
}
public void setaMapLocationListener(AMapLocationListener aMapLocationListener) {
this.aMapLocationListener = aMapLocationListener;
}
public LocationManager(Context context) {
this.context = context;
}
public void initLocation(){
//初始化client
locationClient = new AMapLocationClient(context);
locationOption = getDefaultOption();
//设置定位参数
locationClient.setLocationOption(locationOption);
// 设置定位监听
locationClient.setLocationListener(aMapLocationListener);
//poi查询
//keyWord表示搜索字符串,
//第二个参数表示POI搜索类型,二者选填其一,选用POI搜索类型时建议填写类型代码,码表可以参考下方(而非文字)
//cityCode表示POI搜索区域,可以是城市编码也可以是城市名称,也可以传空字符串,空字符串代表全国在全国范围内进行搜索
query = new PoiSearch.Query("汽车服务|汽车销售|\n" +
" 汽车维修|摩托车服务|餐饮服务|购物服务|生活服务|体育休闲服务|医疗保健服务|\n" +
" 住宿服务|风景名胜|商务住宅|政府机构及社会团体|科教文化服务|交通设施服务|\n" +
" 金融保险服务|公司企业|道路附属设施|地名地址信息|公共设施", "", "");
query.setPageSize(10);// 设置每页最多返回多少条poiitem
poiSearch = new PoiSearch(context, query);
poiSearch.setOnPoiSearchListener(onPoiSearchListener);
}
public void setBound(LatLonPoint latLonPoint, int distance) {
//设置周边搜索的中心点以及半径
if (poiSearch != null) {
poiSearch.setBound(new SearchBound(latLonPoint, distance));
}
}
public void query(int currentPage) {
query.setPageNum(currentPage);//设置查询页码
poiSearch.searchPOIAsyn();
}
private AMapLocationClientOption getDefaultOption(){
AMapLocationClientOption mOption = new AMapLocationClientOption();
mOption.setLocationMode(AMapLocationMode.Hight_Accuracy);//可选,设置定位模式,可选的模式有高精度、仅设备、仅网络。默认为高精度模式
mOption.setGpsFirst(false);//可选,设置是否gps优先,只在高精度模式下有效。默认关闭
mOption.setHttpTimeOut(30000);//可选,设置网络请求超时时间。默认为30秒。在仅设备模式下无效
mOption.setInterval(2000);//可选,设置定位间隔。默认为2秒
mOption.setNeedAddress(true);//可选,设置是否返回逆地理地址信息。默认是true
mOption.setOnceLocation(false);//可选,设置是否单次定位。默认是false
mOption.setOnceLocationLatest(false);//可选,设置是否等待wifi刷新,默认为false.如果设置为true,会自动变为单次定位,持续定位时不要使用
AMapLocationClientOption.setLocationProtocol(AMapLocationProtocol.HTTP);//可选, 设置网络请求的协议。可选HTTP或者HTTPS。默认为HTTP
mOption.setSensorEnable(false);//可选,设置是否使用传感器。默认是false
mOption.setWifiScan(true); //可选,设置是否开启wifi扫描。默认为true,如果设置为false会同时停止主动刷新,停止以后完全依赖于系统刷新,定位位置可能存在误差
mOption.setLocationCacheEnable(true); //可选,设置是否使用缓存定位,默认为true
return mOption;
}
public String getGPSStatusString(int statusCode){
String str = "";
switch (statusCode){
case AMapLocationQualityReport.GPS_STATUS_OK:
str = "GPS状态正常";
break;
case AMapLocationQualityReport.GPS_STATUS_NOGPSPROVIDER:
str = "手机中没有GPS Provider,无法进行GPS定位";
break;
case AMapLocationQualityReport.GPS_STATUS_OFF:
str = "GPS关闭,建议开启GPS,提高定位质量";
break;
case AMapLocationQualityReport.GPS_STATUS_MODE_SAVING:
str = "选择的定位模式中不包含GPS定位,建议选择包含GPS定位的模式,提高定位质量";
break;
case AMapLocationQualityReport.GPS_STATUS_NOGPSPERMISSION:
str = "没有GPS定位权限,建议开启gps定位权限";
break;
}
return str;
}
public void startLocation(){
System.out.println("========开始定位");
// 设置定位参数
locationClient.setLocationOption(locationOption);
// 启动定位
locationClient.startLocation();
}
public void destroyLocation(){
if (null != locationClient) {
/**
* 如果AMapLocationClient是在当前Activity实例化的,
* 在Activity的onDestroy中一定要执行AMapLocationClient的onDestroy
*/
locationClient.onDestroy();
locationClient = null;
locationOption = null;
}
}
//逆坐标查询
private void niQuery(LatLonPoint latLonPoint) {
geocoderSearch = new GeocodeSearch(context);
geocoderSearch.setOnGeocodeSearchListener(new OnGeocodeSearchListener() {
@Override
public void onRegeocodeSearched(RegeocodeResult regeocodeResult, int i) {
System.out.println("你坐标:"+regeocodeResult.getRegeocodeAddress().getCity()+"\n"
+regeocodeResult.getRegeocodeAddress().getDistrict()+"\n"
+regeocodeResult.getRegeocodeAddress().getFormatAddress()+"\n"
+regeocodeResult.getRegeocodeAddress().getBusinessAreas()+"\n"
+regeocodeResult.getRegeocodeAddress().getPois()+"\n"
);
}
@Override
public void onGeocodeSearched(GeocodeResult geocodeResult, int i) {
}
});
//查询 第一个参数表示一个Latlng,第二参数表示范围多少米,第三个参数表示是火系坐标系还是GPS原生坐标系
RegeocodeQuery query = new RegeocodeQuery(latLonPoint, 200,GeocodeSearch.GPS);
geocoderSearch.getFromLocationAsyn(query);
}
}
这个工具类可以直接拿去用;这里搜索的poi是把20多种类别全都传进去了;当然你也可以根据需要传入自己想要的。
4.完整的代码已经放在了GitHub上;地址:https://github.com/yueshaolong/MyCamera
如果没看明白,可以把demo拉下来跑一跑。