标签: Android开发

LeakCanary原理及使用

一、LeakCanary简介
LeakCanary是Square公司为Android开发者提供的一个自动检测内存泄漏的工具,LeakCanary本质上是一个基于MAT进行Android应用程序内存泄漏自动化检测的的开源工具,我们可以通过集成LeakCanary提供的jar包到自己的工程中,一旦检测到内存泄漏,LeakCanary就会dump Memory信息,并通过另一个进程分析内存泄漏的信息并展示出来,随时发现和定位内存泄漏问题,而不用每次在开发流程中都抽出专人来进行内存泄漏问题检测,*大地方便了Android应用程序的开发。

二、LeakCanary工作机制
RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
然后在后台线程检查引用是否被清除,如果没有,调用GC。
如果引用还是未被清除,把 heap 内存 dump 到 APP对应的文件系统中的一个 .hprof 文件中。
在另外一个进程中的 HeapAnalyzerService 有一个HeapAnalyzer 使用HAHA 解析这个文件。
得益于唯一的 reference key, HeapAnalyzer 找到KeyedWeakReference,定位内存泄露。
HeapAnalyzer 计算到 GC roots的*短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
引用链传递到 APP 进程中的DisplayLeakService, 并以通知的形式展示出来。
三、LeakCanary的Android Studio集成
1. 在build.gradle中添加LeakCanary的依赖包
debugImplementation ‘com.squareup.leakcanary:leakcanary-android:1.6.3’
releaseImplementation ‘com.squareup.leakcanary:leakcanary-android-no-op:1.6.3’

注意: debug和release版本要一致,否则会报错。

2. 在我们自定义Application的onCreate方法中注册LeakCanary
public class LeakApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) { //1
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}
}

注释1处的代码用来进行过滤操作,如果当前的进程是用来给LeakCanary 进行堆分析的则return,否则会执行LeakCanary的install方法。这样我们就可以使用LeakCanary了,如果检测到某个Activity 有内存泄露,LeakCanary 就会给出提示。

3. 重写Application
上述代码只能够检测Activity的内存泄漏,当然还存在其他类的内存泄漏,这时我们就需要使用RefWatcher来进行监控。重写Application,如下所示:

public class LeakApplication extends Application {
private RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
refWatcher= setupLeakCanary();
}
private RefWatcher setupLeakCanary() {
if (LeakCanary.isInAnalyzerProcess(this)) {
return RefWatcher.DISABLED;
}
return LeakCanary.install(this);
}
public static RefWatcher getRefWatcher(Context context) {
LeakApplication leakApplication = (LeakApplication) context.getApplicationContext();
return leakApplication.refWatcher;
}
}

install方法会返回RefWatcher用来监控对象,LeakApplication中还要提供getRefWatcher静态方法来返回全局RefWatcher。
注意: 需要在AndroidManifest.xml文件中添加android:name=”.LeakApplication”,指定Application子类,当应用启动时,这个类的实例被*个创建。这个属性是可选的,大多数APP都不需要这个属性。在没有这个属性的时候,Android会启动一个Application类的实例。

%title插图%num
4. 在activity或fragment中使用leak canary举例
public class SearchActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LeakThread leakThread = new LeakThread();
leakThread.start();
}
class LeakThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(6 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = LeakApplication.getRefWatcher(this);//1
refWatcher.watch(this);
}
}

SearchActivity存在内存泄漏,原因就是非静态内部类LeakThread持有外部类SearchActivity的引用,LeakThread中做了耗时操作,导致SearchActivity无法被释放。
在注释1处得到RefWatcher,并调用它的watch方法,watch方法的参数就是要监控的对象。当然,在这个例子中onDestroy方法是多余的,因为LeakCanary在调用install方法时会启动一个ActivityRefWatcher类,它用于自动监控Activity执行onDestroy方法之后是否发生内存泄露。这里只是为了方便举例,如果想要监控Fragment,在Fragment中添加如上的onDestroy方法是有用的。

public abstract class BaseFragment extends Fragment {
@Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = App.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}

运行程序,这时会在界面生成一个名为Leaks的应用图标。接下来不断的切换横竖屏,这时会闪出一个提示框,提示内容为:“Dumping memory, app will freeze.Brrrr.”。再稍等片刻,内存泄漏信息就会通过Notification展示出来,比如荣耀magic的通知栏如下图1所示。
Notification中提示了SearchActivity发生了内存泄漏。点击Notification就可以进入内存泄漏详细页,除此之外也可以通过Leaks应用的列表界面进入,列表界面如下图2所示。内存泄漏详细页如下图3所示。
点击加号就可以查看具体类所在的包名称。整个详情就是一个引用链:SearchActivity的内部类LeakThread引用了LeakThread的this$0,this$0的含义就是内部类自动保留的一个指向所在外部类的引用,而这个外部类就是详情*后一行所给出的SearchActivity的实例,这将会导致SearchActivity无法被GC(garbage collection,垃圾回收),从而产生内存泄漏。
图1:%title插图%num

图2:%title插图%num

图3:%title插图%num

解决该内存泄露的方法就是将LeakThread改为静态内部类。再次运行程序LeakThread就不会给出内存泄漏的提示了。


static class LeakThread extends Thread {

}

四、LeakCanary2使用
1. 和 LeakCanary1 相比,LeakCanary2 有以下改动
完全使用 Kotlin 重写。
使用新的Heap分析工具Shark,替换到之前的haha,按官方的说法,内存占用减少了10倍。
泄露类型分组。
2. LeakCanary2集成
只需要增加以下依赖即可:

debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.2’
1
LeakCanary2 实现了自动调用 install() 方法,实现方式是使用的 ContentProvider,相关代码位于 leakcanary-object-watcher-android 模块中的 AppWatcherInstaller.kt 中。
AppWatcherInstaller 继承 ContentProvider,重写了 onCreate() 方法,这里利用的是,注册在 Manifest 文件中的 ContentProvider,会在应用启动时,由 ActivityThread 创建并初始化。
————————————————

二维码识别之Android完整编译Zbar

大概刚做Android开发的时候就做过二维码扫描,那时候懂的东西少,就搜出来了ZXing和Zbar两个库。ZXing是纯Java代码实现的,适用于Android平台;Zbar是C实现的,可以供很多语言和平台使用,比如Java、iOS平台、Android平台,Python等等。很明显Zbar的识别率和速度都是明显快于ZXing的,但是无奈那时候不会编译Zbar,只好下载了ZXing,但是由于当时技术能力不足,对于ZXing自定义剪切框也做不出来,只好下载了别人编译好的Zbar,可能由于别人修改了代码或者编译的不是很完整,后期bug层出,废了好大劲才完善好。

后来一直没有机会学习二维码扫描,直到前几天需要给我们平台的APP加上了二维码扫描功能,我决定使用ZBar,于是我完整的编译了一次,今天把这个过程记录下来,希望可以帮助到需要的同学。

比如微信使用的是ZXing,但是我肯定的说他们修改了不少源码,而且有很多地方应该改成了jni实现,所以微信的识别速率和准确率是相当高的,不过今天我编译后的封装也是秒秒钟就可以识别。

因为Zbar是基于LGPL-2.1开源的,因此我基于LGPL-2.1协议,我把一个完整的项目源码和sample放到Github上了,提供直接调用zbar的识别byte[]数据的功能和调用相机识别二维码的功能:
https://github.com/yanzhenjie/android-zbar-sdk

特别声明:本文已经修复了zbar识别中文乱码的问题!!!

编译Zbar
在正式编译之前要注意:编译Zbar需要先编译libiconv,编译libiconv需要linux环境,需要用到gcc。如果你没有linux环境也没有关系,我已经提供了编译好的libiconv。

其实在Zbar的官网也可以下载到他们已经编译好的so和jar,但是so文件他们只提供了armeabi、armeabi-v7a、x86平台:
https://sourceforge.net/projects/zbar/files/?source=navbar

所以我就抛弃了提供的编译包,自己编译了,下面是步骤。

首先在Zbar的开源主页下载Zbar源码:
https://github.com/ZBar/ZBar

顺便在开源主页点开android文件夹,发现编译Zbar需要libiconv,接下来下载libiconv:
http://www.gnu.org/software/libiconv

对于libiconv我是下载的在2017-02-02时发布的*新版1.15。

一、编译libiconv
如果你没有linux环境编译libiconv,那么你可以在这里下载我已经编译好的libiconv1.15:
http://download.csdn.net/detail/yanzhenjie1003/9833225,下好好文件后,你就可以直接跳过这一节,看下面Zbar和libiconv一起编译了。

如果你有linux环境可以编译libiconv,那么继续往下看。
下载好libiconv后,进入libiconv文件夹,如果报权限错误进不去的话执行sudo chmod 777 -R libiconv就可以了:

%title插图%num
进来后先执行:./configure,如果提示没权限那么执行:sudo chmod 777 configure,然后重新执行/.configure即可。

等./configure执行完后再执行make命令即可完成编译

编译时可能遇到以下错误:
1、configure: error: no acceptable C compiler found in $PATH
这个是说你没有安装gcc,安装gcc后再次执行未完成命令即可。

二、Zbar和libiconv一起编译
libiconv编译完成了,接下来把Zbar和libiconv放到一起,编译出我们需要的so文件。

把刚才编译好的libiconv放入我们项目的jni文件夹。
解压刚才下载好的Zbar,首先把Zbar的头文件所在文件夹zbar/include放入我们项目的jni文件夹下。
把Zbar对java的接口文件zbarjni.c放入我们项目的jni文件夹,zbrjni.c在zbar/java文件夹下。
把Zbar的核心库文件所在的文件夹zbar/zbar放到我们项目的jni文件夹下。
把Zbar编译时需要的Android.mk、Applicaiton.mk、config.h从zbar\android\jni下拷贝到我们项目的jni文件夹下。
此时我们项目的jni文件夹是这样的:

%title插图%num
理论上现在可以开始编译了吧,但是呢因为我们改动了zbar的文件夹结构,所以我们要对Android.mk进行改动,主要改的是文件夹路径和文件路径,修改后的Android.mk的内容如下:

MY_LOCAL_PATH := $(call my-dir)

# libiconv
include $(CLEAR_VARS)
LOCAL_PATH := $(MY_LOCAL_PATH)
LOCAL_MODULE := libiconv
LOCAL_CFLAGS := \
-Wno-multichar \
-D_ANDROID \
-DLIBDIR=”c” \
-DBUILDING_LIBICONV \
-DBUILDING_LIBCHARSET \
-DIN_LIBRARY

LOCAL_SRC_FILES := \
libiconv-1.15/lib/iconv.c \
libiconv-1.15/libcharset/lib/localcharset.c \
libiconv-1.15/lib/relocatable.c

LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/libiconv-1.15/include \
$(LOCAL_PATH)/libiconv-1.15/libcharset \
$(LOCAL_PATH)/libiconv-1.15/libcharset/include

include $(BUILD_SHARED_LIBRARY)

LOCAL_LDLIBS := -llog -lcharset

# —————————————————–

# libzbar
include $(CLEAR_VARS)
LOCAL_PATH := $(MY_LOCAL_PATH)
LOCAL_MODULE := zbar
LOCAL_SRC_FILES := \
zbarjni.c \
zbar/img_scanner.c \
zbar/decoder.c \
zbar/image.c \
zbar/symbol.c \
zbar/convert.c \
zbar/config.c \
zbar/scanner.c \
zbar/error.c \
zbar/refcnt.c \
zbar/video.c \
zbar/video/null.c \
zbar/decoder/code128.c \
zbar/decoder/code39.c \
zbar/decoder/code93.c \
zbar/decoder/codabar.c \
zbar/decoder/databar.c \
zbar/decoder/ean.c \
zbar/decoder/i25.c \
zbar/decoder/qr_finder.c \
zbar/qrcode/bch15_5.c \
zbar/qrcode/binarize.c \
zbar/qrcode/isaac.c \
zbar/qrcode/qrdec.c \
zbar/qrcode/qrdectxt.c \
zbar/qrcode/rs.c \
zbar/qrcode/util.c

LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include \
$(LOCAL_PATH)/zbar \
$(LOCAL_PATH)/libiconv-1.15/include

LOCAL_SHARED_LIBRARIES := libiconv

include $(BUILD_SHARED_LIBRARY)

 

然后在Application.mk中填写你要编译的平台,如果想全部编译:

APP_ABI := all
1
如果要指定编译某几个平台,把平台名称依次空格隔开写上即可:

APP_ABI := armeabi armeabi-v7a x86 x86_64 mips mips_64 arm64_v8a
1
此时我们用命令行进入项目的jni文件夹的父母路,比如一般jni情况下jni文件夹位于ProjectName/ModuleName/src/main/jni,那么我们就进入这个main,然后此时执行ndk-build进行编译。

如果提示没有ndk-build这个命令,那么你需要从http://developer.android.com下载ndk并且在电脑上配置PATH。

等ndk-build执行完后会在libs下生成所有平台的so文件夹,文件夹里面是需要的libiconv和zbar的so文件。

编译Zbar和libiconv时遇到的错误解决
编译过程中可能发现如下错误,请按照修改方案修改即可。

1、libiconv-1.15/jni/libcharset/lib/localcharset.c:51:24: error: langinfo.h: No such file or directory
打开libiconv-1.15/libcharset/config.h文件,搜索#define HAVE_LANGINFO_CODESET,大概在14行,把这行注释了即可:

/* #define HAVE_LANGINFO_CODESET 1 */
1
2、…c undeclaired…
打开libiconv-1.15/libcharset/lib/localcharset.c,搜索到函数get_charset_aliases(),大概在124行。

大概在195行左右,有一个int c;(没有的话你可以搜索int c;),把这个一行代码移动到get_charset_aliases()开头:

%title插图%num
zbar的jar包
现在so文件有了,剩下的就是怎么调用so中的函数来识别条码/二维码了,首先把zbar/java下在net.sourceforge.zbar包和里边的java文件拷贝到你的项目的java目录下,大概结构如下:

%title插图%num
当然你也像这样使用源码,也可以把这几个类打包成jar包。

调用Zbar识别二维码
现在全部都编译好了,jar文件也有了,我们可以调用jar中封装的方法来识别二维码了:

byte[] imageData = …;

Image barcode = new Image(size.width, size.height, “Y800”);
barcode.setData(imageData);
// 指定二维码在图片中的区域,也可以不指定,识别全图。
// barcode.setCrop(startX, startY, width, height);

String qrCodeString = null;

int result = mImageScanner.scanImage(barcode);
if (result != 0) {
SymbolSet symSet = mImageScanner.getResults();
for (Symbol sym : symSet)
qrCodeString = sym.getData();
}

if (!TextUtils.isEmpty(qrCodeString)) {
// 成功识别二维码,qrCodeString就是数据。
}

如何和相机结合使用等复杂操作这里不再说了,一个完整的项目我放到Github上了:
https://github.com/yanzhenjie/android-zbar-sdk

山高水远,江湖再见!

 

Android开发趋势及必备技术点!

一、关于Android的前景
不断地也听见很多人在谈做Android是否还有前途、Android研发在走下坡路了、Android的工作太难找了,对于这些其实我的看法很简单,现在真的还没到说Android开发已经无路可走的地步,当然未来怎样我无法预判。现在各大公司其实都很缺Android研发(中高级),不断的在招人,就拿很多一线互联网来说,别说来面试的人了,就简历都拿不到太多,需要花很多时间去找简历。所以,对于有Android开发经验的同学,更多应该想想怎么往深探索,而不是一味想着换方向,不管换到哪个方向,都会面对从初级到高级到资深再到专家的时间点。所以我认为正确的职业规划应该是金字塔形,核心竞争力一定要扎实!

二、知识点详细清单
对于现在的Android及移动互联网来说,我们需要掌握的技术,我做了一个清单:

泛型原理丶反射原理丶Java虚拟机原理丶线程池原理丶
注解原理丶注解原理丶序列化
Activity知识体系(Activity的生命周期丶Activity的任务栈丶Activity的启动模式丶View源码丶Fragment内核相关丶service原理等)
代码框架结构优化(数据结构丶排序算法丶设计模式)
APP性能优化(用户体验优化丶适配丶代码调优)
热修复丶热升级丶Hook技术丶IOC架构设计
NDK(c编程丶C++丶JNI丶LINUX)
如何提高开发效率?
MVC丶MVP丶MVVM
微信小程序
Hybrid
Flutter

 

三丶解析知识点,为什么要学
1.数据结构和算法

数据结构和算法其实是分开的东西,我们需要先掌握各种数据结构,再去加深算法,数据结构和算法其实也属于基础,但是它现在越来越重要,所以我就单独拿出来说了。数据结构怎么深入同样我也推荐大家去看清华或者浙大《数据结构》公开课,特别是清华的,值得反复研究。至于算法,首先要做的就是动手,LeetCode上直接干!第二阶段就是要总结各种算法的思想和套路,像递归、动态规划等这些算法都是有套路的,在LeetCode上也有按数据结构和算法分类的筛选,大家可以针对性练习和总结。当然,对于一个Android程序员,能做到每天在LeetCode上刷题就非常不错了,所以一定要坚持,等你坚持到一定的时间,你会发现你越来越游刃有余,我从15年底开始在LeetCode上刷题,目前已经刷了200多道了,小米的面试也非常注重算法,还要能写。而且对一些特别注重算法的公司,算法这块的考核非常严苛,对,就是严苛,而不是严格。

2.设计模式

设计模式中包括了设计原则,其实对于Android开发人员来说,设计模式就那23种,知道并了解这些设计模式是*个阶段,仅仅是到这个阶段是不够的,一般面试也不会问你某个设计模式的概念,而会让你具体的说说你对某一种设计模式的深入了解和使用,它的优缺点,所以,第二阶段就是要运用它们,其次要和Android源码中运用到设计模式地方进行结合学习。例如建造者模式,Andoird中的Dialog创建就使用到了,还有像单例模式、适配器模式、观察者模式等等都是在Android中非常常用的设计模式,也是在面试中出现频率很高的。

3.语言学习开发语言

Android应用是由Java语音进行开发的,SDK也是由Java语言编写的,所以我们要学习Java语言。另外,虽然说Kotlin语言也得到Android官方的热推,但是Kotlin也是编译成了Java语言在运行的。对于Android开发来说。只要SDK没有用Kotlin重写,那么Java语言都是需要学习的。而且Android apk的后台服务器程序大概率是Java语言构建,所以学习Java是一个必然。那么Java中那些东西是我们Android中比较相关的稍微比较难的Java基础几乎是一个门槛,像泛型丶多线程丶反射丶JVM丶JavaIO丶注解丶序列化等等

4.APP开发框架知识

这块知识是现今使用者*多的,我们称之为Android2013-2016nian 的技术。但是,即使是这样的技术,很多开发者也往往因为网上很多copy代码的习惯而导致对这块的使用的代码熟悉而陌,熟悉的是天天和它们打交道天天在复制,陌生的是天天打交道却没有深入研究过他们,要学习源码,模仿源码,然后在hook源码,这样才能说懂这块的知识。

5.App性能优化

一个app的西能好不好我们需要从两个层面分析:

①从写代码的时候就注意,让自己的代码是高性能高可用的代码,这个过程是书写高性能代码
②对已经成型的代码通过工具检测代码问题,通过检查到问题来指导我们进行代码的删改这个过程被称为调优

这里提供一份性能优化方面的学习思路给大家:

那如何写出高性能的代码呢?

需要我们具备深厚的代码功底,这就是代码的基础,如:数据结构达到可以根据应用场景写出符合当前场景的特殊结构,比如google针对Android平台特征研发了SparseArray代替HashMap.另外,对常用的算法也有自己独到的见解。

6.NDK模块开发

音视频丶高清大图片丶人工智能丶抖音直播等这些年与用户紧密相关,与我们生活*相关的技术一直都在寻找*终的技术落地平台,以前是Windows系统,而现在是移动系统了。而移动系统中Android比例又是*大的。所以NDK可以说是必备要学习的,除此之外,音视频的编解码技术流媒体协议,ffmepeg,c,c++,JNI,linux
都是音视频开发必备技能。而且OpenCV \OpenGI这些又是图像处理必备

关于NDK模块开发的学习思路:

7.如何提高开发效率?

工欲善其事必先利其器,如何提高开发效率,很多开发者在开发中由于gradle不会用导致加载代码非常耗时,这些都是你的工作成本的浪费。还有就是git的使用也可以帮助我们管理好我们的代码,这个非常关键,因为这个工具可以让我们修改的代码不会因为错误操作而导致丢失。另外,对移动开发者我们至少需要知道如何抓取网络包。其中,*常用的stetho就是一个非常好用的可以抓取网络包的工具

8.混合开发

混合开发的flutter现在已经逐渐成了主流的混合开发框架,另外由于阿里系的强大存在,导致阿里系的公司都在用Weex混合架构,这些都是一个Android工程师开拓视野,走向未来必不可少的基本技能的。

Flutter学习思维导图:

四、写在*后
不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言,一定会认真查询,修正不足,谢谢。

Android开发6年了,三级缓存框架问题你都了解了吗?

前言
不知道大家面试的时候,有没有遇到这种情况,面试工资谈的是10K,*后干着40K的活!说着冠冕堂*,提升大家能力的话,做着死命压榨员工,996成了程序员心里的魔咒!

初级安卓开发工程师(10K-15K)

掌握扎实的java基础,了解各种设计模式,熟练掌握Android UI控件、Android Java层API的相关使用。往中级层次发展时,继续深入学习java编程技术,掌握更多Android开发需要的库,还要精通ml、json解析,会做socket c/s端的程序。

中级安卓开发工程师(15K-30K)

除了上面提到的必备技能外,中级开发人员需要延伸到精通NDK JNI方式的开发,熟悉Android framework,会移植类似Fmpeg、Mplayer这样的开源项目,并在Android上运行。

高级安卓开发工程师(50K-100K)

作为高级app开发工程师,你只需了解,并不需要亲自操作初级和中级工程师的工作流程,但是,你需要有一个整体知识构架,安卓开发各方面所涉及的知识你都有要了解,并有自己独到的见解。需要熟练掌握Linux驱动开发,并对Linux内核结构很精通,掌握Android移植,包括硬件移植。

该如何提前应对这个问题
经历过的人都明白,想象得到那种场景的人都能体会,那*不是我们想要的结果。

那么,我们要如何才能避免这种情况,化解即将到来的危机呢?

*根本的点,是摆脱工作环境的惯性,不断提升自己创造溢价的能力。

具体来讲,有4大策略:
1.做好一件事,成为某个领域的*
2.跟进行业动态
3.拓展自己的能力边界
4.定期去面试

做好一件事,成为某个领域的*

做好一件事,成为某个领域的*,这是非常棒的策略,可以让你有“成为高手”的体验和经验,也可以让人看得见你,给你更多机会,甚至会让同行看到你,来高薪挖你。

你可能会说,*只有一个,公司那么多人,好难做到。实际上,公司并非只有一个*,而是可能有很多个方向的*,有人是营销文案*,有人是设计模式*,有人是业务分析*,有人是在线排障*。

你要做的,是找到一个能匹配你的细分方向,成为*,让大家一提到某个东西,就想到你。反过来,大家一提到你,就会说你某某方面很厉害。这样你就有了标签,就更容易被需要,也更容易有影响力。

跟进行业动态
我们一旦进了一家公司,就会觉得安全了,可以放心让公司带着我们前进了,我们就会放松对自己的要求,就很容易闭目塞听,觉得什么产品啊市场啊技术啊趋势啊竞品啊,公司有专门的人负责,自己只要做好自己份内的那点事情就好了。

而这必然导致自我隔离和信息茧房,使得我们慢慢不知道外面的世界发生了什么,还觉得自己跟得上公司的步调,蛮不错的。这样一来,将来进入开放市场时,就会遭遇困境。所以,跟进行业动态,是我们必须要做的事情。公司在哪个行业,我们所用的技能属于哪个生态,这些地方,发生了什么重要事情,有哪些新奇的东西出来,旧的东西有哪些演变,我们都需要关注。

跟进行业动态,我们才不会和市场大环境脱节,才能保持我们的“手感”,帮助为我们成为一个有准备的人。具体到执行层面,有一些常见的方法,比如订阅新闻,比如参加业界交流会议,比如参加一些社群等等,根据自己的需要,选择适合自己的方式就好。

拓展自己的能力边界
为成长焦虑的程序员,经常跟我提到这个问题:公司的项目都是老技术,领导稳定*,不让用新技术,我该怎么提升自己?实际上,能力有千百个方向,不只某个新技术;锻炼的方式也有千百种,不只是通过公司的项目。

*重要的,是你要做一个规划,看自己要提升哪种能力,然后围绕着它,来制定行动计划。比如K所在的公司,领导为了稳妥,坚持使用旧的技术架构,不让尝试SSM等稍微新点的技术,那K如果通过第2种策略了解市面上的主流技术,其实可以自己去学习去实践啊,可以参考公司项目,设计一个DEMO项目,来用新技术。比如你想做项目管理,目前公司没有新的项目给你负责,那你怎么办?是不是可以自己发起一个兴趣类项目?

是不是可以主动承接年会项目?我们要养成挑战自己的习惯,而不是随随便便给自己找个“领导不允许”、“环境不支持”之类的理由,合理化自己躺在舒适圈的欲望。**只有不断挑战自己,拓展能力边界,我们才能更有议价能力。

定期去面试
我们待在一个固定的环境里,稍一松懈,就容易只和身边的人比较,觉得我比同组小王的技术实力强,我比部门老刘有创意,认为自己鹤立鸡群,还蛮不错的。实际上,一家公司很难活过10年,我们很难在一个小环境里工作一辈子,我们终将进入开放市场,和更多的人竞争更好的工作机会。

既然如此,那我们就要了解自己在整个市场上的位置,通过和市场的比较来发现自己的价值和不足,以便对自己有一个清醒的认识。一个推荐的方法,就是面试。每一个能够担任面试官的人,都是公司里在某方面比较出色的人,都具备自己独特的视角,他们带着选拔优秀者的心态来审视你,往往可以给你带来珍贵的反馈。

所以,每半年悄悄的找几家有实力的公司去面试,感受下不同水平面试官对你的评价,这能帮助你认清自己当下的状况。一旦你发现自己掉队了,跟不上市场的要求了,你就可以运用第3种策略,逼迫自己跟上来。

必须不停地奔跑,才能留在原地
《爱丽丝梦游仙境》中红桃*后说过的一句话:“在我们这个地方,你必须不停地奔跑,才能留在原地。”不管你是否乐意接受,我们现在所处的地方,就是这句话里的地方。不想被时代抛弃,那就拼命奔跑吧。

面试复习笔记
将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找*优的解答方案。每一道面试题都是百分百的大厂面经真题+*优解答。包知识脉络 + 诸多细节。

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正*能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

及实战技术的提升都是不利的。

真正*能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

Android开发中登录注册界面的框架实现

小项目框架
今天用QQ的时候想到了,不如用android studio 做一个类似于这样的登录软件。当然QQ的实现的功能特别复杂,UI界面也很多,不是单纯的一时新奇就可以做出来的。就是简单的实现了一些功能,做了三个界面;1.登录界面。2.注册界面。3.登陆后的界面。

功能描述
登录按钮——按钮实现跳转到下一个界面,并且判断输入的账号、密码是否符合规则(不为空),提示,登陆成功或失败
注册按钮——按钮实现跳转到注册界面

登录界面

%title插图%num

main_activity.xml

<LinearLayout
android:id=”@+id/number”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_below=”@+id/iv”
android:layout_centerVertical=”true”
android:layout_marginBottom=”5dp”
android:layout_marginLeft=”10dp”
android:layout_marginRight=”10dp”
android:layout_marginTop=”15dp”
android:background=”#ffffff”>
<TextView
android:id=”@+id/tv_number”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:padding=”10dp”
android:text=”账号”
android:textColor=”#000″
android:textSize=”20dp” />
<EditText
android:id=”@+id/et_username”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_marginLeft=”5dp”
android:background=”@null”
android:padding=”10dp” />
</LinearLayout>
<LinearLayout
android:id=”@+id/password”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_below=”@+id/number”
android:layout_centerVertical=”true”
android:layout_marginLeft=”10dp”
android:layout_marginRight=”10dp”
android:background=”#ffffff”>
<TextView
android:id=”@+id/tv_password”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:padding=”10dp”
android:text=”密码”
android:textSize=”20dp”
android:textColor=”#000″ />
<EditText
android:id=”@+id/et_password”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_marginLeft=”5dp”
android:layout_toRightOf=”@id/tv_password”
android:background=”@null”
android:inputType=”textPassword”
android:padding=”10dp” />
</LinearLayout>
<Button
android:id=”@+id/button_login”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_below=”@id/password”
android:layout_marginLeft=”10dp”
android:layout_marginRight=”10dp”
android:layout_marginTop=”60dp”
android:background=”#3c8dc4″
android:text=”登录”
android:textColor=”#ffffff”
android:textSize=”20dp” />
<Button
android:id=”@+id/button_register”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_below=”@id/button_login”
android:layout_marginLeft=”10dp”
android:layout_marginRight=”10dp”
android:layout_marginTop=”30dp”
android:background=”#b7585556″
android:text=”注册”
android:textColor=”#ffffff”
android:textSize=”20dp” />

<CheckBox
android:checked=”true”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”记住密码”
android:id=”@+id/checkBox”
android:layout_below=”@+id/password”
android:layout_marginLeft=”10dp”
android:layout_marginTop=”5dp”/>

注册界面
确定注册——按钮实现注册,判断以上四个注册信息是否符合规则,判断两次输入密码是否一样,并且不为空。并且显示提示信息
返回登录——按钮实现跳转到刚才的登录界面

%title插图%num

main_activity.java

public class MainActivity extends AppCompatActivity {
private EditText et_username;
private EditText et_password;
private EditText et_password2;
private EditText et_mail;
private Button btn_login;
private Button btn_register;
private CheckBox checkbox;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Map<String, String> userInfo = SaveInfo.getSaveInformation(this);
if (userInfo != null) {
et_username.setText(userInfo.get(“username”));
et_password.setText(userInfo.get(“password”));
}
et_username =(EditText) findViewById(R.id.et_username);
et_password =(EditText) findViewById(R.id.et_password);
et_password2 =(EditText) findViewById(R.id.reg_password2);
et_mail = (EditText) findViewById(R.id.reg_mail);
checkbox = (CheckBox) findViewById(R.id.checkBox);
btn_login =(Button) findViewById(R.id.button_login);
btn_register =(Button) findViewById(R.id.button_register);
btn_login.setOnClickListener(new MyButton());
btn_register.setOnClickListener(new MyButton());
}
public class MyButton implements View.OnClickListener{
@Override
public void onClick(View view){
String username =et_username.getText().toString().trim();
String password =et_password.getText().toString().trim();
switch (view.getId()) {
//当点击登录按钮时
case R.id.button_login:
if(TextUtils.isEmpty(username) || TextUtils.isEmpty(password)){
Toast.makeText(MainActivity.this,”密码或账号不能为空”,Toast.LENGTH_SHORT).show();
} else {
if(checkbox.isChecked()){
//保存密码的操作
}
Toast.makeText(MainActivity.this,”登录成功”,Toast.LENGTH_SHORT).show();
Intent intent = new Intent(MainActivity.this, LoginActivity.class);
startActivity(intent);
}
break;
//当点击注册按钮事件时
case R.id.button_register:
Intent intent = new Intent(MainActivity.this,RegisterActivity.class);
startActivity(intent);
break;

}
}
}
}

register_activity

<TextView
android:layout_marginTop=”60dp”
android:id=”@+id/reg_number1″
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:padding=”10dp”
android:text=”账号:”
android:textColor=”#000″
android:textSize=”20dp” />
<EditText
android:layout_alignBottom=”@+id/reg_number1″
android:layout_toRightOf=”@+id/reg_number1″
android:id=”@+id/reg_username”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:padding=”10dp” />
<TextView
android:id=”@+id/reg_number2″
android:layout_marginTop=”5dp”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/reg_number1″
android:padding=”10dp”
android:text=”密码:”
android:textColor=”#000″
android:textSize=”20dp” />
<EditText
android:layout_alignBottom=”@id/reg_number2″
android:layout_toRightOf=”@+id/reg_number2″
android:id=”@+id/reg_password”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:padding=”10dp” />
<TextView
android:id=”@+id/reg_number3″
android:layout_marginTop=”5dp”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/reg_number2″
android:padding=”10dp”
android:text=”密码:”
android:textColor=”#000″
android:textSize=”20dp” />
<EditText
android:layout_alignBottom=”@id/reg_number3″
android:layout_toRightOf=”@+id/reg_number3″
android:id=”@+id/reg_password2″
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:padding=”10dp” />
<TextView
android:id=”@+id/reg_number4″
android:layout_marginTop=”5dp”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/reg_number3″
android:padding=”10dp”
android:text=”邮箱:”
android:textColor=”#000″
android:textSize=”20dp” />
<EditText
android:layout_alignBottom=”@id/reg_number4″
android:layout_toRightOf=”@+id/reg_number4″
android:id=”@+id/reg_mail”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:padding=”10dp” />

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”确定注册”
android:background=”#74e674″
android:id=”@+id/reg_btn_sure”
android:layout_marginTop=”38dp”
android:layout_below=”@+id/reg_mail”
android:layout_marginLeft=”50dp” />

<Button
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”返回登录”
android:background=”#f27758″
android:id=”@+id/reg_btn_login”
android:layout_alignBottom=”@id/reg_btn_sure”
android:layout_toRightOf=”@id/reg_btn_sure”
android:layout_marginLeft=”60dp”
/>
<TextView
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”账号注册”
android:textSize=”30dp”
android:layout_marginTop=”5dp”
android:layout_alignParentLeft=”true”
android:layout_alignParentStart=”true” />
</RelativeLayout>

registeractivity.java

public class RegisterActivity extends AppCompatActivity {
private EditText reg_username;
private EditText reg_password;
private EditText reg_password2;
private EditText reg_mail;
private Button reg_btn_sure;
private Button reg_btn_login;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_register);
reg_username = (EditText) findViewById(R.id.reg_username);
reg_password = (EditText) findViewById(R.id.reg_password);
reg_password2 = (EditText) findViewById(R.id.reg_password2);
reg_mail = (EditText) findViewById(R.id.reg_mail);
reg_btn_sure = (Button) findViewById(R.id.reg_btn_sure);
reg_btn_login = (Button) findViewById(R.id.reg_btn_login);
reg_btn_sure.setOnClickListener(new RegisterButton());
reg_btn_login.setOnClickListener(new RegisterButton());
}

public class RegisterButton implements View.OnClickListener {
@Override
public void onClick(View v) {
String username = reg_username.getText().toString().trim();
String password = reg_password.getText().toString().trim();
String password2 = reg_password2.getText().toString().trim();
String mail = reg_mail.getText().toString().trim();
switch (v.getId()) {
//注册开始,判断注册条件
case R.id.reg_btn_sure:
if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password) || TextUtils.isEmpty(password2) || TextUtils.isEmpty(mail)) {
Toast.makeText(RegisterActivity.this, “各项均不能为空”, Toast.LENGTH_SHORT).show();
} else {
if (TextUtils.equals(password, password2)) {
//执行注册操作
SaveInfo.SaveInformation(RegisterActivity.this,username,password,password2,mail);
Toast.makeText(RegisterActivity.this,”注册成功,请返回登录”,Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(RegisterActivity.this, “两次输入的密码不一样”, Toast.LENGTH_SHORT).show();
}
}
break;
case R.id.reg_btn_login:
Intent intent = new Intent(RegisterActivity.this, MainActivity.class);
startActivity(intent);
break;

}
}
}
}

登录成功界面创建一个布局文件就可以了,写上你想要的东西,我自己就是创建了一个布局,什么都没有,所以就在这里不写了
在这里因为要做一个保存操作,所以创建了一个java工具类,其中定义了两个方法,一个保存登录名和密码,一个负责调用保存的登录名和密码
saveinfo

public class SaveInfo {
public static boolean SaveInformation(Context context, String username, String password, String password2, String mail) {
try {
FileOutputStream fos = context.openFileOutput(“data.txt”, Context.MODE_APPEND);
fos.write((“用户名:” + username + ” 密码:” + password + “邮箱:” + mail).getBytes());
fos.close();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

public static Map<String, String> getSaveInformation(Context context) {
try {
FileInputStream fis = context.openFileInput(“data.txt”);
BufferedReader br = new BufferedReader(new InputStreamReader(fis));
String str = br.readLine();
String[] infos = str.split(“用户名:”+”密码:”+”邮箱:”);
Map<String, String> map = new HashMap<String, String>();
map.put(“username”, infos[0]);
map.put(“password”, infos[1]);
fis.close();
return map;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

示例图片

 

%title插图%num %title插图%num %title插图%num %title插图%num %title插图%num

Android Studio:使用Camera拍照(一)调用系统相机

写在前面的话:每一个实例的代码都会附上相应的代码片或者图片,保证代码完整展示。*重要的是保证例程的完整性!!!方便自己也方便他人~欢迎大家交流讨论~

(注:每处代码都会附上对应的代码片or图片)
1.新建一个Android项目,取名为startcamera。
java主文件(startcamera.java)
layout文件(activity_startcamerae.xml)

2.编辑activity_startcamerae.xml

<?xml version=”1.0″ encoding=”utf-8″?>
<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”
tools:context=”.startcamera”>

<Button
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:onClick=”startCamera”
android:text=”camera”/>
</LinearLayout>

%title插图%num

%title插图%num
3.编辑startcamera.java

package com.example.administrator.startcamere;

import android.app.Activity;
import android.content.Intent;
import android.provider.MediaStore;
import android.view.View;
import android.widget.ImageView;

public class startcamera extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_startcamera);}
public void startCamera(View view){
Intent intent=new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivity(intent);
}
}

4.运行
运行模拟器or用USB连接真机开启USB调试模式即可看到执行效果

Android开发实践八:点击一个按钮退出整个程序

# 参考资源 #

https://blog.csdn.net/sinat_33921105/article/details/57096554

main.java添加如下代码:

public static List<Activity> activityList = new LinkedList();

public void exit()
{

for(Activity act:activityList)
{
act.finish();
}

System.exit(0);

}
在main.java调用的按钮里添加如下代码:

btn4.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
exit();

}
});
在每个activity的oncreat里面写上如下这么一句代码,记得要写在 setContentView 之后:

Main.activityList.add(this);

 

Android开发技巧-Camera拍照功能

本篇是我对开发项目的拍照功能过程中,对Camera拍照使用的总结。由于camera2是在api level 21(5.0.1)才引入的,而Camera到6.0仍可使用,所以暂未考虑camera2。

文档中的Camera
要使用Camera,首先我们先看一下文档(http://androiddoc.qiniudn.com/reference/android/hardware/Camera.html)中是怎么介绍的。相对于其他*大多数类,文档对Camera的介绍还是比较详尽的,包含了使用过程中所需要的步骤说明,当然,这也表明了它在实际使用中的繁琐。
首先,需要在AndroidManifest.xml中声明以下权限和特性:

<uses-permission android:name=”android.permission.CAMERA” />
<uses-feature android:name=”android.hardware.camera” />
<uses-feature android:name=”android.hardware.camera.autofocus” />

然后,拍照的话,需要以下十步:
1. 通过open(int)方法得到一个实例
2. 通过getParameters()方法得到默认的设置
3. 如果有必要,修改上面所返回的Camera.Parameters对象,并调用setParameters(Camera.Parameters) 进行设置
4. 如果有需要,调用setDisplayOrientation(int)设置显示的方向
5. 这一步很重要,通过setPreviewDisplay(SurfaceHolder)传入一个已经初始化了的SurfaceHolder,否则无法进行预览。
6. 这一步也很重要,通过startPreview()开始更新你的预览界面,在你拍照之前,它必须开始。
7. 调用takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)进行拍照,等待它的回调
8. 拍照之后,预览的展示会停止。如果想继续拍照,需要先再调用startPreview()。
9. 调用stopPreview()停止预览。
10. 非常重要,调用release()释放Camera,以使其他应用也能够使用相机。你的应用应该在onPause()被调用时就进行释放,在onResume()时再重新open()。

上面就是文档中关于使用Camera进行拍照的介绍了。接下来说一下我的使用场景。

我的使用场景

%title插图%num

这是项目的界面需求。下面一个圆的拍照按钮,然后是一个取消按钮,上面是预览界面(SurfaceView)加个取景框。再上面就是一块黑的了。点拍照,拍照之后,跳到一个裁剪图片的界面,所以不会有连续拍多次照片的场景。
取景框什么的这里略过不谈,布局文件也相对比较简单,下面直接看Java代码里对Camera的使用。

实际使用及填坑
SurfaceHolder的回调
我在Activity中实现SurfaceHolder.Callback接口。然后在onCreate(Bundle)方法中,添加SurfaceHolder的回调。

SurfaceHolder holder = mSurfaceView.getHolder();
holder.addCallback(this);

它的回调方法有3个,分别是surface被创建时的回调surfaceCreated(SurfaceHolder),surface被销毁时的回调surfaceDestroyed(SurfaceHolder)以及surface改变时的回调surfaceChanged(SurfaceHolder holder, int, int, int)。这里我们只关注创建和销毁时的回调,定义一个变量用于标志它的状态。

private boolean mIsSurfaceReady;

@Override
public void surfaceCreated(SurfaceHolder holder) {
mIsSurfaceReady = true;
startPreview();
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mIsSurfaceReady = false;
}

其中的startPreview()方法将在下面讲到。

打开相机
然后是打开相机。这些代码在我定义的openCamera方法中。

if (mCamera == null) {
try {
mCamera = Camera.open();
} catch (RuntimeException e) {
if (“Fail to connect to camera service”.equals(e.getMessage())) {
//提示无法打开相机,请检查是否已经开启权限
} else if (“Camera initialization failed”.equals(e.getMessage())) {
//提示相机初始化失败,无法打开
} else {
//提示相机发生未知错误,无法打开
}
finish();
return;
}
}

打开相机失败的话,我们无法进行下一步操作,所以在提示之后会直接把界面关掉。

拍照参数
final Camera.Parameters cameraParams = mCamera.getParameters();
cameraParams.setPictureFormat(ImageFormat.JPEG);
cameraParams.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);

分别设置图片格式,以及对焦模式。然后因为我这里是竖屏拍照,所以还需要对Camera旋转90度。

cameraParams.setRotation(90);

注意:涉及到旋转的有两个方法,一个是旋转相机,一个是旋转预览。这里设置的是对相机的旋转。
继续注意:由于机型兼容的问题,这里设置旋转之后,有些手机照片来的照片就是竖的了,但是有些手机(比如万恶的三星)拍出来的照片还是横的,但是它们在照片的Exif信息中有相关的角度属性。所以对于拍出来的照片还是横着的,我们在裁剪时再继续处理。关于照片的旋转处理,后续博客中会讲到。

尺寸参数
这里还是Camera的参数设置,但是我把它单独抽出来是因为,它不像上面设置的参数那样简单直接,而需要进行计算。下面是我们需要注意的问题:

首先,相机的宽高比例主要有两种,一种是16:9,一种是4:3。
其次,我们需要SurfaceView的比例与Camera预览尺寸的比例一样,才不会导致预览出来的结果是变形的。
由于机型分辨率的问题,再加上我们的SurfaceView不是满屏的(即使满屏,还要考虑一些虚拟导航栏和各种奇葩分辨率的机型),16:9的比例我们需要上是不会用到的了,我们会让Camera预览的尺寸比例与SurfaceView的大小比例一样。
要特别注意,一些手机,如果设置预览的大小与设置的图片大小相差太大(但宽高比例相同)的话,拍出来的照片可能范围也不一样。比如你拍的时候明明是一幅画包括画框,保存的图片却只有画框里的内容。
下面的代码还是写在我们的openCamera()方法中。由于我们需要能够获取到SurfaceView的大小,所以openCamera()是这样调用的:

@Override
protected void onResume() {
super.onResume();
mSurfaceView.post(new Runnable() {
@Override
public void run() {
openCamera();
}
});
}

它可以保证在openCamera()被调用时surfaceView一定是绘制完成了的。
然后在openCamera()的后续代码中,先获取surfaceView的宽高比例。注意,对于surfaceView我开始在布局上写的是高度占满剩下的空间。

<SurfaceView
android:id=”@+id/surface_view”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:layout_above=”@id/bottom”/>

这时候得到的宽高比就是我们所能接受的*小比例了。

// 短边比长边
final float ratio = (float) mSurfaceView.getWidth() / mSurfaceView.getHeight();

然后获取相机支持的图片尺寸,找出*适合的尺寸。

// 设置pictureSize
List<Camera.Size> pictureSizes = cameraParams.getSupportedPictureSizes();
if (mBestPictureSize == null) {
mBestPictureSize =findBestPictureSize(pictureSizes, cameraParams.getPictureSize(), ratio);
}
cameraParams.setPictureSize(mBestPictureSize.width, mBestPictureSize.height);

findBestPictureSize的代码如下。注意,因为我们是旋转了相机的,所以计算的时候,对surfaceView的比例是宽除以高,而对Camera.Size则是高除以宽。

/**
* 找到短边比长边大于于所接受的*小比例的*大尺寸
*
* @param sizes 支持的尺寸列表
* @param defaultSize 默认大小
* @param minRatio 相机图片短边比长边所接受的*小比例
* @return 返回计算之后的尺寸
*/
private Camera.Size findBestPictureSize(List<Camera.Size> sizes, Camera.Size defaultSize, float minRatio) {
final int MIN_PIXELS = 320 * 480;

sortSizes(sizes);

Iterator<Camera.Size> it = sizes.iterator();
while (it.hasNext()) {
Camera.Size size = it.next();
//移除不满足比例的尺寸
if ((float) size.height / size.width <= minRatio) {
it.remove();
continue;
}
//移除太小的尺寸
if (size.width * size.height < MIN_PIXELS) {
it.remove();
}
}

// 返回符合条件中*大尺寸的一个
if (!sizes.isEmpty()) {
return sizes.get(0);
}
// 没得选,默认吧
return defaultSize;
}

接下来是设置预览图片的尺寸:

// 设置previewSize
List<Camera.Size> previewSizes = cameraParams.getSupportedPreviewSizes();
if (mBestPreviewSize == null) {
mBestPreviewSize = findBestPreviewSize(previewSizes, cameraParams.getPreviewSize(),
mBestPictureSize, ratio);
}
cameraParams.setPreviewSize(mBestPreviewSize.width, mBestPreviewSize.height);

根据图片尺寸,以及SurfaceView的比例来计算preview的尺寸。

/**
* @param sizes
* @param defaultSize
* @param pictureSize 图片的大小
* @param minRatio preview短边比长边所接受的*小比例
* @return
*/
private Camera.Size findBestPreviewSize(List<Camera.Size> sizes, Camera.Size defaultSize,
Camera.Size pictureSize, float minRatio) {
final int pictureWidth = pictureSize.width;
final int pictureHeight = pictureSize.height;
boolean isBestSize = (pictureHeight / (float)pictureWidth) > minRatio;
sortSizes(sizes);

Iterator<Camera.Size> it = sizes.iterator();
while (it.hasNext()) {
Camera.Size size = it.next();
if ((float) size.height / size.width <= minRatio) {
it.remove();
continue;
}

// 找到同样的比例,直接返回
if (isBestSize && size.width * pictureHeight == size.height * pictureWidth) {
return size;
}
}

// 未找到同样的比例的,返回尺寸*大的
if (!sizes.isEmpty()) {
return sizes.get(0);
}

// 没得选,默认吧
return defaultSize;
}

上面的两个findBestxxx方法,可以自己根据业务需要进行调整。整体思路就是先对尺寸排序,然后遍历排除掉不满足条件的尺寸,如果找到比例一样的,则直接返回。如果遍历完了仍没找到,则返回*大的尺寸,如果发现都排除完了,只能返回默认的那一个了。
然后,我们还要再根据previewSize来重新设置我们的surfaceView的大小,以使它们的比例完全一样,才不会导致预览时变形。

ViewGroup.LayoutParams params = mSurfaceView.getLayoutParams();
params.height = mSurfaceView.getWidth() * mBestPreviewSize.width / mBestPreviewSize.height;
mSurfaceView.setLayoutParams(params);

再下来就是把参数设置过去:

mCamera.setParameters(cameraParams);

然后预览。

预览
由于相机打开会需要一些时间,而surfaceHolder的回调也需要一些时间。我希望的是当相机准备完成可以回调并且surface也创建完毕的时候,就可以马上预览(尽量减小进入界面后可能会有的黑一下的时间),所以这里我的代码如下:

if (mIsSurfaceReady) {
startPreview();
}

同时在surface被创建的时候,也会调用一下这个startPreview()方法。
startPreview()代码如下,在camera初始化之后,首先设置SurfaceHolder对象,然后对预览旋转90度,然后开始预览。

private void startPreview() {
if (mCamera == null) {
return;
}
try {
mCamera.setPreviewDisplay(mSurfaceView.getHolder());
mCamera.setDisplayOrientation(90);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
BugReport.report(e);
}
}

自动对焦
我希望在点击预览图的时候能够进行自动对焦。由于在界面上我在surfaceview之上放了一个取景框View,所以我直接对这个View设置一个点击事件,进行触发自动对焦。
自动对焦的代码如下:

/**
* 请求自动对焦
*/
private void requestFocus() {
if (mCamera == null || mWaitForTakePhoto) {
return;
}
mCamera.autoFocus(null);
}

这里我只需要相机能够对焦,并不是要在对焦成功之后才进行拍照,所以回调我传了一个null。
之所以这样使用是因为,之前我写的是对焦成功之后才拍照,但是会有两个问题:一是对焦会有一个过程,这样对完焦之后才拍照会慢,二是可能在点拍照的时候预览的界面正是我们想要的,但是一对焦,可能对焦失败,导致没有拍照或者是拍出来的是模糊的。

拍照
拍照也是异步回调,并且会需要点时间,所以这里我定义了一个mWaitForTakePhoto变量,表示正在拍照,还没完成。在拍照的过程中,不允许重新对焦或重新拍照。

private void takePhoto() {
if (mCamera == null || mWaitForTakePhoto) {
return;
}
mWaitForTakePhoto = true;
mCamera.takePicture(null, null, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
onTakePhoto(data);
mWaitForTakePhoto = false;
}
});
}

保存照片。这里返回的data可以直接写入文件,就是一张jpg图了。

private void onTakePhoto(byte[] data) {
final String tempPath = mOutput + “_”;
FileOutputStream fos = null;
try {
fos = new FileOutputStream(tempPath);
fos.write(data);
fos.flush();
//启动我的裁剪界面
} catch (Exception e) {
BugReport.report(e);
} finally {
IOUtils.close(fos);
}
}

相机的打开与关闭以及Activity的生命周期
@Override
protected void onResume() {
super.onResume();
mSurfaceView.post(new Runnable() {
@Override
public void run() {
openCamera();
}
});
}

@Override
protected void onPause() {
super.onPause();
closeCamera();
}

关闭相机时,首先要取消掉自动对焦,否则如果正好在自动对焦,又关掉相机,会引发异常。接着停止preview,然后再释放:

private void closeCamera() {
if (mCamera == null) {
return;
}
mCamera.cancelAutoFocus();
stopPreview();
mCamera.release();
mCamera = null;
}

总结
1,该类的全部代码见:https://gist.github.com/msdx/f8ca0fabf0092f67d829 。没有Demo项目,没有Demo项目,没有Demo项目。如果你打开不了该链接,请先确认是否能打开github。如果打开不了github,请科学上网
2,文档很重要。
3,我不保证我的代码完全没问题,至少我现在没发现。如果有出现什么问题,欢迎提出。
4,注意相机打开和释放。
5,注意不同机型的相机旋转设置。特别是三星。
6,尺寸计算,previewSize的比例一定要和surfaceView可以显示的比例一样,才不会变形。

Android开发技巧-大图裁剪

本篇内容是接上篇《Android开发技巧-定制仿微信图片裁剪控件》 的,先简单介绍对上篇所封装的裁剪控件的使用,再详细说明如何使用它进行大图裁剪,包括对旋转图片的裁剪。

裁剪控件的简单使用
XML代码
使用如普通控件一样,首先在布局文件里包含该控件:

<com.githang.clipimage.ClipImageView
xmlns:app=”http://schemas.android.com/apk/res-auto”
android:id=”@+id/clip_image_view”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:layout_above=”@+id/bottom”
app:civClipPadding=”@dimen/padding_common”
app:civHeight=”2″
app:civMaskColor=”@color/viewfinder_mask”
app:civWidth=”3″/>

支持的属性如下:

civHeight 高度比例,默认为1
civWidth 宽度比例,默认为1
civTipText 裁剪的提示文字
civTipTextSize 裁剪的提示文字的大小
civMaskColor 遮罩层颜色
civClipPadding 裁剪框边距
Java代码
如果裁剪的图片不大,可以直接设置,就像使用ImageView一样,通过如下四种方法设置图片:

mClipImageView.setImageURI(Uri.fromFile(new File(mInput)));
mClipImageView.setImageBitmap(bitmap);
mClipImageView.setImageResource(R.drawable.xxxx);
mClipImageView.setImageDrawable(drawable);

裁剪的时候调用mClipImageView.clip();就可以返回裁剪之后的Bitmap对象。

大图裁剪
这里会把大图裁剪及图片文件可能旋转的情况一起处理。
注意:由于裁剪图片*终还是需要把裁剪结果以Bitmap对象加载到内存中,所以裁剪之后的图片也是会有大小限制的,否则会有OOM的情况。所以,下面会设一个裁剪后的*大宽度的值。

读取图片旋转角度
在*篇《 Android开发技巧——Camera拍照功能 》的时候,有提到过像三星的手机,竖屏拍出来的照片还是横的,但是有Exif信息记录了它的旋转方向。考虑到我们进行裁剪的时候,也会遇到类似这样的照片,所以对于这种照片需要旋转的情况,我选择了在裁剪的时候才进行处理。所以首先,我们需要读到图片的旋转角度:

/**
* 读取图片属性:旋转的角度
*
* @param path 图片*对路径
* @return degree旋转的角度
*/
public static int readPictureDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}

如果你能确保要裁剪的图片不大不会导致OOM的情况发生的话,是可以直接通过这个角度,创建一个Matrix对象,进行postRotate,然后由原图创建一个新的Bitmap来得到一个正确朝向的图片的。但是这里考虑到我们要裁剪的图片是从手机里读取的,有可能有大图,而我们的裁剪控件本身只实现了简单的手势缩放和裁剪功能,并没有实现大图加载的功能,所以需要在设置图片进行之前进行一些预处理。

采样缩放
由于图片较大,而我们又需要把整张图都加载进来而不是只加载局部,所以就需要在加载的时候进行采样,来加载缩小之后的图片,这样加载到的图片较小,就能有效避免OOM了。
以前文提到的裁剪证件照为例,这里仍以宽度为参考值来计算采样值,具体是用宽还是高或者是综合宽高(这种情况较多,考虑到可能会有很长的图)来计算采样值,还得看你具体情况。在计算采样的时候,我们还需要用到上面读到的旋转值,在图片被旋转90度或180度时,进行宽和高的置换。所以,除了相关的控件,我们需要定义如下相关的变量:

private String mOutput;
private String mInput;
private int mMaxWidth;

// 图片被旋转的角度
private int mDegree;
// 大图被设置之前的采样比例
private int mSampleSize;
private int mSourceWidth;
private int mSourceHeight;

计算采样代码如下:

mClipImageView.post(new Runnable() {
@Override
public void run() {
mClipImageView.setMaxOutputWidth(mMaxWidth);

mDegree = readPictureDegree(mInput);

final boolean isRotate = (mDegree == 90 || mDegree == 270);

final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(mInput, options);

mSourceWidth = options.outWidth;
mSourceHeight = options.outHeight;

// 如果图片被旋转,则宽高度置换
int w = isRotate ? options.outHeight : options.outWidth;

// 裁剪是宽高比例3:2,只考虑宽度情况,这里按border宽度的两倍来计算缩放。
mSampleSize = findBestSample(w, mClipImageView.getClipBorder().width());
//代码未完,将下面的[缩放及设置]里分段讲到。
}
});

由于我们是需要裁剪控件的裁剪框来计算采样,所以需要获取裁剪框,因此我们把上面的代码通过控件的post方法来调用。
inJustDecodeBounds在许多讲大图缩放的博客都有讲到,相信很多朋友都清楚,本文就不赘述了。
注意:采样的值是2的幂次方的,如果你传的值不是2的幂次方,它在计算的时候*终会往下找到*近的2的幂次方的值。所以,如果你后面还需要用这个值来进行计算,就不要使用网上的一些直接用两个值相除进行计算sampleSize的方法。精确的计算方式应该是直接计算时这个2的幂次方的值,例如下面代码:

/**
* 计算*好的采样大小。
* @param origin 当前宽度
* @param target 限定宽度
* @return sampleSize
*/
private static int findBestSample(int origin, int target) {
int sample = 1;
for (int out = origin / 2; out > target; out /= 2) {
sample *= 2;
}
return sample;
}

缩放及设置
接下来就是设置inJustDecodeBounds,inSampleSize,以及把inPreferredConfig设置为RGB_565,然后把图片给加载进来,如下:

options.inJustDecodeBounds = false;
options.inSampleSize = mSampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
final Bitmap source = BitmapFactory.decodeFile(mInput, options);

这里加载的图片还是没有旋转到正确朝向的,所以我们要根据上面所计算的角度,对图片进行旋转。我们竖屏拍的图,在一些手机上是横着保存的,但是它会记录一个旋转90度的值在Exif中。如下图中,左边是保存的图,它依然是横着的,右边是我们显示时的图。所以我们读取到这个值后,需要对它进行顺时针的旋转。

%title插图%num

代码如下:

// 解决图片被旋转的问题
Bitmap target;
if (mDegree == 0) {
target = source;
} else {
final Matrix matrix = new Matrix();
matrix.postRotate(mDegree);
target = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, false);
if (target != source && !source.isRecycled()) {
source.recycle();
}
}
mClipImageView.setImageBitmap(target);

这里需要补充的一个注意点是:Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, false);这个方法返回的Bitmap不一定是重新创建的,如果matrix相同并且宽高相同,而且你没有对Bitmap进行其他设置的话,它可能会返回原来的对象。所以在创建新的Bitmap之后,回收原来的Bitmap时要判断是否可以回收,否则可能导致创建出来的target对象被回收而使ImageView的图片无法显示出来。
如上,就是完整的设置大图时的处理过程的代码。

裁剪
裁剪时需要创建一个裁剪之后的Bitmap,再把它保存下来。下面介绍一下这个创建过程。完整代码如下:

private Bitmap createClippedBitmap() {
if (mSampleSize <= 1) {
return mClipImageView.clip();
}

// 获取缩放位移后的矩阵值
final float[] matrixValues = mClipImageView.getClipMatrixValues();
final float scale = matrixValues[Matrix.MSCALE_X];
final float transX = matrixValues[Matrix.MTRANS_X];
final float transY = matrixValues[Matrix.MTRANS_Y];

// 获取在显示的图片中裁剪的位置
final Rect border = mClipImageView.getClipBorder();
final float cropX = ((-transX + border.left) / scale) * mSampleSize;
final float cropY = ((-transY + border.top) / scale) * mSampleSize;
final float cropWidth = (border.width() / scale) * mSampleSize;
final float cropHeight = (border.height() / scale) * mSampleSize;

// 获取在旋转之前的裁剪位置
final RectF srcRect = new RectF(cropX, cropY, cropX + cropWidth, cropY + cropHeight);
final Rect clipRect = getRealRect(srcRect);

final BitmapFactory.Options ops = new BitmapFactory.Options();
final Matrix outputMatrix = new Matrix();

outputMatrix.setRotate(mDegree);
// 如果裁剪之后的图片宽高仍然太大,则进行缩小
if (mMaxWidth > 0 && cropWidth > mMaxWidth) {
ops.inSampleSize = findBestSample((int) cropWidth, mMaxWidth);

final float outputScale = mMaxWidth / (cropWidth / ops.inSampleSize);
outputMatrix.postScale(outputScale, outputScale);
}

// 裁剪
BitmapRegionDecoder decoder = null;
try {
decoder = BitmapRegionDecoder.newInstance(mInput, false);
final Bitmap source = decoder.decodeRegion(clipRect, ops);
recycleImageViewBitmap();
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), outputMatrix, false);
} catch (Exception e) {
return mClipImageView.clip();
} finally {
if (decoder != null && !decoder.isRecycled()) {
decoder.recycle();
}
}
}

下面分段介绍。

计算在采样缩小前的裁剪框
首先,如果采样值不大于1,也就是我们没有进行图片缩小的时候,就不需要进行下面的计算了,直接调用我们的裁剪控件返回裁剪后的图片即可。否则,就是我们对图片进行缩放的情况了,所以会需要综合我们的采样值mSampleSize,计算我们的裁剪框实际上在原图上的位置。所以会看到相对于上篇所讲的裁剪控件对裁剪框的计算,这里多乘了一个mSampleSize的值,如下:

// 获取在显示的图片中裁剪的位置
final Rect border = mClipImageView.getClipBorder();
final float cropX = ((-transX + border.left) / scale) * mSampleSize;
final float cropY = ((-transY + border.top) / scale) * mSampleSize;
final float cropWidth = (border.width() / scale) * mSampleSize;
final float cropHeight = (border.height() / scale) * mSampleSize;

然后我们创建这个在原图大小时的裁剪框:

final RectF srcRect = new RectF(cropX, cropY, cropX + cropWidth, cropY + cropHeight);

计算在图片旋转前的裁剪框
对于大图的裁剪,我们可以使用BitmapRegionDecoder类,来只加载图片的一部分,也就是用它来加载我们所需要裁剪的那一部分,但是它是从旋转之前的原图进行裁剪的,所以还需要对这个裁剪框进行反向的旋转,来计算它在原图上的位置。
如下图所示,ABCD是旋转90度之后的图片,EFGH是我们的裁剪框。

%title插图%num

但是在原图中,它们的对应位置如下图所示:

%title插图%num

也就是B点成了A点,A点成了D点,等等。
所以我们获取EFGH在ABCD中的位置,也不能像裁剪控件那样,而需要进行反转之后的计算。以旋转90度为例,现在我们的左上角变成了F点,那么它的left就是原来的top,它的top就是图片的高度减去原来的right,它的right就是原来的bottom,它的bottom就是图片的高度减去原来的left,完整代码如下:

private Rect getRealRect(RectF srcRect) {
switch (mDegree) {
case 90:
return new Rect((int) srcRect.top, (int) (mSourceHeight – srcRect.right),
(int) srcRect.bottom, (int) (mSourceHeight – srcRect.left));
case 180:
return new Rect((int) (mSourceWidth – srcRect.right), (int) (mSourceHeight – srcRect.bottom),
(int) (mSourceWidth – srcRect.left), (int) (mSourceHeight – srcRect.top));
case 270:
return new Rect((int) (mSourceWidth – srcRect.bottom), (int) srcRect.left,
(int) (mSourceWidth – srcRect.top), (int) srcRect.right);
default:
return new Rect((int) srcRect.left, (int) srcRect.top, (int) srcRect.right, (int) srcRect.bottom);
}
}

所以在原图上的真正的裁剪框位置是:
final Rect clipRect = getRealRect(srcRect);

局部加载所裁剪的图片部分
大图裁剪,我们使用BitmapRegionDecoder类,它可以只加载指定的某一部分的图片内容,通过它的public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options)方法,我们可以把所裁剪的内容加载出来,得到一个Bitmap,这个Bitmap就是我们要裁剪的内容了。但是,我们加载的这部分内容,同样可能太宽,所以还可能需要进行采样缩小。如下:

final BitmapFactory.Options ops = new BitmapFactory.Options();
final Matrix outputMatrix = new Matrix();//用于*图图片的精确缩放

outputMatrix.setRotate(mDegree);
// 如果裁剪之后的图片宽高仍然太大,则进行缩小
if (mMaxWidth > 0 && cropWidth > mMaxWidth) {
ops.inSampleSize = findBestSample((int) cropWidth, mMaxWidth);

final float outputScale = mMaxWidth / (cropWidth / ops.inSampleSize);
outputMatrix.postScale(outputScale, outputScale);
}

计算出采样值sampleSize之后,再使用它及我们计算的裁剪框,加载所裁剪的内容:

// 裁剪
BitmapRegionDecoder decoder = null;
try {
decoder = BitmapRegionDecoder.newInstance(mInput, false);
final Bitmap source = decoder.decodeRegion(clipRect, ops);
recycleImageViewBitmap();
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), outputMatrix, false);
} catch (Exception e) {
return mClipImageView.clip();
} finally {
if (decoder != null && !decoder.isRecycled()) {
decoder.recycle();
}
}

总结
完整代码见github上我的clip-image项目的示例ClipImageActivity.java。
上面例子中,我所用的图片并不大,下面我打包了一个大图的apk,它使用了维基百科上的一张世界地图,下载地址如下:http://download.csdn.net/detail/maosidiaoxian/9464200

上面的例子截图:

%title插图%num

可以看出,在这个例子中,虽然在裁剪过程当中图片被缩放过所以不太清晰,但是我们真正的裁剪是对原图进行裁剪再进行适当的缩放的,所以裁剪之后的图片更清晰。

Android开发技巧-定制仿微信图片裁剪控件

拍照——裁剪,或者是选择图片——裁剪,是我们设置头像或上传图片时经常需要的一组操作。上篇讲了Camera的使用,这篇讲一下我对图片裁剪的实现。

背景
下面的需求都来自产品。
裁剪图片要像微信那样,拖动和放大的是图片,裁剪框不动。
裁剪框外的内容要有半透明黑色遮罩。
裁剪框下面要显示一行提示文字(这点我至今还是持保留意见的)。
在Android中,裁剪图片的控件库还是挺多的,特别是github上比较流行的几个,都已经进化到比较稳定的阶段,但比较遗憾的是它们的裁剪过程是拖动或缩放裁剪框,于是只好自己再找,看有没有现成或半成品的轮子,可以不必从零开始。
踏破铁鞋无觅处,*天不负苦心人。我终于找到了两篇博客:《Android高仿微信头像裁剪》和《Android 高仿微信头像截取 打造不一样的自定义控件》,以及csdn上找到的前面博客所对应的一份代码,并*终实现了自己的裁剪控件。

大神的实现过程
首先先了解一下上面的高仿微信裁剪控件的实现过程。说起来也不难,主要是下面几点:
1,重写ImageView,并监听手势事件,包括双点,两点缩放,拖动,使它成为一个实现缩放拖动图片功能的控件。
2,定义一个Matrix成员变量,对于维护该图片的缩放、平移等矩阵数据。
3,拖动或缩放时,图片与裁剪框的相交面积一定与裁剪框相等。即图片不能拖离裁剪框。
3,在设置图片时,先根据图片的大小进行初始化的缩放平移操作,使得上面第三条的条件下图片尽可能的小。
4,每次接收到相对应的手势事件,都进行对应的矩阵计算,并将计算结果通过ImageView的setImageMatrix方法应用到图片上。
5,裁剪框是一个单独的控件,与ImageView同样大,叠加到它上面显示出来。
6,用一个XXXLayout把裁剪框和缩放封装起来。
7,裁剪时,先创建一个空的Bitmap并用其创建一个Canvas,把缩放平移后的图片画到这个Bitmap上,并创建在裁剪框内的Bitmap(通过调用Bitmap.createBitmap方法)。

我的定制内容
我拿到的代码是鸿洋大神版本之后再被改动的,代码上有点乱(虽然功能上是实现的裁剪)。在原有的功能上,我希望进行的改动有:

合并裁剪框的内容到ImageView中
裁剪框可以是任意长宽比的矩形
裁剪框的左右外边距可以设置
遮罩层颜色可以设置
裁剪框下有提示文字(自己的产品需求)
后面产品又加入了一条裁剪图片的*大大小
属性定义
在上面的功能需求中,我定义了以下属性:

<declare-styleable name=”ClipImageView”>
<attr name=”civHeight” format=”integer”/>
<attr name=”civWidth” format=”integer”/>
<attr name=”civTipText” format=”string”/>
<attr name=”civTipTextSize” format=”dimension”/>
<attr name=”civMaskColor” format=”color”/>
<attr name=”civClipPadding” format=”dimension”/>
</declare-styleable>

其中:

civHeight和civWidth是裁剪框的宽高比例。
civTipText提示文字的内容
civTipTextSize提示文字的大小
civMaskColor遮罩层的颜色值
civClipPadding裁剪内边距。由于裁剪框是在控件内部的,*终我选择使用padding来说明裁剪框与我们控件边缘的距离。
成员变量
成员变量我进行了一些改动,把原本用于定义裁剪框的水平边距变量及其他没什么用的变量等给去掉了,并加入了自己的一些成员变量,*终如下:

private final int mMaskColor;//遮罩层颜色

private final Paint mPaint;//画笔
private final int mWidth;//裁剪框宽的大小(从属性上读到的整型值)
private final int mHeight;//裁剪框高的大小(同上)
private final String mTipText;//提示文字
private final int mClipPadding;//裁剪框相对于控件的内边距

private float mScaleMax = 4.0f;//图片*大缩放大小
private float mScaleMin = 2.0f;//图片*小缩放大小

/**
* 初始化时的缩放比例
*/
private float mInitScale = 1.0f;

/**
* 用于存放矩阵
*/
private final float[] mMatrixValues = new float[9];

/**
* 缩放的手势检查
*/
private ScaleGestureDetector mScaleGestureDetector = null;
private final Matrix mScaleMatrix = new Matrix();

/**
* 用于双击
*/
private GestureDetector mGestureDetector;
private boolean isAutoScale;

private float mLastX;
private float mLastY;

private boolean isCanDrag;
private int lastPointerCount;

private Rect mClipBorder = new Rect();//裁剪框
private int mMaxOutputWidth = 0;//裁剪后的图片的*大输出宽度

构造方法
构造方法里主要是多了一些我们自定义属性的读取:

public ClipImageView(Context context) {
this(context, null);
}

public ClipImageView(Context context, AttributeSet attrs) {
super(context, attrs);

setScaleType(ScaleType.MATRIX);
mGestureDetector = new GestureDetector(context,
new SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
if (isAutoScale)
return true;

float x = e.getX();
float y = e.getY();
if (getScale() < mScaleMin) {
ClipImageView.this.postDelayed(new AutoScaleRunnable(mScaleMin, x, y), 16);
} else {
ClipImageView.this.postDelayed(new AutoScaleRunnable(mInitScale, x, y), 16);
}
isAutoScale = true;

return true;
}
});
mScaleGestureDetector = new ScaleGestureDetector(context, this);
this.setOnTouchListener(this);

mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.WHITE);

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClipImageView);
mWidth = ta.getInteger(R.styleable.ClipImageView_civWidth, 1);
mHeight = ta.getInteger(R.styleable.ClipImageView_civHeight, 1);
mClipPadding = ta.getDimensionPixelSize(R.styleable.ClipImageView_civClipPadding, 0);
mTipText = ta.getString(R.styleable.ClipImageView_civTipText);
mMaskColor = ta.getColor(R.styleable.ClipImageView_civMaskColor, 0xB2000000);
final int textSize = ta.getDimensionPixelSize(R.styleable.ClipImageView_civTipTextSize, 24);
mPaint.setTextSize(textSize);
ta.recycle();

mPaint.setDither(true);
}

定义裁剪框
裁剪框的位置
裁剪框是在控件正中间的,首先我们从属性中读取到的是宽高的比例,以及左右边距,但是在构造方法中,由于控件还没有绘制出来,无法获取到控件的宽高,所以并不能计算裁剪框的大小和位置。所以我重写了onLayout方法,在这里计算裁剪框的位置:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
final int width = getWidth();
final int height = getHeight();
mClipBorder.left = mClipPadding;
mClipBorder.right = width – mClipPadding;
final int borderHeight = mClipBorder.width() * mHeight / mWidth;
mClipBorder.top = (height – borderHeight) / 2;
mClipBorder.bottom = mClipBorder.top + borderHeight;
}

绘制裁剪框
这里我顺便把绘制提示文字的代码也一并给出,都是在同一个方法里的。很简单,重写onDraw方法即可。绘制裁剪框有两种方法,一是绘制一个满屏的遮罩层,然后从中间抠出一个长方形出来,但是我用的时候发现抠不出来,所以我采用的是下面这一种:

%title插图%num

先画上下两个矩形,再画左右两个矩形,中间所围起来的没有画的部分就是我们的裁剪框。

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int width = getWidth();
final int height = getHeight();

mPaint.setColor(mMaskColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(0, 0, width, mClipBorder.top, mPaint);
canvas.drawRect(0, mClipBorder.bottom, width, height, mPaint);
canvas.drawRect(0, mClipBorder.top, mClipBorder.left, mClipBorder.bottom, mPaint);
canvas.drawRect(mClipBorder.right, mClipBorder.top, width, mClipBorder.bottom, mPaint);

mPaint.setColor(Color.WHITE);
mPaint.setStrokeWidth(1);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawRect(mClipBorder.left, mClipBorder.top, mClipBorder.right, mClipBorder.bottom, mPaint);

if (mTipText != null) {
final float textWidth = mPaint.measureText(mTipText);
final float startX = (width – textWidth) / 2;
final Paint.FontMetrics fm = mPaint.getFontMetrics();
final float startY = mClipBorder.bottom + mClipBorder.top / 2 – (fm.descent – fm.ascent) / 2;
mPaint.setStyle(Paint.Style.FILL);
canvas.drawText(mTipText, startX, startY, mPaint);
}
}

修改图片的初始显示
这里我不使用全局布局的监听(通过getViewTreeObserver加入回调),而是直接重写几个设置图片的方法,在设置图片后进行初始显示的设置:

@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
postResetImageMatrix();
}

@Override
public void setImageResource(int resId) {
super.setImageResource(resId);
postResetImageMatrix();
}

@Override
public void setImageURI(Uri uri) {
super.setImageURI(uri);
postResetImageMatrix();
}

private void postResetImageMatrix() {
post(new Runnable() {
@Override
public void run() {
resetImageMatrix();
}
});
}

resetImageMatrix()方法设置图片的初始缩放及平移,参考图片大小,控件本身大小,以及裁剪框的大小进行计算:

/**
* 垂直方向与View的边矩
*/
public void resetImageMatrix() {
final Drawable d = getDrawable();
if (d == null) {
return;
}

final int dWidth = d.getIntrinsicWidth();
final int dHeight = d.getIntrinsicHeight();

final int cWidth = mClipBorder.width();
final int cHeight = mClipBorder.height();

final int vWidth = getWidth();
final int vHeight = getHeight();

final float scale;
final float dx;
final float dy;

if (dWidth * cHeight > cWidth * dHeight) {
scale = cHeight / (float) dHeight;
} else {
scale = cWidth / (float) dWidth;
}

dx = (vWidth – dWidth * scale) * 0.5f;
dy = (vHeight – dHeight * scale) * 0.5f;

mScaleMatrix.setScale(scale, scale);
mScaleMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));

setImageMatrix(mScaleMatrix);

mInitScale = scale;
mScaleMin = mInitScale * 2;
mScaleMax = mInitScale * 4;
}

注意:这里有一个坑。把一个Bitmap设置到ImageView中,显示时要计算的是ImageView获取的Drawable对象以及这个对象的宽高,而不是Bitmap对象。Drawable对象可能由于对Bitmap的放大或缩小显示,导致它的宽或高与Bitmap的宽高不同。
还有一点小注意:获取控件宽高是要在控件被绘制出来之后才能获取得到的,所以上面我通过post一个Runnable对象到主线程的Looper中,保证它是在界面绘制完成之后被调用。

缩放及拖动
缩放及拖动时都需求判断是否超出边界,如果超出,则取允许的*终值。这里的代码我没怎么动,稍后可直接参考源码,暂不赘述。

裁剪
这里是另外一个改造的重点了。
首先,鸿洋大神是通过创建一个空的Bitmap,并根据它创建出一个Canvas对象,然后通过draw方法把缩放后的图片给绘制到这个Bitmap中,再调用Bitmap.createBitmap得到属于裁剪框的内容。但是我们已经重写了onDraw方法画出裁剪框,所以这里就不考虑了。
另外,这种方法还有一个问题:它绘制的是Drawable对象。如果我们设置进去的是一个比较大的Bitmap,那么就可能被缩放了,这里裁剪的是缩放后的Bitmap,也就是它不是对原图进行裁剪的。
这里我参考了其他裁剪图片库,通过保存了缩放平移的Matrix成员变量进行计算,获取出裁剪框在其的对应范围,并根据*终所需(我们产品要限制一个*大大小),得到*终的图片,代码如下:

public Bitmap clip() {
final Drawable drawable = getDrawable();
final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();

final float[] matrixValues = new float[9];
mScaleMatrix.getValues(matrixValues);
final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / originalBitmap.getWidth();
final float transX = matrixValues[Matrix.MTRANS_X];
final float transY = matrixValues[Matrix.MTRANS_Y];

final float cropX = (-transX + mClipBorder.left) / scale;
final float cropY = (-transY + mClipBorder.top) / scale;
final float cropWidth = mClipBorder.width() / scale;
final float cropHeight = mClipBorder.height() / scale;

Matrix outputMatrix = null;
if (mMaxOutputWidth > 0 && cropWidth > mMaxOutputWidth) {
final float outputScale = mMaxOutputWidth / cropWidth;
outputMatrix = new Matrix();
outputMatrix.setScale(outputScale, outputScale);
}

return Bitmap.createBitmap(originalBitmap,
(int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight,
outputMatrix, false);
}

由于我们是对Bitmap进行裁剪,所以首先获取这个Bitmap:

final Drawable drawable = getDrawable();
final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();

然后,我们的矩阵值可以通过一个包含9个元素的float数组读出:

final float[] matrixValues = new float[9];
mScaleMatrix.getValues(matrixValues);

比如,读X上的缩放值,代码为matrixValues[Matrix.MSCALE_X]。
要特别注意一点,在前文也有提到,这里缩放的是Drawable对象,但是我们裁剪时用的Bitmap,如果图片太大的话是可能在Drawable上进行缩放的,所以缩放大小的计算应该为:

final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / originalBitmap.getWidth();

然后获取图片平移量:

final float transX = matrixValues[Matrix.MTRANS_X];
final float transY = matrixValues[Matrix.MTRANS_Y];

计算裁剪框对应在图片上的起点及宽高:

final float cropX = (-transX + mClipBorder.left) / scale;
final float cropY = (-transY + mClipBorder.top) / scale;
final float cropWidth = mClipBorder.width() / scale;
final float cropHeight = mClipBorder.height() / scale;

上面就是我们所要裁剪出来的*终结果。
但是,我前面也说的,应产品需求,要限制*大输出大小。由于我们裁剪出来的图片宽高比是3:2,我这里只取宽度(你要取高度也可以)进行限制,所以又加上了如下代码,当裁剪出来的宽度超出我们*大宽度时,进行缩放。

Matrix outputMatrix = null;
if (mMaxOutputWidth > 0 && cropWidth > mMaxOutputWidth) {
final float outputScale = mMaxOutputWidth / cropWidth;
outputMatrix = new Matrix();
outputMatrix.setScale(outputScale, outputScale);
}

*终根据上面计算出来的值,创建裁剪出来的Bitmap:

Bitmap.createBitmap(originalBitmap,
(int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight,
outputMatrix, false);

这样,图片裁剪控件就算全部完成。

实现效果

%title插图%num %title插图%num

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速