├── .gitignore ├── README.md ├── build.gradle ├── libs └── AndroidUtils.aar ├── notes ├── 你需要知道的Android拍照适配问题.md ├── 深入理解Android中的Matrix.md └── 知乎和简书的夜间模式实现套路.md ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── clock │ └── study │ └── ApplicationTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── clock │ │ └── study │ │ ├── StudyApplication.java │ │ ├── activity │ │ ├── AnimationActivity.java │ │ ├── AnimationTestActivity.java │ │ ├── AnimatorActivity.java │ │ ├── AuthorActivity.java │ │ ├── CapturePhotoActivity.java │ │ ├── DayNightActivity.java │ │ ├── MainActivity.java │ │ └── PhotoPreviewActivity.java │ │ ├── adapter │ │ └── SimpleAuthorAdapter.java │ │ ├── animation │ │ └── SimpleCustomAnimation.java │ │ ├── animator │ │ └── ColorEvaluator.java │ │ ├── helper │ │ ├── CapturePhotoHelper.java │ │ └── DayNightHelper.java │ │ ├── manager │ │ └── FolderManager.java │ │ └── type │ │ └── DayNight.java └── res │ ├── animator │ └── simple_animator.xml │ ├── layout │ ├── activity_android_anim.xml │ ├── activity_animation_test.xml │ ├── activity_animator.xml │ ├── activity_author.xml │ ├── activity_camera_take_photo.xml │ ├── activity_day_night.xml │ ├── activity_main.xml │ ├── activity_photo_preview.xml │ ├── author_card_layout.xml │ └── author_info_layout.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── lufy.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── test └── java └── com └── clock └── study └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.iml 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidStudyCode 2 | 3 | 记录一些 Android 相关的学习知识和原理,需要的童鞋可以看看。有bug请提Issue哇! 4 | 5 | ## 运行须知 6 | 7 | - 本库clone到本地后,直接以 moudle 形式导入即可运行; 8 | - 本库libs下有个**AndroidUtils.aar**的依赖包,是由[AndroidUtils](https://github.com/D-clock/AndroidUtils)代码编译生成; 9 | - 想要查看**AndroidUtils.aar**中的源代码,可以参考[这里](https://github.com/D-clock/Doc/blob/master/Android/%E4%B8%AA%E4%BA%BA%E6%94%B6%E8%97%8F/%E5%A6%82%E4%BD%95%E6%9F%A5%E7%9C%8Baar%E7%9A%84%E6%BA%90%E4%BB%A3%E7%A0%81.md) 10 | 11 | ## 最新更新(编辑于2016-08-29) 12 | 13 | - 兼添加夜间模式的实现分析,详见文章 [知乎和简书的夜间模式实现套路](notes/知乎和简书的夜间模式实现套路.md) 14 | 15 | ## 归档文章 16 | 17 | - [你需要知道的Android拍照适配问题](notes/你需要知道的Android拍照适配问题.md) 18 | - [深入理解Android中的Matrix](notes/深入理解Android中的Matrix.md) 19 | - [知乎和简书的夜间模式实现套路](notes/知乎和简书的夜间模式实现套路.md) 20 | 21 | ## 找我 22 | 23 | - [新浪微博](http://weibo.com/2480694892/profile?rightmod=1&wvr=6&mod=personinfo&is_all=1) 24 | - [Diycode社区](http://diycode.cc/d_clock) 25 | - [简书](http://www.jianshu.com/users/ec95b5891948/latest_articles) 26 | - [稀土掘金](http://gold.xitu.io/#/user/5647657960b27f7a01ba5122) 27 | - [开发者头条](http://toutiao.io/u/167597) -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:2.1.3' 7 | // NOTE: Do not place your application dependencies here; they belong 8 | // in the individual module build.gradle files 9 | } 10 | } 11 | 12 | apply plugin: 'com.android.application' 13 | 14 | android { 15 | compileSdkVersion 23 16 | buildToolsVersion "23.0.3" 17 | 18 | defaultConfig { 19 | applicationId "com.clock.study" 20 | minSdkVersion 11 21 | targetSdkVersion 23 22 | versionCode 1 23 | versionName "1.0" 24 | } 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | } 32 | 33 | repositories { 34 | flatDir { 35 | dirs 'libs' //this way we can find the .aar file in libs folder 36 | } 37 | } 38 | 39 | dependencies { 40 | compile fileTree(dir: 'libs', include: ['*.jar']) 41 | testCompile 'junit:junit:4.12' 42 | compile(name: 'AndroidUtils', ext: 'aar') 43 | //compile project(":AndroidUtils") 44 | compile 'com.android.support:appcompat-v7:23.2.1' 45 | compile 'com.android.support:recyclerview-v7:23.2.1' 46 | compile 'com.android.support:cardview-v7:23.1.1' 47 | } -------------------------------------------------------------------------------- /libs/AndroidUtils.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-clock/AndroidStudyCode/f656b3a533edf2867f46f6a26bb4c7d21811bd95/libs/AndroidUtils.aar -------------------------------------------------------------------------------- /notes/你需要知道的Android拍照适配问题.md: -------------------------------------------------------------------------------- 1 | # 你需要知道的Android拍照适配方案 2 | 3 | 近段时间,家里陪自己度过大学四年的电脑坏了,挑选好的新电脑配件终于在本周全部到货,自己动手完成组装。从AMD到i7的CPU,6G内存到14G内存,打开 AndroidStudio 的速度终于杠杆的上去了,感动到泪流满面啊!!!!!!!扯了这么多,回归一下正题,还是来说说本篇文章要写什么吧!说起调用系统相机来拍照的功能,大家肯定不陌生,现在所有应用都具备这个功能。例如最基本的,用户拍照上传头像。Android开发的孩纸都知道,碎片化给拍照这个功能的实现带来挺多头疼的问题。**所以,我决定写写一些网上不多见但又经常听到童鞋们吐槽的问题。** 4 | 5 | ## 拍照功能实现 6 | 7 | Android 程序上实现拍照功能的方式分为两种:第一种是利用相机的 API 来自定义相机,第二种是利用 Intent 调用系统指定的相机拍照。下面讲的内容都是针对第二种实现方式的适配。 8 | 9 | 通常情况下,我们调用拍照的业务场景是如下面这样的: 10 | 11 | 1. A 界面,点击按钮调用相机拍照; 12 | 2. A 界面得到拍完照片,跳转到 B 界面进行预览; 13 | 3. B 界面有个按钮,点击后触发某个业务流程来处理这张照片; 14 | 15 | 实现的大体流程代码如下: 16 | 17 | ```java 18 | 19 | //1、调用相机 20 | File mPhotoFile = new File(folder,filename); 21 | Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 22 | Uri fileUri = Uri.fromFile(mPhotoFile); 23 | captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); 24 | mActivity.startActivityForResult(captureIntent, CAPTURE_PHOTO_REQUEST_CODE); 25 | 26 | //2、拿到照片 27 | @Override 28 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 29 | if (requestCode == CapturePhotoHelper.CAPTURE_PHOTO_REQUEST_CODE && resultCode == RESULT_OK) { 30 | File photoFile = mCapturePhotoHelper.getPhoto();//获取拍完的照片 31 | if (photoFile != null) { 32 | PhotoPreviewActivity.preview(this, photoFile);//跳转到预览界面 33 | } 34 | finish(); 35 | } else { 36 | super.onActivityResult(requestCode, resultCode, data); 37 | } 38 | } 39 | 40 | //3、各种各样处理这张图片的业务代码 41 | 42 | ``` 43 | 44 | 到这里基本科普完了如何调用系统相机拍照,相信这些网上一搜一大把的代码,很多童鞋都能看懂。 45 | 46 | ## 有没有相机可用? 47 | 48 | 前面讲到我们是调用系统指定的相机app来拍照,那么系统是否存在可以被我们调用的app呢?这个我们不敢确定,毕竟 Android 奇葩问题多,还真有遇到过这种极端的情况导致闪退的。虽然很极端,但作为客户端人员还是要进行处理,方式有二: 49 | 50 | 1. 调用相机时,简单粗暴的 try-catch 51 | 2. 调用相机前,检测系统有没有相机 app 可用 52 | 53 | try-catch 这种粗暴的方式大家肯定很熟悉了,那么要如何检测系统有没有相机 app 可用呢?系统在 PackageManager 里为我们提供这样一个 API 54 | 55 | ![](http://d.hiphotos.baidu.com/image/pic/item/2e2eb9389b504fc2e61bced6e2dde71191ef6d98.jpg) 56 | 57 | 通过这样一个 API ,可以知道系统是否存在 action 为 MediaStore.ACTION_IMAGE_CAPTURE 的 intent 可以唤起的拍照界面,具体实现代码如下: 58 | 59 | ```java 60 | 61 | /** 62 | * 判断系统中是否存在可以启动的相机应用 63 | * 64 | * @return 存在返回true,不存在返回false 65 | */ 66 | public boolean hasCamera() { 67 | PackageManager packageManager = mActivity.getPackageManager(); 68 | Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 69 | List list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 70 | return list.size() > 0; 71 | } 72 | 73 | ``` 74 | 75 | ## 拍出来的照片“歪了”!!! 76 | 77 | 经常会遇到一种情况,拍照时看到照片是正的,但是当我们的 app 获取到这张照片时,却发现旋转了 90 度(也有可能是180、270,不过90度比较多见,貌似都是由于手机传感器导致的)。很多童鞋对此感到很困扰,因为不是所有手机都会出现这种情况,就算会是出现这种情况的手机上,也并非每次必现。要怎么解决这个问题呢?从解决的思路上看,只要获取到照片旋转的角度,利用 Matrix 来进行角度纠正即可。**那么问题来了,要怎么知道照片旋转的角度呢?**细心的童鞋可能会发现,拍完一张照片去到相册点击属性查看,能看到下面这样一堆关于照片的属性数据 78 | 79 | ![](http://a.hiphotos.baidu.com/image/pic/item/32fa828ba61ea8d3c52d32d7900a304e241f58d1.jpg) 80 | 81 | 没错,这里面就有一个旋转角度,倘若拍照后保存的成像照片文件发生了角度旋转,这个图片的属性参数就能告诉我们到底旋转了多少度。只要获取到这个角度值,我们就能进行纠正的工作了。 Android 系统提供了 ExifInterface 类来满足获取图片各个属性的操作 82 | 83 | ![](http://h.hiphotos.baidu.com/image/pic/item/503d269759ee3d6de2abc41844166d224e4adef0.jpg) 84 | 85 | 通过 ExifInterface 类拿到 TAG_ORIENTATION 属性对应的值,即为我们想要得到旋转角度。再根据利用 Matrix 进行旋转纠正即可。实现代码大致如下: 86 | 87 | ```java 88 | 89 | /** 90 | * 获取图片的旋转角度 91 | * 92 | * @param path 图片绝对路径 93 | * @return 图片的旋转角度 94 | */ 95 | public static int getBitmapDegree(String path) { 96 | int degree = 0; 97 | try { 98 | // 从指定路径下读取图片,并获取其EXIF信息 99 | ExifInterface exifInterface = new ExifInterface(path); 100 | // 获取图片的旋转信息 101 | int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); 102 | switch (orientation) { 103 | case ExifInterface.ORIENTATION_ROTATE_90: 104 | degree = 90; 105 | break; 106 | case ExifInterface.ORIENTATION_ROTATE_180: 107 | degree = 180; 108 | break; 109 | case ExifInterface.ORIENTATION_ROTATE_270: 110 | degree = 270; 111 | break; 112 | } 113 | } catch (IOException e) { 114 | e.printStackTrace(); 115 | } 116 | return degree; 117 | } 118 | 119 | /** 120 | * 将图片按照指定的角度进行旋转 121 | * 122 | * @param bitmap 需要旋转的图片 123 | * @param degree 指定的旋转角度 124 | * @return 旋转后的图片 125 | */ 126 | public static Bitmap rotateBitmapByDegree(Bitmap bitmap, int degree) { 127 | // 根据旋转角度,生成旋转矩阵 128 | Matrix matrix = new Matrix(); 129 | matrix.postRotate(degree); 130 | // 将原始图片按照旋转矩阵进行旋转,并得到新的图片 131 | Bitmap newBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); 132 | if (bitmap != null && !bitmap.isRecycled()) { 133 | bitmap.recycle(); 134 | } 135 | return newBitmap; 136 | } 137 | 138 | ``` 139 | 140 | ExifInterface 能拿到的信息远远不止旋转角度,其他的参数感兴趣的童鞋可以看看 API 文档。 141 | 142 | ## 拍完照怎么闪退了? 143 | 144 | 曾在小米和魅族的某些机型上遇到过这样的问题,调用系统相机拍照,拍完点击确定回到自己的app里面却莫名奇妙的闪退了。这种闪退有两个特点: 145 | 146 | 1. 没有什么错误日志(有些机子啥日志都没有,有些机子会出来个空异常错误日志); 147 | 2. 同个机子上非必现(有时候怎么拍都不闪退,有时候一拍就闪退); 148 | 149 | 对待非必现问题往往比较头疼,当初遇到这样的问题也是非常不解。上网搜罗了一圈也没方案,后来留意到一个比较有意思信息:**有些系统厂商的 ROM 会给自带相机应用做优化,当某个 app 通过 intent 进入相机拍照界面时,系统会把这个 app 当前最上层的 Activity 销毁回收。(注意:我遇到的情况是有时候很快就回收掉,有时候怎么等也不回收,没有什么必现规律)**为了验证一下,便在启动相机的 Activity 中对 onDestory 方法进行加 log 。果不其然,终于发现进入拍照界面的时候 onDestory 方法被执行了。所以,前面提到的闪退基本可以推测是 Activity 被回收导致某些非UI控件的成员变量为空导致的。(有些机子会报出空异常错误日志,但是有些机子闪退了什么都不报,是不是觉得很奇葩!) 150 | 151 | 既然涉及到 Activity 被回收的问题,自然要想起 onSaveInstanceState 和 onRestoreInstanceState 这对方法。去到 onSaveInstanceState 把数据保存,并在 onRestoreInstanceState 方法中进行恢复即可。大体代码思路如下: 152 | 153 | ```java 154 | 155 | @Override 156 | protected void onSaveInstanceState(Bundle outState) { 157 | super.onSaveInstanceState(outState); 158 | mRestorePhotoFile = mCapturePhotoHelper.getPhoto(); 159 | if (mRestorePhotoFile != null) { 160 | outState.putSerializable(EXTRA_RESTORE_PHOTO, mRestorePhotoFile); 161 | } 162 | 163 | } 164 | 165 | @Override 166 | protected void onRestoreInstanceState(Bundle savedInstanceState) { 167 | super.onRestoreInstanceState(savedInstanceState); 168 | mRestorePhotoFile = (File) savedInstanceState.getSerializable(EXTRA_RESTORE_PHOTO); 169 | mCapturePhotoHelper.setPhoto(mRestorePhotoFile); 170 | } 171 | 172 | ``` 173 | 174 | 对于 onSaveInstanceState 和 onRestoreInstanceState 方法的作用还不熟悉的童鞋,网上资料很多,可以自行搜索。 175 | 176 | 到这里,可能有童鞋要问,这种闪退并不能保证复现,我要怎么知道问题所在和是否修复了呢?我们可以去到开发者选项里开启**不保留活动**这一项进行调试验证 177 | 178 | ![](http://h.hiphotos.baidu.com/image/pic/item/e61190ef76c6a7ef011c2bf5fafaaf51f3de662f.jpg) 179 | 180 | 它作用是保留当前和用户接触的 Activity ,并将目前无法和用户交互 Activity 进行销毁回收。打开这个调试选项就可以满足验证的需求,当你的 app 的某个 Activity 跳转到拍照的 Activity 后,这个 Activity 立马就会被系统销毁回收,这样就可以很好的完全复现闪退的场景,帮助开发者确认问题有没有修复了。 181 | 182 | 涉及到 Activity 被销毁,还想提一下代码实现上的问题。假设当前有两个 Activity ,MainActivity 中有个 Button ,点击可以调用系统相机拍照并显示到 PreviewActivity 进行预览。有下面两种实现方案: 183 | 184 | - 方案一:MainActivity 中点击 Button 后,启动系统相机拍照,并在 MainActivity 的 onActivityResult 方法中获取拍下来的照片,并启动跳转到 PreviewActivity 界面进行效果预览; 185 | - 方案二:MainActivity 中点击 Button 后,启动 PreviewActivity 界面,在 PreviewActivity 的 onCreate(或者onStart、onResume)方法中启动系统相机拍照,然后在 PreviewActivity 的 onActivityResult 方法中获取拍下来的照片进行预览; 186 | 187 | 上面两种方案得到的实现效果是一模一样的,但是第二种方案却存在很大的问题。因为启动相机的代码放在 onCreate(或者onStart、onResume)中,当进入拍照界面后,PreviewActivity 随即被销毁,拍完照确认后回到 PreviewActivity 时,被销毁的 PreviewActivity 需要重建,又要走一遍 onCreate、onStart、onResume,又调用了启动相机拍照的代码,周而复始的进入了死循环状态。**为了避免让你的用户抓狂,果断明智的选择方案一。** 188 | 189 | 以上这种情况提到调用系统拍照时,Activity就回收的情况,在**小米4S**和**小米4 LTE**机子上**(MIUI的版本是7.3,Android系统版本是6.0)**出现的概率很高。 所以,建议看到此文的童鞋也可以去验证适配一下。 190 | 191 | ## 图片无法显示 192 | 193 | 图片无法显示这个问题也是略坑,如何坑法?往下看,同样是在**小米4S**和**小米4 LTE**机子上**(MIUI的版本是7.3,Android系统版本是6.0)**出现概率很高的场景(当然,不保证其他机子没出现过)。按照我们前面提到的业务场景,调用相机拍照完成后,我们的 app 会有一个预览图片的界面。但是在用了小米的机子进行拍照后,自己 app 的预览界面却怎么也无法显示出照片来,同样是相当郁闷,郁闷完后还是要一步一步去排查解决问题的!为此,需要一步一步猜测验证问题所在。 194 | 195 | - 猜测一:没有拿到照片路径,所以无法显示? 196 | 197 | 直接断点打 log 跟踪,猜测一很快被推翻,路径是有的。 198 | 199 | - 猜测二:Bitmap太大了,无法显示? 200 | 201 | 直接在 AS 的 log 控制台仔细的观察了一下系统 log ,发现了一些蛛丝马迹 202 | 203 | ![](http://g.hiphotos.baidu.com/image/pic/item/83025aafa40f4bfb05254687044f78f0f6361868.jpg) 204 | 205 | ``` 206 | 207 | OpenGLRenderer: Bitmap too large to be uploaded into a texture 208 | 209 | ``` 210 | 211 | 每次拍完照片,都会出现上面这样的 log ,果然,因为图片太大而导致在 ImageView 上无法显示。到这里有童鞋要吐槽了,没对图片的采样率 **inSampleSize** 做处理?天地良心啊,绝对做处理了,直接看代码: 212 | 213 | ```java 214 | 215 | /** 216 | * 压缩Bitmap的大小 217 | * 218 | * @param imagePath 图片文件路径 219 | * @param requestWidth 压缩到想要的宽度 220 | * @param requestHeight 压缩到想要的高度 221 | * @return 222 | */ 223 | public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { 224 | if (!TextUtils.isEmpty(imagePath)) { 225 | if (requestWidth <= 0 || requestHeight <= 0) { 226 | Bitmap bitmap = BitmapFactory.decodeFile(imagePath); 227 | return bitmap; 228 | } 229 | BitmapFactory.Options options = new BitmapFactory.Options(); 230 | options.inJustDecodeBounds = true;//不加载图片到内存,仅获得图片宽高 231 | BitmapFactory.decodeFile(imagePath, options); 232 | options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight); //计算获取新的采样率 233 | options.inJustDecodeBounds = false; 234 | return BitmapFactory.decodeFile(imagePath, options); 235 | 236 | } else { 237 | return null; 238 | } 239 | } 240 | 241 | public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { 242 | final int height = options.outHeight; 243 | final int width = options.outWidth; 244 | int inSampleSize = 1; 245 | Log.i(TAG, "height: " + height); 246 | Log.i(TAG, "width: " + width); 247 | if (height > reqHeight || width > reqWidth) { 248 | 249 | final int halfHeight = height / 2; 250 | final int halfWidth = width / 2; 251 | 252 | while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { 253 | inSampleSize *= 2; 254 | } 255 | 256 | long totalPixels = width * height / inSampleSize; 257 | 258 | final long totalReqPixelsCap = reqWidth * reqHeight * 2; 259 | 260 | while (totalPixels > totalReqPixelsCap) { 261 | inSampleSize *= 2; 262 | totalPixels /= 2; 263 | } 264 | } 265 | return inSampleSize; 266 | } 267 | 268 | ``` 269 | 270 | 瞄了代码后,是不是觉得没有问题了?没错,inSampleSize 确确实实经过处理,那为什么图片还是太大而显示不出来呢? requestWidth、int requestHeight 设置得太大导致 inSampleSize 太小了?不可能啊,我都试着把长宽都设置成 100 了还是没法显示!干脆,直接打印 inSampleSize 值,一打印,inSampleSize 值居然为 1 。 我去,彻底打脸了,明明说好的处理过了,居然还是 **1** !!!!为了一探究竟,干脆加 log 。 271 | 272 | ```java 273 | 274 | public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { 275 | if (!TextUtils.isEmpty(imagePath)) { 276 | Log.i(TAG, "requestWidth: " + requestWidth); 277 | Log.i(TAG, "requestHeight: " + requestHeight); 278 | if (requestWidth <= 0 || requestHeight <= 0) { 279 | Bitmap bitmap = BitmapFactory.decodeFile(imagePath); 280 | return bitmap; 281 | } 282 | BitmapFactory.Options options = new BitmapFactory.Options(); 283 | options.inJustDecodeBounds = true;//不加载图片到内存,仅获得图片宽高 284 | BitmapFactory.decodeFile(imagePath, options); 285 | Log.i(TAG, "original height: " + options.outHeight); 286 | Log.i(TAG, "original width: " + options.outWidth); 287 | options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight); //计算获取新的采样率 288 | Log.i(TAG, "inSampleSize: " + options.inSampleSize); 289 | options.inJustDecodeBounds = false; 290 | return BitmapFactory.decodeFile(imagePath, options); 291 | 292 | } else { 293 | return null; 294 | } 295 | } 296 | 297 | ``` 298 | 299 | 运行打印出来的日志如下: 300 | 301 | ![](http://g.hiphotos.baidu.com/image/pic/item/ca1349540923dd54f02d67bbd609b3de9d824883.jpg) 302 | 303 | 图片原来的宽高居然都是 -1 ,真是奇葩了!难怪,inSampleSize 经过处理之后结果还是 1 。狠狠的吐槽了之后,总是要回来解决问题的。那么,图片的宽高信息都丢失了,我去哪里找啊? 像下面这样? 304 | 305 | ```java 306 | 307 | public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { 308 | ... 309 | BitmapFactory.Options options = new BitmapFactory.Options(); 310 | options.inJustDecodeBounds = true;//不加载图片到内存,仅获得图片宽高 311 | Bitmap bitmap = BitmapFactory.decodeFile(imagePath, options); 312 | bitmap.getWidth(); 313 | bitmap.getHeight(); 314 | ... 315 | } else { 316 | return null; 317 | } 318 | } 319 | 320 | ``` 321 | 322 | no,此方案行不通,inJustDecodeBounds = true 时,BitmapFactory 获得 Bitmap 对象是 null;那要怎样才能获图片的宽高呢?前面提到的 ExifInterface 再次帮了我们大忙,通过它的下面两个属性即可拿到图片真正的宽高 323 | 324 | ![](http://e.hiphotos.baidu.com/image/pic/item/2e2eb9389b504fc2b97997d1e2dde71190ef6d3b.jpg) 325 | 326 | 顺手吐槽一下,为什么高不是 TAG_IMAGE_HEIGHT 而是 TAG_IMAGE_LENGTH。改良过后的代码实现如下: 327 | 328 | ```java 329 | 330 | public static Bitmap decodeBitmapFromFile(String imagePath, int requestWidth, int requestHeight) { 331 | if (!TextUtils.isEmpty(imagePath)) { 332 | Log.i(TAG, "requestWidth: " + requestWidth); 333 | Log.i(TAG, "requestHeight: " + requestHeight); 334 | if (requestWidth <= 0 || requestHeight <= 0) { 335 | Bitmap bitmap = BitmapFactory.decodeFile(imagePath); 336 | return bitmap; 337 | } 338 | BitmapFactory.Options options = new BitmapFactory.Options(); 339 | options.inJustDecodeBounds = true;//不加载图片到内存,仅获得图片宽高 340 | BitmapFactory.decodeFile(imagePath, options); 341 | Log.i(TAG, "original height: " + options.outHeight); 342 | Log.i(TAG, "original width: " + options.outWidth); 343 | if (options.outHeight == -1 || options.outWidth == -1) { 344 | try { 345 | ExifInterface exifInterface = new ExifInterface(imagePath); 346 | int height = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的高度 347 | int width = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, ExifInterface.ORIENTATION_NORMAL);//获取图片的宽度 348 | Log.i(TAG, "exif height: " + height); 349 | Log.i(TAG, "exif width: " + width); 350 | options.outWidth = width; 351 | options.outHeight = height; 352 | } catch (IOException e) { 353 | e.printStackTrace(); 354 | } 355 | } 356 | options.inSampleSize = calculateInSampleSize(options, requestWidth, requestHeight); //计算获取新的采样率 357 | Log.i(TAG, "inSampleSize: " + options.inSampleSize); 358 | options.inJustDecodeBounds = false; 359 | return BitmapFactory.decodeFile(imagePath, options); 360 | 361 | } else { 362 | return null; 363 | } 364 | } 365 | 366 | ``` 367 | 368 | 再看一下,打印出来的log 369 | 370 | ![](http://g.hiphotos.baidu.com/image/pic/item/4bed2e738bd4b31c71acb46f80d6277f9f2ff856.jpg) 371 | 372 | 这样就可以解决问题啦。 373 | 374 | ## 总结 375 | 376 | 以上总结了这么些身边童鞋经常问起,但网上又不多见的适配问题,希望可以帮到一些开发童鞋少走弯路。文中多次提到小米的机子,并不代表只有MIUI上有这样的问题存在,仅仅只是因为我身边带的几部机子大都是小米的。对待适配问题,在搜索引擎都无法提供多少有效的信息时,我们只能靠断点、打log、观察控制台的日志、以及API文档来寻找一些蛛丝马迹作为突破口,相信办法总比困难多。 377 | 378 | 以上的示例代码已经整理到:https://github.com/D-clock/AndroidStudyCode ,主要的代码在下面红圈部分 379 | 380 | ![](http://a.hiphotos.baidu.com/image/pic/item/9c16fdfaaf51f3ded4b6a6b093eef01f3b2979d9.jpg) 381 | 382 | 感兴趣的童鞋可以自行查看!如有错误,欢迎大家指正! -------------------------------------------------------------------------------- /notes/深入理解Android中的Matrix.md: -------------------------------------------------------------------------------- 1 | # 深入理解 Android 中的 Matrix 2 | 3 | 在 Android 开发中,矩阵是一个功能强大并且应用广泛的神器,例如:用它来制作动画效果、改变图片大小、给图片加各类滤镜等。对于矩阵,Android 官方 SDK 为我们提供了一个强大的类 Matrix (还有 ColorMatrix )是一直困扰着我的问题,虽然大致能够调用相应的 API ,但却一直 get 不到其内在的梗。但是出来混总是别想着蒙混过关的,所以最近重新操起一年毕业的线性代数,再本着小事问老婆,大事问Google的心态,终于把多年不解的问题给破了。出于好记性不如烂笔头的原因,便有了本文。在此先感谢下面两篇令我茅舍顿开的文章: 4 | 5 | - [齐次坐标系入门级思考](https://oncemore2020.github.io/blog/homogeneous/) 6 | - [仿射变换与齐次坐标](https://guangchun.wordpress.com/2011/10/12/affineandhomogeneous/) 7 | 8 | 读完本文,相信你能够搞明白以下三个问题: 9 | 10 | - 为什么 Matrix 是个 3 X 3 的矩阵 11 | - Matrix 这个 3 X 3 的矩阵每个元素的作用 12 | - Matrix 的 setXXX、preXXX、postXXX API 方法的工作原理 13 | 14 | ## Matrix 的结构 15 | 16 | Matrix 是 Android SDK 提供的一个矩阵类,它代表一个 3 X 3 的矩阵(不懂矩阵为何物的童鞋就要自行 Google 了)。 Matrix 提供了让我们获得 Matrix 值的 API —— **getValues** 17 | 18 | ![](http://h.hiphotos.baidu.com/image/pic/item/43a7d933c895d1438a9f6d507bf082025baf07d3.jpg) 19 | 20 | 利用此 API 传入一个长度为 9 的 float 数组,即可获得矩阵中每个元素的值。那么这 9 个浮点数的作用和意义是什么呢,从 Android 官方文档上看,它为这个数组中的每一个元素都定义了一个下标常量 21 | 22 | ![](http://c.hiphotos.baidu.com/image/pic/item/f3d3572c11dfa9ece819dc646ad0f703918fc18a.jpg) 23 | 24 | 这个 9 个常量取值分别是 0 - 8 25 | 26 | ![](http://c.hiphotos.baidu.com/image/pic/item/908fa0ec08fa513d4cd5e6ad356d55fbb2fbd942.jpg) 27 | 28 | 如果我们将这个 float 排成直观的矩阵格式,那它将是下面这样子的 29 | 30 | ![](http://d.hiphotos.baidu.com/image/pic/item/2934349b033b5bb5425dd68d3ed3d539b600bc3f.jpg) 31 | 32 | 实际上我们平常利用 Matrix 来进行 Translate(平移)、Scale(缩放)、Rotate(旋转)的操作,就是在操作着这个矩阵中元素的数值来达到我们想要的效果。但是现在问题来了,上面提到的平移、缩放、旋转操作中,旋转和缩放可以用乘法表示,而平移就只能用加法表示,而且 Matrix 是一个 3 X 3 的矩阵,实际上表示这些操作 2 X 2 的矩阵足矣! 33 | 34 | ![](http://e.hiphotos.baidu.com/image/pic/item/7acb0a46f21fbe096feccf6063600c338744ad17.jpg) 35 | 36 | ![](http://h.hiphotos.baidu.com/image/pic/item/bf096b63f6246b600bbcbe00e3f81a4c510fa217.jpg) 37 | 38 | ![](http://c.hiphotos.baidu.com/image/pic/item/d058ccbf6c81800a737a857ab93533fa828b4722.jpg) 39 | 40 | 如上,可以依次看到平移、缩放、旋转的矩阵,其中 41 | 42 | - (x',y')表示执行操作后的点的坐标,(x,y)表示执行操作前的点的坐标 43 | - tx、ty 分别表示x轴、y轴上平移的距离,Sx、Sy 分别表示x轴、y轴上的缩放比例 44 | - θ 则表示旋转角度 45 | 46 | 至于上面矩阵的推导过程,网络上很多,这里就不去赘述了。以前到了这里,我就会很纳闷,为什么 2 X 2 矩阵能干的事情,偏偏要用 3 X 3 矩阵去做,直到遇到前面提到的两篇文章才有所领悟。 47 | 48 | 其实在计算机图形应用涉及到几何变换,主要包括平移、旋转、缩放。以矩阵表达式来计算这些变换时,平移是矩阵相加,旋转和缩放则是矩阵相乘。那些数学大神们为了方便计算,所以引入了一样神器叫做**齐次坐标**(不懂的童鞋,老规矩自行搜索),将平移的加法合并用乘法表示。所以,2 X 2 的矩阵经过一番变换后,成了下面这样的。 49 | 50 | ![](http://h.hiphotos.baidu.com/image/pic/item/5d6034a85edf8db184610e100123dd54564e7472.jpg) 51 | 52 | ![](http://b.hiphotos.baidu.com/image/pic/item/c83d70cf3bc79f3d89569944b2a1cd11728b290a.jpg) 53 | 54 | ![](http://b.hiphotos.baidu.com/image/pic/item/5fdf8db1cb13495458519a105e4e9258d1094a72.jpg) 55 | 56 | 至此,我们可以得知**为什么 Matrix 是一个 3 X 3 的矩阵**,其实 2 X 2 的矩阵是足以表示的,不过是为了方便计算而合并写成了 3 X 3 的格式。 57 | 58 | ## Matrix 元素的作用 59 | 60 | 一个 Matrix 共有 9 个元素,那么它每个元素的值发生改变会起到什么作用呢?按照前面所示的齐次坐标转换得到 3 X 3 的矩阵和 Android 文档提供的官方结构相对应,我们不难看出下面的对应关系(其实从 Matrix 中每个位置的常量命名也可以看出来): 61 | 62 | ![](http://f.hiphotos.baidu.com/image/pic/item/aec379310a55b3193603a1034ba98226cefc17c9.jpg) 63 | 64 | ![](http://e.hiphotos.baidu.com/image/pic/item/562c11dfa9ec8a136f837496ff03918fa0ecc02e.jpg) 65 | 66 | ![](http://b.hiphotos.baidu.com/image/pic/item/aa64034f78f0f73676046f770255b319eac413c9.jpg) 67 | 68 | 从这我们可以看出这个 Matrix 结构中的每个参数发挥着如下作用: 69 | 70 | - MTRANS_X、MTRANS_Y 同时控制着 Translate 71 | - MSCALE_X、MSCALE_Y 同时控制着 Scale 72 | - MSCALE_X、MSKEW_X、MSCALE_Y、MSKEW_Y 同时控制着 Rotate 73 | - 从名称上看,我们可以顺带看出 MSKEW_X、MSKEW_Y 同时控制着 Skew 74 | 75 | 如果要进行代码验证的话,也非常简单,例如直接只对 Matrix 做 Translate 的 API 调用操作,再将 Matrix 的信息打印到控制台,你会发现整个 Matrix 确实只有 MTRANS_X、MTRANS_Y 两个位置的数字在发生变化。其他 Scale、Rotate、Skew 操作也是一样,感兴趣的童鞋可以自行代码验证一番。 76 | 77 | 至此,我们可以大致弄清矩阵每个元素的作用。至于 MPERSP_0、MPERSP_1、MPERSP_2 这三个参数,目前暂时不得而知,网上有文章说这三个参数控制着透视变换,但是文档和 API 上都没怎么提及,所以还是有待验证研究的,有明白的童鞋不妨留言赐教一下,不胜感激。 78 | 79 | ## 理解 Matrix API 调用 80 | 81 | 按照第一小节里面通过齐次坐标转换而来的矩阵方程可以知道,假设一根线执行了平移操作,相当于线上每个点的坐标被下方的矩阵左乘。(缩放和旋转操作也是同理) 82 | 83 | ![](http://f.hiphotos.baidu.com/image/pic/item/5882b2b7d0a20cf49c1f77a87e094b36acaf993e.jpg) 84 | 85 | 如果要进行同时缩放、平移之类的符合变化操作,也无非就是选取相应的矩阵做左乘操作。为了加深矩阵变换对应 Matrix API 调用的理解,直接通过下面的一个自定义的动画效果和代码来讲解好了。 86 | 87 | ```java 88 | 89 | public class SimpleCustomAnimation extends Animation { 90 | 91 | private int mWidth, mHeight; 92 | 93 | @Override 94 | public void initialize(int width, int height, int parentWidth, int parentHeight) { 95 | super.initialize(width, height, parentWidth, parentHeight); 96 | this.mWidth = width; 97 | this.mHeight = height; 98 | } 99 | 100 | @Override 101 | protected void applyTransformation(float interpolatedTime, Transformation t) { 102 | Matrix matrix = t.getMatrix(); 103 | matrix.preScale(interpolatedTime, interpolatedTime);//缩放 104 | matrix.preRotate(interpolatedTime * 360);//旋转 105 | //下面的Translate组合是为了将缩放和旋转的基点移动到整个View的中心,不然系统默认是以View的左上角作为基点 106 | matrix.preTranslate(-mWidth / 2, -mHeight / 2); 107 | matrix.postTranslate(mWidth / 2, mHeight / 2); 108 | } 109 | } 110 | 111 | ``` 112 | 113 | 熟悉动画这块的童鞋肯定知道,Animation 就是通过不断改变 applyTransformation 函数传入的 Matrix 来实现各种各样的动画效果的,通过上面 applyTransformation 寥寥的几行 Matrix 的复合变换操作可以得到如下效果 114 | 115 | ![](http://f.hiphotos.baidu.com/image/pic/item/9358d109b3de9c82048c06f86481800a18d843d8.jpg) 116 | 117 | 实际上这几行代码用矩阵来表示就相当于如下所示: 118 | 119 | ![](http://a.hiphotos.baidu.com/image/pic/item/a686c9177f3e67097989758633c79f3df8dc5539.jpg) 120 | 121 | 关于代码的作用上边已经给出了注释,这里就不多写了。主要还是要弄明白 Matrix 复合变换中 pre 、 post 等操作与其对应的矩阵发生的左乘、右乘变化。 122 | 123 | ## 总结 124 | 125 | 到此,整篇文章已经完结,相信已经能够让你明白开头提到的三个问题。其实我们也可以发现,Google 封装了 Matrix 已经是很完美了,几乎屏蔽了所有的数学细节,使得我这种数学水平一般的开发者也能够去调用相应的 API 实现一些简单的效果。虽然被封装得很完美,但掌握相应的一些原理,依旧可以帮你更好的理解一些技术实现,此次加深了对 Matrix 一些操作的理解,帮我自己解决了以前不少的困惑,不知道有没有帮你 get 到一些什么呢? 126 | 127 | 上面给的示例代码很简单,复制黏贴即可运行玩耍,实在需要直接运行源码的童鞋就到 https://github.com/D-clock/AndroidStudyCode 找吧! 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /notes/知乎和简书的夜间模式实现套路.md: -------------------------------------------------------------------------------- 1 | # 知乎和简书的夜间模式实现套路 2 | 3 | Hello,大家好,我是Clock。今天要写的这篇文章主题是关于夜间模式的实现套路。本来这篇文章是上周要写的,结果因为上周末有其他事情,所以拖到这个周末才完成。曾经和薇薇(钛媒体漂亮的程序媛)聊过夜间模式实现的问题,当时薇薇酱负责钛媒体客户端的重构工作,有个夜间模式功能在考虑要不要用 Android M 新加的夜间模式特性。凭借稍微有点点老司机的经验,我直接说了 NO。按照以往的套路,通常新出的功能都会有坑,或者向下兼容性的问题。自己弄弄 Demo 玩玩是可以的,但是引入企业开发还是谨慎点,说白了就是先等等,让大家把坑填完了再用。果然,Android M 发正式版的时候,预览版里面的夜间模式功能被暂时移除了(哈哈哈哈,机智如我,最新发布的 Android N 正式版已经有夜间模式了,大家可以去玩玩)。 4 | 5 | ## 前言 6 | 7 | 好了,回归正题,说回夜间模式。在网上看到很多童鞋都说用什么什么框架来实现这个功能,然后仔细去看一下各个推荐的框架,发现其实都是动态换肤的,动态换肤可比夜间模式要复杂多了,未免大材小用了。说实话,我一直没用什么好思路,虽然网上有童鞋提供了一种思路是通过 setTheme 然后再 recreate Activity 的方式,但是这样带来的问题是非常多的,看起来就相当不科学(为什么不科学,后文会说)。**于是,直接想到了去逆向分析那些夜间模式做得好的应用的源代码,学习他们的实现套路。所以,本文的实现思路来自于编写这些应用的夜间模式功能的童鞋,先在这里向他们表示感谢。**我的手机里面使用高频的应用不少,其中简书和知乎是属于夜间模式做得相当 nice 的。先给两个效果图大家对比感受下 8 | 9 | 10 | | 简书 | 知乎 | 11 | | ----------------- | ------------------ | 12 | | ![](http://diycode.b0.upaiyun.com/photo/2016/eeb88e2a8812262c25e220479f05a3a3.gif)| ![](http://diycode.b0.upaiyun.com/photo/2016/59c44219315c5ea097afdc75e5563b87.gif)| 13 | 14 | 如果大家仔细观察,肯定会发现,知乎的切换效果更漂亮些,因为它有一个渐变的效果。那么它们的夜间模式到底是如何实现的呢?别急接着往下看,你也可以。 15 | 16 | ## 实现套路 17 | 18 | 这里先展示一下我的实现效果吧 19 | 20 | | 简书实现效果 | 知乎实现效果 | 21 | | ----------------- | ------------------ | 22 | | ![](http://diycode.b0.upaiyun.com/photo/2016/9886ffe716e2191fda98658a0446a4ca.gif)| ![](http://diycode.b0.upaiyun.com/photo/2016/a7a5cdfd844c0982410ef10d156dfff4.gif)| 23 | 24 | 此处分为两个部分,一部分是 xml 文件中要干的活,一部分是 Java 代码要实现的活,先说 xml 吧。 25 | 26 | ### XML 配置 27 | 28 | 首先,先写一套UI界面出来,上方左边是两个 TextView,右边是两个 CheckBox,下方是一个 RecyclerView ,实现很简单,这里我不贴代码了。 29 | 30 | ![](http://diycode.b0.upaiyun.com/photo/2016/5d65ddae827097a0fe7097ac51f512d1.png) 31 | 32 | 接着,在 styles 文件中添加两个 Theme,一个是日间主题,一个是夜间主题。它们的属性都是一样的,唯一区别在于颜色效果不同。 33 | 34 | ```xml 35 | 36 | 37 | 44 | 45 | 46 | 53 | 54 | ``` 55 | 56 | 需要注意的是,上面的 clockTextColor 和 clockBackground 是我自定义的 color 类型属性 57 | 58 | ```xml 59 | 60 | 61 | 62 | 63 | 64 | ``` 65 | 66 | 然后再到所有需要实现夜间模式功能的 xml 布局文件中,加入类似下面设置,比如我在 RecyclerView 的 Item 布局文件中做了如下设置 67 | 68 | ![](http://diycode.b0.upaiyun.com/photo/2016/0733c4fc7fcffc15ccf1d6bdb2ae366a.png) 69 | 70 | 稍稍解释下其作用,如 TextView 里的 android:textColor="?attr/clockTextColor" 是让其字体颜色跟随所设置的 Theme。到这里,xml 需要做的配置全部完成,接下来是 Java 代码实现了。 71 | 72 | ### Java 代码实现 73 | 74 | 大家可以先看下面的实现代码,看不懂的童鞋可以边结合我代码下方实现思路解说。 75 | 76 | ```java 77 | 78 | package com.clock.study.activity; 79 | 80 | import ... 81 | 82 | /** 83 | * 夜间模式实现方案 84 | * 85 | * @author Clock 86 | * @since 2016-08-11 87 | */ 88 | public class DayNightActivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener { 89 | 90 | private final static String TAG = DayNightActivity.class.getSimpleName(); 91 | /**用于将主题设置保存到SharePreferences的工具类**/ 92 | private DayNightHelper mDayNightHelper; 93 | 94 | private RecyclerView mRecyclerView; 95 | 96 | private LinearLayout mHeaderLayout; 97 | private List mLayoutList; 98 | private List mTextViewList; 99 | private List mCheckBoxList; 100 | 101 | @Override 102 | protected void onCreate(Bundle savedInstanceState) { 103 | super.onCreate(savedInstanceState); 104 | supportRequestWindowFeature(Window.FEATURE_NO_TITLE); 105 | initData(); 106 | initTheme(); 107 | setContentView(R.layout.activity_day_night); 108 | initView(); 109 | } 110 | 111 | private void initView() { 112 | mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view); 113 | RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); 114 | mRecyclerView.setLayoutManager(layoutManager); 115 | mRecyclerView.setAdapter(new SimpleAuthorAdapter()); 116 | 117 | mHeaderLayout = (LinearLayout) findViewById(R.id.header_layout); 118 | 119 | mLayoutList = new ArrayList<>(); 120 | mLayoutList.add((RelativeLayout) findViewById(R.id.jianshu_layout)); 121 | mLayoutList.add((RelativeLayout) findViewById(R.id.zhihu_layout)); 122 | 123 | mTextViewList = new ArrayList<>(); 124 | mTextViewList.add((TextView) findViewById(R.id.tv_jianshu)); 125 | mTextViewList.add((TextView) findViewById(R.id.tv_zhihu)); 126 | 127 | mCheckBoxList = new ArrayList<>(); 128 | CheckBox ckbJianshu = (CheckBox) findViewById(R.id.ckb_jianshu); 129 | ckbJianshu.setOnCheckedChangeListener(this); 130 | mCheckBoxList.add(ckbJianshu); 131 | CheckBox ckbZhihu = (CheckBox) findViewById(R.id.ckb_zhihu); 132 | ckbZhihu.setOnCheckedChangeListener(this); 133 | mCheckBoxList.add(ckbZhihu); 134 | 135 | } 136 | 137 | @Override 138 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 139 | int viewId = buttonView.getId(); 140 | if (viewId == R.id.ckb_jianshu) { 141 | changeThemeByJianShu(); 142 | 143 | } else if (viewId == R.id.ckb_zhihu) { 144 | changeThemeByZhiHu(); 145 | 146 | } 147 | } 148 | 149 | private void initData() { 150 | mDayNightHelper = new DayNightHelper(this); 151 | } 152 | 153 | private void initTheme() { 154 | if (mDayNightHelper.isDay()) { 155 | setTheme(R.style.DayTheme); 156 | } else { 157 | setTheme(R.style.NightTheme); 158 | } 159 | } 160 | 161 | /** 162 | * 切换主题设置 163 | */ 164 | private void toggleThemeSetting() { 165 | if (mDayNightHelper.isDay()) { 166 | mDayNightHelper.setMode(DayNight.NIGHT); 167 | setTheme(R.style.NightTheme); 168 | } else { 169 | mDayNightHelper.setMode(DayNight.DAY); 170 | setTheme(R.style.DayTheme); 171 | } 172 | } 173 | 174 | /** 175 | * 使用简书的实现套路来切换夜间主题 176 | */ 177 | private void changeThemeByJianShu() { 178 | toggleThemeSetting(); 179 | refreshUI(); 180 | } 181 | 182 | /** 183 | * 使用知乎的实现套路来切换夜间主题 184 | */ 185 | private void changeThemeByZhiHu() { 186 | showAnimation(); 187 | toggleThemeSetting(); 188 | refreshUI(); 189 | } 190 | 191 | /** 192 | * 刷新UI界面 193 | */ 194 | private void refreshUI() { 195 | TypedValue background = new TypedValue();//背景色 196 | TypedValue textColor = new TypedValue();//字体颜色 197 | Resources.Theme theme = getTheme(); 198 | theme.resolveAttribute(R.attr.clockBackground, background, true); 199 | theme.resolveAttribute(R.attr.clockTextColor, textColor, true); 200 | 201 | mHeaderLayout.setBackgroundResource(background.resourceId); 202 | for (RelativeLayout layout : mLayoutList) { 203 | layout.setBackgroundResource(background.resourceId); 204 | } 205 | for (CheckBox checkBox : mCheckBoxList) { 206 | checkBox.setBackgroundResource(background.resourceId); 207 | } 208 | for (TextView textView : mTextViewList) { 209 | textView.setBackgroundResource(background.resourceId); 210 | } 211 | 212 | Resources resources = getResources(); 213 | for (TextView textView : mTextViewList) { 214 | textView.setTextColor(resources.getColor(textColor.resourceId)); 215 | } 216 | 217 | int childCount = mRecyclerView.getChildCount(); 218 | for (int childIndex = 0; childIndex < childCount; childIndex++) { 219 | ViewGroup childView = (ViewGroup) mRecyclerView.getChildAt(childIndex); 220 | childView.setBackgroundResource(background.resourceId); 221 | View infoLayout = childView.findViewById(R.id.info_layout); 222 | infoLayout.setBackgroundResource(background.resourceId); 223 | TextView nickName = (TextView) childView.findViewById(R.id.tv_nickname); 224 | nickName.setBackgroundResource(background.resourceId); 225 | nickName.setTextColor(resources.getColor(textColor.resourceId)); 226 | TextView motto = (TextView) childView.findViewById(R.id.tv_motto); 227 | motto.setBackgroundResource(background.resourceId); 228 | motto.setTextColor(resources.getColor(textColor.resourceId)); 229 | } 230 | 231 | //让 RecyclerView 缓存在 Pool 中的 Item 失效 232 | //那么,如果是ListView,要怎么做呢?这里的思路是通过反射拿到 AbsListView 类中的 RecycleBin 对象,然后同样再用反射去调用 clear 方法 233 | Class recyclerViewClass = RecyclerView.class; 234 | try { 235 | Field declaredField = recyclerViewClass.getDeclaredField("mRecycler"); 236 | declaredField.setAccessible(true); 237 | Method declaredMethod = Class.forName(RecyclerView.Recycler.class.getName()).getDeclaredMethod("clear", (Class[]) new Class[0]); 238 | declaredMethod.setAccessible(true); 239 | declaredMethod.invoke(declaredField.get(mRecyclerView), new Object[0]); 240 | RecyclerView.RecycledViewPool recycledViewPool = mRecyclerView.getRecycledViewPool(); 241 | recycledViewPool.clear(); 242 | 243 | } catch (NoSuchFieldException e) { 244 | e.printStackTrace(); 245 | } catch (ClassNotFoundException e) { 246 | e.printStackTrace(); 247 | } catch (NoSuchMethodException e) { 248 | e.printStackTrace(); 249 | } catch (InvocationTargetException e) { 250 | e.printStackTrace(); 251 | } catch (IllegalAccessException e) { 252 | e.printStackTrace(); 253 | } 254 | 255 | refreshStatusBar(); 256 | } 257 | 258 | /** 259 | * 刷新 StatusBar 260 | */ 261 | private void refreshStatusBar() { 262 | if (Build.VERSION.SDK_INT >= 21) { 263 | TypedValue typedValue = new TypedValue(); 264 | Resources.Theme theme = getTheme(); 265 | theme.resolveAttribute(R.attr.colorPrimary, typedValue, true); 266 | getWindow().setStatusBarColor(getResources().getColor(typedValue.resourceId)); 267 | } 268 | } 269 | 270 | /** 271 | * 展示一个切换动画 272 | */ 273 | private void showAnimation() { 274 | final View decorView = getWindow().getDecorView(); 275 | Bitmap cacheBitmap = getCacheBitmapFromView(decorView); 276 | if (decorView instanceof ViewGroup && cacheBitmap != null) { 277 | final View view = new View(this); 278 | view.setBackgroundDrawable(new BitmapDrawable(getResources(), cacheBitmap)); 279 | ViewGroup.LayoutParams layoutParam = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 280 | ViewGroup.LayoutParams.MATCH_PARENT); 281 | ((ViewGroup) decorView).addView(view, layoutParam); 282 | ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f); 283 | objectAnimator.setDuration(300); 284 | objectAnimator.addListener(new AnimatorListenerAdapter() { 285 | @Override 286 | public void onAnimationEnd(Animator animation) { 287 | super.onAnimationEnd(animation); 288 | ((ViewGroup) decorView).removeView(view); 289 | } 290 | }); 291 | objectAnimator.start(); 292 | } 293 | } 294 | 295 | /** 296 | * 获取一个 View 的缓存视图 297 | * 298 | * @param view 299 | * @return 300 | */ 301 | private Bitmap getCacheBitmapFromView(View view) { 302 | final boolean drawingCacheEnabled = true; 303 | view.setDrawingCacheEnabled(drawingCacheEnabled); 304 | view.buildDrawingCache(drawingCacheEnabled); 305 | final Bitmap drawingCache = view.getDrawingCache(); 306 | Bitmap bitmap; 307 | if (drawingCache != null) { 308 | bitmap = Bitmap.createBitmap(drawingCache); 309 | view.setDrawingCacheEnabled(false); 310 | } else { 311 | bitmap = null; 312 | } 313 | return bitmap; 314 | } 315 | } 316 | 317 | 318 | ``` 319 | 320 | 实现思路和代码解说: 321 | 322 | 1. DayNightHelper 类是用于保存夜间模式设置到 SharePreferences 的工具类,在 initData 函数中被初始化,其他的 View 和 Layout 都是界面布局,在 initView 函数中被初始化; 323 | 2. 在 Activity 的 onCreate 函数调用 setContentView 之前,需要先去 setTheme,因为当 View 创建成功后 ,再去 setTheme 是无法对 View 的 UI 效果产生影响的; 324 | 3. onCheckedChanged 用于监听日间模式和夜间模式的切换操作; 325 | 4. **refreshUI 是本实现的关键函数**,起着切换效果的作用,通过 TypedValue 和 Theme.resolveAttribute 在代码中获取 Theme 中设置的颜色,来重新设置控件的背景色或者字体颜色等等。**需要特别注意的是 RecyclerView 和 ListView 这种比较特殊的控件处理方式,代码注释中已经说明,大家可以看代码中注释**; 326 | 5. refreshStatusBar 用于刷新顶部通知栏位置的颜色; 327 | 6. **showAnimation 和 getCacheBitmapFromView 同样是本实现的关键函数**,getCacheBitmapFromView 用于将 View 中的内容转换成 Bitmap(类似于截屏操作那样),showAnimation 是用于展示一个渐隐效果的属性动画,这个属性作用在哪个对象上呢?是一个 View ,一个在代码中动态填充到 DecorView 中的 View(不知道 DecorView 的童鞋得回去看看 Android Window 相关的知识)。**知乎之所以在夜间模式切换过程中会有渐隐效果,是因为在切换前进行了截屏,同时将截屏拿到的 Bitmap 设置到动态填充到 DecorView 中的 View 上,并对这个 View 执行一个渐隐的属性动画,所以使得我们能够看到一个漂亮的渐隐过渡的动画效果。而且在动画结束的时候再把这个动态添加的 View 给 remove 了,避免了 Bitmap 造成内存飙升问题。**对待知乎客户端开发者这种处理方式,我必须双手点赞外加一个大写的服。 328 | 329 | 到这里,实现套路基本说完了,简书和知乎的实现套路如上所述,区别就是知乎多了个截屏和渐隐过渡动画效果而已。 330 | 331 | ## 一些思考 332 | 333 | 整理逆向分析的过程,也对夜间模式的实现有了不少思考,希望与各位童鞋们探讨分享。 334 | 335 | > 最初步的逆向分析过程就发现了,知乎和简书并没有引入任何第三方框架来实现夜间模式,为什么呢? 336 | 337 | 因为我看到的大部分都实现夜间模式的思路都是用开源的换肤框架,或多或少存在着些 BUG。简书和知乎不用可能是出于框架不稳定性,以及我前面提到的用换肤框架来实现夜间模式大材小用吧。(我也只是瞎猜,哈哈哈) 338 | 339 | > 前面我提到,通过 setTheme 然后再去 Activity recreate 的方案不可行,为什么呢? 340 | 341 | 我认为不可行的原因有两点,一个是 Activity recreate 会有闪烁效果体验不加,二是 Activity recreate 涉及到状态状态保存问题,如自身的状态保存,如果 Activity 中包含着多个 Fragment ,那就更加头疼了。 342 | 343 | > 知乎和简书设置夜间模式的位置,有点巧妙,巧妙在哪? 344 | 345 | 知乎和简书出发夜间模式切换的地方,都是在 MainActivity 的一个 Fragment 中。也就是说,如果你要切换模式时,必须回到主界面,此时只存在主界面一个 Activity,只需要遍历主界面更新控件色调即可。而对于其他设置夜间模式后新建的 Activity ,只需要在 setContentView 之前做一下判断并 setTheme 即可。 346 | 347 | ## 总结 348 | 349 | 关于简书和知乎夜间模式功能实现的套路就讲解到这里,**整个实现套路都是我通过逆向分析简书和知乎的代码取得,这里再一次向实现这些代码的童鞋以示感谢。**当然,上面的代码我是经过精简提炼过的,在原先简书和知乎客户端中的实现代码还做了相应的抽象设计和递归遍历等等,这里是为了方便讲解而做了精简。如果有童鞋喜欢这种实现套路,也可以自己加以抽象封装。这里也推荐各位童鞋一个我常用的思路,就是当你对一个功能没有思路时,大可找一些实现了这类功能的优秀应用进行逆向代码分析。需要实现代码的童鞋,可以访问: 350 | 351 | > https://github.com/D-clock/AndroidStudyCode 352 | 353 | **也欢迎大家关注我的[简书](http://www.jianshu.com/users/ec95b5891948/latest_articles)和[Github](https://github.com/D-clock)。** -------------------------------------------------------------------------------- /proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Android-Studio\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /src/androidTest/java/com/clock/study/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.clock.study; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 28 | 32 | 35 | 38 | 41 | 44 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/StudyApplication.java: -------------------------------------------------------------------------------- 1 | package com.clock.study; 2 | 3 | import android.app.Application; 4 | 5 | import com.clock.study.manager.FolderManager; 6 | import com.clock.utils.crash.CrashExceptionHandler; 7 | 8 | /** 9 | * Created by Clock on 2016/5/13. 10 | */ 11 | public class StudyApplication extends Application { 12 | 13 | @Override 14 | public void onCreate() { 15 | super.onCreate(); 16 | 17 | configCollectCrashInfo(); 18 | } 19 | 20 | /** 21 | * 配置奔溃信息的搜集 22 | */ 23 | private void configCollectCrashInfo() { 24 | CrashExceptionHandler crashExceptionHandler = new CrashExceptionHandler(this, FolderManager.getCrashLogFolder()); 25 | Thread.setDefaultUncaughtExceptionHandler(crashExceptionHandler); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/activity/AnimationActivity.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.activity; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.View; 6 | import android.view.animation.AlphaAnimation; 7 | import android.view.animation.Animation; 8 | import android.view.animation.AnimationSet; 9 | import android.view.animation.RotateAnimation; 10 | import android.view.animation.ScaleAnimation; 11 | import android.view.animation.TranslateAnimation; 12 | import android.widget.Button; 13 | 14 | import com.clock.study.R; 15 | import com.clock.study.animation.SimpleCustomAnimation; 16 | 17 | /** 18 | * Android动画效果实现复习 19 | */ 20 | public class AnimationActivity extends AppCompatActivity implements View.OnClickListener { 21 | 22 | private Button mBtnTranslate; 23 | private Button mBtnScale; 24 | private Button mBtnRotate; 25 | private Button mBtnAlpha; 26 | private Button mBtnSet; 27 | private Button mBtnSimpleCustom; 28 | 29 | @Override 30 | protected void onCreate(Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | setContentView(R.layout.activity_android_anim); 33 | 34 | mBtnTranslate = (Button) findViewById(R.id.btn_translate); 35 | mBtnTranslate.setOnClickListener(this); 36 | 37 | mBtnScale = (Button) findViewById(R.id.btn_scale); 38 | mBtnScale.setOnClickListener(this); 39 | 40 | mBtnRotate = (Button) findViewById(R.id.btn_rotate); 41 | mBtnRotate.setOnClickListener(this); 42 | 43 | mBtnAlpha = (Button) findViewById(R.id.btn_alpha); 44 | mBtnAlpha.setOnClickListener(this); 45 | 46 | mBtnSet = (Button) findViewById(R.id.btn_set); 47 | mBtnSet.setOnClickListener(this); 48 | 49 | mBtnSimpleCustom = (Button) findViewById(R.id.btn_simple_custom_anim); 50 | mBtnSimpleCustom.setOnClickListener(this); 51 | 52 | } 53 | 54 | @Override 55 | public void onClick(View v) { 56 | int viewId = v.getId(); 57 | if (viewId == R.id.btn_translate) {//偏移动画 58 | //TranslateAnimation translateAnim = new TranslateAnimation(0, 500, 0, 500); 59 | /*TranslateAnimation translateAnim = new TranslateAnimation(Animation.ABSOLUTE, 0, Animation.ABSOLUTE, 500, Animation.ABSOLUTE, 0, 60 | Animation.ABSOLUTE, 500);*/ 61 | TranslateAnimation translateAnim = new TranslateAnimation(Animation.RELATIVE_TO_PARENT, 0, Animation.RELATIVE_TO_PARENT, 1.0f, Animation.RELATIVE_TO_PARENT, 0, 62 | Animation.RELATIVE_TO_PARENT, 1.0f); 63 | translateAnim.setDuration(2000); 64 | mBtnTranslate.startAnimation(translateAnim); 65 | //translateAnim.setFillAfter(true);//保持动画效果 66 | 67 | } else if (viewId == R.id.btn_scale) {//缩放动画 68 | 69 | //ScaleAnimation scaleAnim = new ScaleAnimation(0.5f, 1, 0.5f, 1); 70 | //ScaleAnimation scaleAnim = new ScaleAnimation(0.5f, 1, 0.5f, 1, 300, 300); 71 | ScaleAnimation scaleAnim = new ScaleAnimation(0.5f, 1, 0.5f, 1, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); 72 | scaleAnim.setDuration(1000); 73 | mBtnScale.startAnimation(scaleAnim); 74 | 75 | } else if (viewId == R.id.btn_rotate) {//旋转动画 76 | 77 | //RotateAnimation rotateAnim = new RotateAnimation(0, 360); 78 | //RotateAnimation rotateAnim = new RotateAnimation(0, 360, 100, 100); 79 | RotateAnimation rotateAnim = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); 80 | rotateAnim.setDuration(2000); 81 | mBtnRotate.startAnimation(rotateAnim); 82 | 83 | } else if (viewId == R.id.btn_alpha) {//透明度动画 84 | 85 | AlphaAnimation alphaAnim = new AlphaAnimation(0, 1); 86 | alphaAnim.setDuration(2000); 87 | mBtnAlpha.startAnimation(alphaAnim); 88 | 89 | } else if (viewId == R.id.btn_set) {//动画合集 90 | 91 | AnimationSet animSet = new AnimationSet(true); 92 | animSet.setDuration(2000); 93 | RotateAnimation rotateAnim = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); 94 | ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); 95 | animSet.addAnimation(rotateAnim); 96 | animSet.addAnimation(scaleAnimation); 97 | AlphaAnimation alphaAnim = new AlphaAnimation(0, 1); 98 | animSet.addAnimation(alphaAnim); 99 | mBtnSet.startAnimation(animSet); 100 | 101 | } else if (viewId == R.id.btn_simple_custom_anim) { 102 | 103 | SimpleCustomAnimation simpleCustomAnim = new SimpleCustomAnimation();//简单的自定义动画效果 104 | simpleCustomAnim.setDuration(1000); 105 | mBtnSimpleCustom.startAnimation(simpleCustomAnim); 106 | 107 | } 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/activity/AnimationTestActivity.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.activity; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.view.animation.Animation; 8 | import android.widget.BaseAdapter; 9 | import android.widget.ListView; 10 | 11 | import com.clock.study.R; 12 | import com.clock.study.animation.SimpleCustomAnimation; 13 | 14 | /** 15 | * 测试Animation动画效果 16 | */ 17 | public class AnimationTestActivity extends AppCompatActivity implements View.OnClickListener { 18 | 19 | private ListView mTestListView; 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_animation_test); 25 | 26 | mTestListView = (ListView) findViewById(R.id.list_view_test); 27 | mTestListView.setAdapter(new SimpleTestListAdapter()); 28 | 29 | findViewById(R.id.btn_test_anim).setOnClickListener(this); 30 | } 31 | 32 | @Override 33 | public void onClick(View v) { 34 | int viewId = v.getId(); 35 | if (viewId == R.id.btn_test_anim) { 36 | Animation animation = new SimpleCustomAnimation(); 37 | animation.setDuration(1000); 38 | mTestListView.startAnimation(animation); 39 | } 40 | } 41 | 42 | private class SimpleTestListAdapter extends BaseAdapter { 43 | 44 | @Override 45 | public int getCount() { 46 | return 20; 47 | } 48 | 49 | @Override 50 | public Object getItem(int position) { 51 | return position; 52 | } 53 | 54 | @Override 55 | public long getItemId(int position) { 56 | return position; 57 | } 58 | 59 | @Override 60 | public View getView(int position, View convertView, ViewGroup parent) { 61 | if (convertView == null) { 62 | convertView = View.inflate(parent.getContext(), R.layout.author_info_layout, null); 63 | } 64 | return convertView; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/activity/AnimatorActivity.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.activity; 2 | 3 | import android.animation.AnimatorInflater; 4 | import android.animation.AnimatorSet; 5 | import android.animation.Keyframe; 6 | import android.animation.ObjectAnimator; 7 | import android.animation.PropertyValuesHolder; 8 | import android.animation.ValueAnimator; 9 | import android.graphics.Color; 10 | import android.os.Bundle; 11 | import android.support.v4.view.ViewCompat; 12 | import android.support.v7.app.AppCompatActivity; 13 | import android.util.Log; 14 | import android.view.View; 15 | import android.view.animation.BounceInterpolator; 16 | 17 | import com.clock.study.R; 18 | 19 | /** 20 | * About Android Animator 21 | * 22 | * @author Clock 23 | * @since 2016-07-21 24 | */ 25 | public class AnimatorActivity extends AppCompatActivity implements View.OnClickListener { 26 | 27 | private static final String TAG = AnimatorActivity.class.getSimpleName(); 28 | 29 | private View mTarget; 30 | 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_animator); 35 | 36 | mTarget = findViewById(R.id.anim_target); 37 | 38 | findViewById(R.id.btn_value_anim).setOnClickListener(this); 39 | findViewById(R.id.btn_object_anim_alpha).setOnClickListener(this); 40 | findViewById(R.id.btn_object_anim_rotation).setOnClickListener(this); 41 | findViewById(R.id.btn_object_anim_set).setOnClickListener(this); 42 | findViewById(R.id.btn_object_anim_xml).setOnClickListener(this); 43 | findViewById(R.id.btn_simple_value_animator).setOnClickListener(this); 44 | findViewById(R.id.btn_value_animator_argb).setOnClickListener(this); 45 | findViewById(R.id.btn_bounce_interpolator).setOnClickListener(this); 46 | findViewById(R.id.btn_simple_key_frame).setOnClickListener(this); 47 | findViewById(R.id.btn_shake_key_frame).setOnClickListener(this); 48 | 49 | } 50 | 51 | @Override 52 | public void onClick(View v) { 53 | int viewId = v.getId(); 54 | if (viewId == R.id.btn_value_anim) { 55 | ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100); 56 | valueAnimator.setDuration(3000); 57 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 58 | @Override 59 | public void onAnimationUpdate(ValueAnimator animation) { 60 | int currentValue = (int) animation.getAnimatedValue(); 61 | Log.i(TAG, "currentValue: " + currentValue); 62 | } 63 | }); 64 | valueAnimator.start(); 65 | 66 | } else if (viewId == R.id.btn_object_anim_alpha) { 67 | ObjectAnimator alphaObjectAnimator = ObjectAnimator.ofFloat(mTarget, "alpha", 1f, 0f, 1f); 68 | alphaObjectAnimator.setDuration(3000); 69 | alphaObjectAnimator.start(); 70 | 71 | } else if (viewId == R.id.btn_object_anim_rotation) { 72 | ObjectAnimator rotationObjectAnimator = ObjectAnimator.ofFloat(mTarget, "rotation", 0f, 360f); 73 | rotationObjectAnimator.setDuration(3000); 74 | rotationObjectAnimator.start(); 75 | 76 | } else if (viewId == R.id.btn_object_anim_set) { 77 | AnimatorSet animatorSet = new AnimatorSet(); 78 | ObjectAnimator alphaObjectAnimator = ObjectAnimator.ofFloat(mTarget, "alpha", 1f, 0f, 1f); 79 | ObjectAnimator rotationObjectAnimator = ObjectAnimator.ofFloat(mTarget, "rotation", 0f, 360f); 80 | animatorSet.play(alphaObjectAnimator).with(rotationObjectAnimator); 81 | //animatorSet.play(alphaObjectAnimator).after(rotationObjectAnimator); 82 | //animatorSet.playTogether(alphaObjectAnimator, rotationObjectAnimator); 83 | animatorSet.setDuration(3000); 84 | animatorSet.start(); 85 | 86 | } else if (viewId == R.id.btn_object_anim_xml) { 87 | AnimatorSet animatorSet = (AnimatorSet) AnimatorInflater.loadAnimator(this, R.animator.simple_animator); 88 | animatorSet.setTarget(mTarget); 89 | animatorSet.start(); 90 | 91 | } else if (viewId == R.id.btn_simple_value_animator) { 92 | displayColorAnimation(mTarget, "#0000ff", "#ff0000"); 93 | 94 | } else if (viewId == R.id.btn_value_animator_argb) { 95 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { 96 | int startColor = 0x00000000; 97 | int centerColor = 0xff00ff89; 98 | int endColor = 0x00000000; 99 | ValueAnimator valueAnimator = ValueAnimator.ofArgb(startColor, centerColor, endColor); 100 | valueAnimator.setDuration(6000); 101 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 102 | @Override 103 | public void onAnimationUpdate(ValueAnimator animation) { 104 | int color = (int) animation.getAnimatedValue(); 105 | mTarget.setBackgroundColor(color); 106 | } 107 | }); 108 | valueAnimator.start(); 109 | } 110 | 111 | } else if (viewId == R.id.btn_bounce_interpolator) { 112 | ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mTarget, "translationY", 0f, 1500f); 113 | objectAnimator.setInterpolator(new BounceInterpolator()); 114 | objectAnimator.setDuration(4000); 115 | objectAnimator.start(); 116 | 117 | } else if (viewId == R.id.btn_simple_key_frame) { 118 | Keyframe kf0 = Keyframe.ofFloat(0f, 0f); 119 | Keyframe kf1 = Keyframe.ofFloat(.5f, 360f); 120 | Keyframe kf2 = Keyframe.ofFloat(1f, 0f); 121 | PropertyValuesHolder pvhRotation = PropertyValuesHolder.ofKeyframe("rotation", kf0, kf1, kf2); 122 | ObjectAnimator rotationAnim = ObjectAnimator.ofPropertyValuesHolder(mTarget, pvhRotation); 123 | rotationAnim.setDuration(5000); 124 | rotationAnim.start(); 125 | 126 | } else if (viewId == R.id.btn_shake_key_frame) { 127 | displayShakeAnimator(mTarget, 1f); 128 | 129 | } 130 | } 131 | 132 | /** 133 | * 创建一个旋转动画 134 | * 135 | * @param target 136 | * @param shakeFactor 137 | * @return 138 | */ 139 | private void displayShakeAnimator(View target, float shakeFactor) { 140 | Keyframe scaleXkf0 = Keyframe.ofFloat(0f, 1f); 141 | Keyframe scaleXkf1 = Keyframe.ofFloat(0.1f, 0.9f); 142 | Keyframe scaleXkf2 = Keyframe.ofFloat(0.2f, 0.9f); 143 | Keyframe scaleXkf3 = Keyframe.ofFloat(0.3f, 0.9f); 144 | Keyframe scaleXkf4 = Keyframe.ofFloat(0.4f, 1.1f); 145 | Keyframe scaleXkf5 = Keyframe.ofFloat(0.5f, 1.1f); 146 | Keyframe scaleXkf6 = Keyframe.ofFloat(0.6f, 1.1f); 147 | Keyframe scaleXkf7 = Keyframe.ofFloat(0.7f, 1.1f); 148 | Keyframe scaleXkf8 = Keyframe.ofFloat(0.8f, 1.1f); 149 | Keyframe scaleXkf9 = Keyframe.ofFloat(0.9f, 1.1f); 150 | Keyframe scaleXkf10 = Keyframe.ofFloat(1f, 1f); 151 | 152 | PropertyValuesHolder scaleXHolder = PropertyValuesHolder.ofKeyframe("scaleX", scaleXkf0, scaleXkf1, scaleXkf2, scaleXkf3, scaleXkf4, 153 | scaleXkf5, scaleXkf6, scaleXkf7, scaleXkf8, scaleXkf9, scaleXkf10); 154 | 155 | Keyframe scaleYkf0 = Keyframe.ofFloat(0f, 1f); 156 | Keyframe scaleYkf1 = Keyframe.ofFloat(0.1f, 0.9f); 157 | Keyframe scaleYkf2 = Keyframe.ofFloat(0.2f, 0.9f); 158 | Keyframe scaleYkf3 = Keyframe.ofFloat(0.3f, 0.9f); 159 | Keyframe scaleYkf4 = Keyframe.ofFloat(0.4f, 1.1f); 160 | Keyframe scaleYkf5 = Keyframe.ofFloat(0.5f, 1.1f); 161 | Keyframe scaleYkf6 = Keyframe.ofFloat(0.6f, 1.1f); 162 | Keyframe scaleYkf7 = Keyframe.ofFloat(0.7f, 1.1f); 163 | Keyframe scaleYkf8 = Keyframe.ofFloat(0.8f, 1.1f); 164 | Keyframe scaleYkf9 = Keyframe.ofFloat(0.9f, 1.1f); 165 | Keyframe scaleYkf10 = Keyframe.ofFloat(1f, 1f); 166 | PropertyValuesHolder scaleYHolder = PropertyValuesHolder.ofKeyframe("scaleY", scaleYkf0, scaleYkf1, scaleYkf2, scaleYkf3, scaleYkf4, 167 | scaleYkf5, scaleYkf6, scaleYkf7, scaleYkf8, scaleYkf9, scaleYkf10); 168 | 169 | 170 | PropertyValuesHolder rotationHolder = PropertyValuesHolder.ofKeyframe("rotation", 171 | Keyframe.ofFloat(0f, 0), 172 | Keyframe.ofFloat(0.1f, -3 * shakeFactor), 173 | Keyframe.ofFloat(0.2f, -3 * shakeFactor), 174 | Keyframe.ofFloat(0.3f, 3 * shakeFactor), 175 | Keyframe.ofFloat(0.4f, -3 * shakeFactor), 176 | Keyframe.ofFloat(0.5f, 3 * shakeFactor), 177 | Keyframe.ofFloat(0.6f, -3 * shakeFactor), 178 | Keyframe.ofFloat(0.7f, -3 * shakeFactor), 179 | Keyframe.ofFloat(0.8f, 3 * shakeFactor), 180 | Keyframe.ofFloat(0.9f, -3 * shakeFactor), 181 | Keyframe.ofFloat(1f, 0)); 182 | 183 | 184 | ObjectAnimator.ofPropertyValuesHolder(target, scaleXHolder, scaleYHolder, rotationHolder).setDuration(1000).start(); 185 | } 186 | 187 | /** 188 | * 显示颜色变化的动画 189 | * 190 | * @param target 191 | * @param startColor 192 | * @param endColor 193 | */ 194 | private void displayColorAnimation(final View target, final String startColor, final String endColor) { 195 | ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 100f); 196 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 197 | @Override 198 | public void onAnimationUpdate(ValueAnimator animation) { 199 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB_MR1) { 200 | float fraction = animation.getAnimatedFraction(); 201 | if (target != null) { 202 | int startRed = Integer.parseInt(startColor.substring(1, 3), 16); 203 | int startGreen = Integer.parseInt(startColor.substring(3, 5), 16); 204 | int startBlue = Integer.parseInt(startColor.substring(5, 7), 16); 205 | int endRed = Integer.parseInt(endColor.substring(1, 3), 16); 206 | int endGreen = Integer.parseInt(endColor.substring(3, 5), 16); 207 | int endBlue = Integer.parseInt(endColor.substring(5, 7), 16); 208 | 209 | int redDiff = Math.abs(endRed - startRed); 210 | int greenDiff = Math.abs(endGreen - startGreen); 211 | int blueDiff = Math.abs(endBlue - startBlue); 212 | int colorDiff = redDiff + greenDiff + blueDiff; 213 | 214 | int currRed = getCurrentColor(startRed, endRed, colorDiff, fraction); 215 | int currGreen = getCurrentColor(startGreen, endGreen, colorDiff, fraction); 216 | int currBlue = getCurrentColor(startBlue, endBlue, colorDiff, fraction); 217 | 218 | String colorString = "#" + getHexString(currRed) + getHexString(currGreen) + getHexString(currBlue); 219 | int color = Color.parseColor(colorString); 220 | target.setBackgroundColor(color); 221 | 222 | } 223 | } 224 | 225 | } 226 | }); 227 | valueAnimator.setDuration(3000); 228 | valueAnimator.start(); 229 | 230 | } 231 | 232 | /** 233 | * 获取当前新颜色 234 | * 235 | * @param startColor 236 | * @param endColor 237 | * @param colorDiff 238 | * @param fraction 239 | * @return 240 | */ 241 | private int getCurrentColor(int startColor, int endColor, int colorDiff, float fraction) { 242 | int currentColor = 0; 243 | if (startColor > endColor) { 244 | currentColor = (int) (startColor - fraction * colorDiff); 245 | } else { 246 | currentColor = (int) (startColor + fraction * colorDiff); 247 | } 248 | if (currentColor >= 0 && currentColor <= 256) {//最终的色值要确保在0到256之间 249 | return currentColor; 250 | } else { 251 | currentColor = endColor; 252 | } 253 | return currentColor; 254 | } 255 | 256 | /** 257 | * 将10进制颜色值转换成16进制。 258 | */ 259 | private String getHexString(int value) { 260 | String hexString = Integer.toHexString(value); 261 | if (hexString.length() == 1) { 262 | hexString = "0" + hexString; 263 | } 264 | return hexString; 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/activity/AuthorActivity.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.activity; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.os.Bundle; 5 | 6 | import com.clock.study.R; 7 | import com.clock.study.helper.DayNightHelper; 8 | 9 | public class AuthorActivity extends AppCompatActivity { 10 | 11 | private DayNightHelper mDayNightHelper; 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | initData(); 17 | initTheme(); 18 | setContentView(R.layout.activity_author); 19 | } 20 | 21 | private void initData() { 22 | mDayNightHelper = new DayNightHelper(this); 23 | } 24 | 25 | private void initTheme() { 26 | if (mDayNightHelper.isDay()) { 27 | setTheme(R.style.DayTheme); 28 | } else { 29 | setTheme(R.style.NightTheme); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/activity/CapturePhotoActivity.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.activity; 2 | 3 | import android.Manifest; 4 | import android.content.DialogInterface; 5 | import android.content.Intent; 6 | import android.content.pm.PackageManager; 7 | import android.net.Uri; 8 | import android.os.Build; 9 | import android.os.Bundle; 10 | import android.provider.Settings; 11 | import android.support.annotation.NonNull; 12 | import android.support.v4.app.ActivityCompat; 13 | import android.support.v7.app.AlertDialog; 14 | import android.support.v7.app.AppCompatActivity; 15 | import android.util.Log; 16 | import android.view.View; 17 | import android.widget.Toast; 18 | 19 | import com.clock.study.R; 20 | import com.clock.study.helper.CapturePhotoHelper; 21 | import com.clock.study.manager.FolderManager; 22 | 23 | import java.io.File; 24 | 25 | /** 26 | * 调用系统相机进行拍照 27 | * 28 | * @author Clock 29 | * @since 2016-05-13 30 | */ 31 | public class CapturePhotoActivity extends AppCompatActivity implements View.OnClickListener { 32 | 33 | private final static String TAG = CapturePhotoActivity.class.getSimpleName(); 34 | private final static String EXTRA_RESTORE_PHOTO = "extra_restore_photo"; 35 | 36 | /** 37 | * 运行时权限申请码 38 | */ 39 | private final static int RUNTIME_PERMISSION_REQUEST_CODE = 0x1; 40 | 41 | private CapturePhotoHelper mCapturePhotoHelper; 42 | private File mRestorePhotoFile; 43 | 44 | @Override 45 | protected void onCreate(Bundle savedInstanceState) { 46 | Log.i(TAG, "onCreate"); 47 | super.onCreate(savedInstanceState); 48 | setContentView(R.layout.activity_camera_take_photo); 49 | 50 | findViewById(R.id.iv_take_photo).setOnClickListener(this); 51 | 52 | } 53 | 54 | @Override 55 | protected void onSaveInstanceState(Bundle outState) { 56 | Log.i(TAG, "onSaveInstanceState"); 57 | super.onSaveInstanceState(outState); 58 | if (mCapturePhotoHelper != null) { 59 | mRestorePhotoFile = mCapturePhotoHelper.getPhoto(); 60 | Log.i(TAG, "onSaveInstanceState , mRestorePhotoFile: " + mRestorePhotoFile); 61 | if (mRestorePhotoFile != null) { 62 | outState.putSerializable(EXTRA_RESTORE_PHOTO, mRestorePhotoFile); 63 | } 64 | } 65 | } 66 | 67 | @Override 68 | protected void onRestoreInstanceState(Bundle savedInstanceState) { 69 | Log.i(TAG, "onRestoreInstanceState"); 70 | super.onRestoreInstanceState(savedInstanceState); 71 | if (mCapturePhotoHelper != null) { 72 | mRestorePhotoFile = (File) savedInstanceState.getSerializable(EXTRA_RESTORE_PHOTO); 73 | Log.i(TAG, "onRestoreInstanceState , mRestorePhotoFile: " + mRestorePhotoFile); 74 | mCapturePhotoHelper.setPhoto(mRestorePhotoFile); 75 | } 76 | } 77 | 78 | @Override 79 | public void onClick(View v) { 80 | int viewId = v.getId(); 81 | if (viewId == R.id.iv_take_photo) { 82 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //Android M 处理Runtime Permission 83 | if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {//检查是否有写入SD卡的授权 84 | Log.i(TAG, "granted permission!"); 85 | turnOnCamera(); 86 | } else { 87 | Log.i(TAG, "denied permission!"); 88 | if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { 89 | Log.i(TAG, "should show request permission rationale!"); 90 | } 91 | requestPermission(); 92 | } 93 | } else { 94 | turnOnCamera(); 95 | } 96 | } 97 | } 98 | 99 | /** 100 | * 开启相机 101 | */ 102 | private void turnOnCamera() { 103 | if (mCapturePhotoHelper == null) { 104 | mCapturePhotoHelper = new CapturePhotoHelper(this, FolderManager.getPhotoFolder()); 105 | } 106 | mCapturePhotoHelper.capture(); 107 | } 108 | 109 | /** 110 | * 申请写入sd卡的权限 111 | */ 112 | private void requestPermission() { 113 | ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, RUNTIME_PERMISSION_REQUEST_CODE); 114 | } 115 | 116 | @Override 117 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 118 | Log.i(TAG, "requestCode: " + requestCode + " resultCode: " + resultCode + " data: " + data); 119 | if (requestCode == CapturePhotoHelper.CAPTURE_PHOTO_REQUEST_CODE) { 120 | File photoFile = mCapturePhotoHelper.getPhoto(); 121 | if (photoFile != null) { 122 | if (resultCode == RESULT_OK) { 123 | PhotoPreviewActivity.preview(this, photoFile); 124 | finish(); 125 | } else { 126 | if (photoFile.exists()) { 127 | photoFile.delete(); 128 | } 129 | } 130 | } 131 | 132 | } else { 133 | super.onActivityResult(requestCode, resultCode, data); 134 | } 135 | } 136 | 137 | @Override 138 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 139 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 140 | if (requestCode == RUNTIME_PERMISSION_REQUEST_CODE) { 141 | for (int index = 0; index < permissions.length; index++) { 142 | String permission = permissions[index]; 143 | if (Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission)) { 144 | if (grantResults[index] == PackageManager.PERMISSION_GRANTED) { 145 | Log.i(TAG, "onRequestPermissionsResult: permission is granted!"); 146 | turnOnCamera(); 147 | 148 | } else { 149 | showMissingPermissionDialog(); 150 | 151 | } 152 | } 153 | } 154 | } 155 | } 156 | 157 | /** 158 | * 显示打开权限提示的对话框 159 | */ 160 | private void showMissingPermissionDialog() { 161 | AlertDialog.Builder builder = new AlertDialog.Builder(this); 162 | builder.setTitle(R.string.help); 163 | builder.setMessage(R.string.help_content); 164 | 165 | builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { 166 | @Override 167 | public void onClick(DialogInterface dialog, int which) { 168 | Toast.makeText(CapturePhotoActivity.this, R.string.camera_open_error, Toast.LENGTH_SHORT).show(); 169 | finish(); 170 | } 171 | }); 172 | 173 | builder.setPositiveButton(R.string.settings, new DialogInterface.OnClickListener() { 174 | @Override 175 | public void onClick(DialogInterface dialog, int which) { 176 | turnOnSettings(); 177 | } 178 | }); 179 | 180 | builder.show(); 181 | } 182 | 183 | /** 184 | * 启动系统权限设置界面 185 | */ 186 | private void turnOnSettings() { 187 | Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 188 | intent.setData(Uri.parse("package:" + getPackageName())); 189 | startActivity(intent); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/activity/DayNightActivity.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.activity; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ObjectAnimator; 6 | import android.animation.ValueAnimator; 7 | import android.content.res.Resources; 8 | import android.graphics.Bitmap; 9 | import android.graphics.drawable.BitmapDrawable; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.support.v7.app.AppCompatActivity; 13 | import android.support.v7.widget.LinearLayoutManager; 14 | import android.support.v7.widget.RecyclerView; 15 | import android.util.TypedValue; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.view.Window; 19 | import android.widget.CheckBox; 20 | import android.widget.CompoundButton; 21 | import android.widget.LinearLayout; 22 | import android.widget.RelativeLayout; 23 | import android.widget.TextView; 24 | 25 | import com.clock.study.R; 26 | import com.clock.study.adapter.SimpleAuthorAdapter; 27 | import com.clock.study.helper.DayNightHelper; 28 | import com.clock.study.type.DayNight; 29 | 30 | import java.lang.reflect.Field; 31 | import java.lang.reflect.InvocationTargetException; 32 | import java.lang.reflect.Method; 33 | import java.util.ArrayList; 34 | import java.util.List; 35 | 36 | /** 37 | * 夜间模式实现方案 38 | * 39 | * @author Clock 40 | * @since 2016-08-11 41 | */ 42 | public class DayNightActivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener { 43 | 44 | private final static String TAG = DayNightActivity.class.getSimpleName(); 45 | 46 | private DayNightHelper mDayNightHelper; 47 | 48 | private RecyclerView mRecyclerView; 49 | 50 | private LinearLayout mHeaderLayout; 51 | private List mLayoutList; 52 | private List mTextViewList; 53 | private List mCheckBoxList; 54 | 55 | @Override 56 | protected void onCreate(Bundle savedInstanceState) { 57 | super.onCreate(savedInstanceState); 58 | supportRequestWindowFeature(Window.FEATURE_NO_TITLE); 59 | initData(); 60 | initTheme(); 61 | setContentView(R.layout.activity_day_night); 62 | initView(); 63 | } 64 | 65 | private void initView() { 66 | mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view); 67 | RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); 68 | mRecyclerView.setLayoutManager(layoutManager); 69 | mRecyclerView.setAdapter(new SimpleAuthorAdapter()); 70 | 71 | mHeaderLayout = (LinearLayout) findViewById(R.id.header_layout); 72 | 73 | mLayoutList = new ArrayList<>(); 74 | mLayoutList.add((RelativeLayout) findViewById(R.id.jianshu_layout)); 75 | mLayoutList.add((RelativeLayout) findViewById(R.id.zhihu_layout)); 76 | 77 | mTextViewList = new ArrayList<>(); 78 | mTextViewList.add((TextView) findViewById(R.id.tv_jianshu)); 79 | mTextViewList.add((TextView) findViewById(R.id.tv_zhihu)); 80 | 81 | mCheckBoxList = new ArrayList<>(); 82 | CheckBox ckbJianshu = (CheckBox) findViewById(R.id.ckb_jianshu); 83 | ckbJianshu.setOnCheckedChangeListener(this); 84 | mCheckBoxList.add(ckbJianshu); 85 | CheckBox ckbZhihu = (CheckBox) findViewById(R.id.ckb_zhihu); 86 | ckbZhihu.setOnCheckedChangeListener(this); 87 | mCheckBoxList.add(ckbZhihu); 88 | 89 | } 90 | 91 | @Override 92 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 93 | int viewId = buttonView.getId(); 94 | if (viewId == R.id.ckb_jianshu) { 95 | changeThemeByJianShu(); 96 | 97 | } else if (viewId == R.id.ckb_zhihu) { 98 | changeThemeByZhiHu(); 99 | 100 | } 101 | } 102 | 103 | private void initData() { 104 | mDayNightHelper = new DayNightHelper(this); 105 | } 106 | 107 | private void initTheme() { 108 | if (mDayNightHelper.isDay()) { 109 | setTheme(R.style.DayTheme); 110 | } else { 111 | setTheme(R.style.NightTheme); 112 | } 113 | } 114 | 115 | /** 116 | * 切换主题设置 117 | */ 118 | private void toggleThemeSetting() { 119 | if (mDayNightHelper.isDay()) { 120 | mDayNightHelper.setMode(DayNight.NIGHT); 121 | setTheme(R.style.NightTheme); 122 | } else { 123 | mDayNightHelper.setMode(DayNight.DAY); 124 | setTheme(R.style.DayTheme); 125 | } 126 | } 127 | 128 | /** 129 | * 使用简书的实现套路来切换夜间主题 130 | */ 131 | private void changeThemeByJianShu() { 132 | toggleThemeSetting(); 133 | refreshUI(); 134 | } 135 | 136 | /** 137 | * 使用知乎的实现套路来切换夜间主题 138 | */ 139 | private void changeThemeByZhiHu() { 140 | showAnimation(); 141 | toggleThemeSetting(); 142 | refreshUI(); 143 | } 144 | 145 | /** 146 | * 刷新UI界面 147 | */ 148 | private void refreshUI() { 149 | TypedValue background = new TypedValue();//背景色 150 | TypedValue textColor = new TypedValue();//字体颜色 151 | Resources.Theme theme = getTheme(); 152 | theme.resolveAttribute(R.attr.clockBackground, background, true); 153 | theme.resolveAttribute(R.attr.clockTextColor, textColor, true); 154 | 155 | mHeaderLayout.setBackgroundResource(background.resourceId); 156 | for (RelativeLayout layout : mLayoutList) { 157 | layout.setBackgroundResource(background.resourceId); 158 | } 159 | for (CheckBox checkBox : mCheckBoxList) { 160 | checkBox.setBackgroundResource(background.resourceId); 161 | } 162 | for (TextView textView : mTextViewList) { 163 | textView.setBackgroundResource(background.resourceId); 164 | } 165 | 166 | Resources resources = getResources(); 167 | for (TextView textView : mTextViewList) { 168 | textView.setTextColor(resources.getColor(textColor.resourceId)); 169 | } 170 | 171 | int childCount = mRecyclerView.getChildCount(); 172 | for (int childIndex = 0; childIndex < childCount; childIndex++) { 173 | ViewGroup childView = (ViewGroup) mRecyclerView.getChildAt(childIndex); 174 | childView.setBackgroundResource(background.resourceId); 175 | View infoLayout = childView.findViewById(R.id.info_layout); 176 | infoLayout.setBackgroundResource(background.resourceId); 177 | TextView nickName = (TextView) childView.findViewById(R.id.tv_nickname); 178 | nickName.setBackgroundResource(background.resourceId); 179 | nickName.setTextColor(resources.getColor(textColor.resourceId)); 180 | TextView motto = (TextView) childView.findViewById(R.id.tv_motto); 181 | motto.setBackgroundResource(background.resourceId); 182 | motto.setTextColor(resources.getColor(textColor.resourceId)); 183 | } 184 | 185 | //让 RecyclerView 缓存在 Pool 中的 Item 失效 186 | //那么,如果是ListView,要怎么做呢?这里的思路是通过反射拿到 AbsListView 类中的 RecycleBin 对象,然后同样再用反射去调用 clear 方法 187 | Class recyclerViewClass = RecyclerView.class; 188 | try { 189 | Field declaredField = recyclerViewClass.getDeclaredField("mRecycler"); 190 | declaredField.setAccessible(true); 191 | Method declaredMethod = Class.forName(RecyclerView.Recycler.class.getName()).getDeclaredMethod("clear", (Class[]) new Class[0]); 192 | declaredMethod.setAccessible(true); 193 | declaredMethod.invoke(declaredField.get(mRecyclerView), new Object[0]); 194 | RecyclerView.RecycledViewPool recycledViewPool = mRecyclerView.getRecycledViewPool(); 195 | recycledViewPool.clear(); 196 | 197 | } catch (NoSuchFieldException e) { 198 | e.printStackTrace(); 199 | } catch (ClassNotFoundException e) { 200 | e.printStackTrace(); 201 | } catch (NoSuchMethodException e) { 202 | e.printStackTrace(); 203 | } catch (InvocationTargetException e) { 204 | e.printStackTrace(); 205 | } catch (IllegalAccessException e) { 206 | e.printStackTrace(); 207 | } 208 | 209 | refreshStatusBar(); 210 | } 211 | 212 | /** 213 | * 刷新 StatusBar 214 | */ 215 | private void refreshStatusBar() { 216 | if (Build.VERSION.SDK_INT >= 21) { 217 | TypedValue typedValue = new TypedValue(); 218 | Resources.Theme theme = getTheme(); 219 | theme.resolveAttribute(R.attr.colorPrimary, typedValue, true); 220 | getWindow().setStatusBarColor(getResources().getColor(typedValue.resourceId)); 221 | } 222 | } 223 | 224 | /** 225 | * 展示一个切换动画 226 | */ 227 | private void showAnimation() { 228 | final View decorView = getWindow().getDecorView(); 229 | Bitmap cacheBitmap = getCacheBitmapFromView(decorView); 230 | if (decorView instanceof ViewGroup && cacheBitmap != null) { 231 | final View view = new View(this); 232 | view.setBackgroundDrawable(new BitmapDrawable(getResources(), cacheBitmap)); 233 | ViewGroup.LayoutParams layoutParam = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 234 | ViewGroup.LayoutParams.MATCH_PARENT); 235 | ((ViewGroup) decorView).addView(view, layoutParam); 236 | ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f); 237 | objectAnimator.setDuration(300); 238 | objectAnimator.addListener(new AnimatorListenerAdapter() { 239 | @Override 240 | public void onAnimationEnd(Animator animation) { 241 | super.onAnimationEnd(animation); 242 | ((ViewGroup) decorView).removeView(view); 243 | } 244 | }); 245 | objectAnimator.start(); 246 | } 247 | } 248 | 249 | /** 250 | * 获取一个 View 的缓存视图 251 | * 252 | * @param view 253 | * @return 254 | */ 255 | private Bitmap getCacheBitmapFromView(View view) { 256 | final boolean drawingCacheEnabled = true; 257 | view.setDrawingCacheEnabled(drawingCacheEnabled); 258 | view.buildDrawingCache(drawingCacheEnabled); 259 | final Bitmap drawingCache = view.getDrawingCache(); 260 | Bitmap bitmap; 261 | if (drawingCache != null) { 262 | bitmap = Bitmap.createBitmap(drawingCache); 263 | view.setDrawingCacheEnabled(false); 264 | } else { 265 | bitmap = null; 266 | } 267 | return bitmap; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.activity; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.view.View; 7 | 8 | import com.clock.study.R; 9 | 10 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | setContentView(R.layout.activity_main); 16 | 17 | findViewById(R.id.btn_camera_take_photo).setOnClickListener(this); 18 | findViewById(R.id.btn_animation).setOnClickListener(this); 19 | findViewById(R.id.btn_animator).setOnClickListener(this); 20 | findViewById(R.id.btn_day_night).setOnClickListener(this); 21 | 22 | } 23 | 24 | @Override 25 | public void onClick(View v) { 26 | int viewId = v.getId(); 27 | if (viewId == R.id.btn_camera_take_photo) { 28 | Intent takePhotoIntent = new Intent(this, CapturePhotoActivity.class); 29 | startActivity(takePhotoIntent); 30 | } else if (viewId == R.id.btn_animation) { 31 | Intent animationIntent = new Intent(this, AnimationActivity.class); 32 | startActivity(animationIntent); 33 | } else if (viewId == R.id.btn_animator) { 34 | Intent animatorIntent = new Intent(this, AnimatorActivity.class); 35 | startActivity(animatorIntent); 36 | } else if (viewId == R.id.btn_day_night) { 37 | Intent animatorIntent = new Intent(this, DayNightActivity.class); 38 | startActivity(animatorIntent); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/activity/PhotoPreviewActivity.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.activity; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.graphics.Bitmap; 6 | import android.os.Bundle; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.view.View; 9 | import android.widget.ImageView; 10 | 11 | import com.clock.study.R; 12 | import com.clock.utils.bitmap.BitmapUtils; 13 | import com.clock.utils.common.RuleUtils; 14 | 15 | import java.io.File; 16 | 17 | /** 18 | * 预览图片界面 19 | * 20 | * @author Clock 21 | * @since 2016-05-13 22 | */ 23 | public class PhotoPreviewActivity extends AppCompatActivity implements View.OnClickListener { 24 | 25 | private final static float RATIO = 0.75f; 26 | 27 | private final static String EXTRA_PHOTO = "extra_photo"; 28 | 29 | private ImageView mPhotoPreview; 30 | private File mPhotoFile; 31 | 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_photo_preview); 36 | 37 | mPhotoPreview = (ImageView) findViewById(R.id.iv_preview_photo); 38 | 39 | mPhotoFile = (File) getIntent().getSerializableExtra(EXTRA_PHOTO); 40 | int requestWidth = (int) (RuleUtils.getScreenWidth(this) * RATIO); 41 | int requestHeight = (int) (RuleUtils.getScreenHeight(this) * RATIO); 42 | Bitmap bitmap = BitmapUtils.decodeBitmapFromFile(mPhotoFile, requestWidth, requestHeight);//按照屏幕宽高的3/4比例进行缩放显示 43 | if (bitmap != null) { 44 | int degree = BitmapUtils.getBitmapDegree(mPhotoFile.getAbsolutePath());//检查是否有被旋转,并进行纠正 45 | if (degree != 0) { 46 | bitmap = BitmapUtils.rotateBitmapByDegree(bitmap, degree); 47 | } 48 | mPhotoPreview.setImageBitmap(bitmap); 49 | } 50 | 51 | findViewById(R.id.btn_display_to_gallery).setOnClickListener(this); 52 | } 53 | 54 | public static void preview(Activity activity, File file) { 55 | Intent previewIntent = new Intent(activity, PhotoPreviewActivity.class); 56 | previewIntent.putExtra(EXTRA_PHOTO, file); 57 | activity.startActivity(previewIntent); 58 | } 59 | 60 | @Override 61 | public void onClick(View v) { 62 | int viewId = v.getId(); 63 | if (viewId == R.id.btn_display_to_gallery) { 64 | BitmapUtils.displayToGallery(this, mPhotoFile); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/adapter/SimpleAuthorAdapter.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.adapter; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import com.clock.study.R; 11 | import com.clock.study.activity.AuthorActivity; 12 | 13 | /** 14 | * Just Simple Author Adapter 15 | *

16 | * Created by Clock on 2016/8/24. 17 | */ 18 | public class SimpleAuthorAdapter extends RecyclerView.Adapter { 19 | 20 | private final View.OnClickListener mSimpleClickListener = new View.OnClickListener() { 21 | @Override 22 | public void onClick(View v) { 23 | Context context = v.getContext(); 24 | Intent intent = new Intent(context, AuthorActivity.class); 25 | context.startActivity(intent); 26 | } 27 | }; 28 | 29 | @Override 30 | public SimpleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 31 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 32 | View rootView = inflater.inflate(R.layout.author_info_layout, parent, false); 33 | return new SimpleViewHolder(rootView); 34 | } 35 | 36 | @Override 37 | public void onBindViewHolder(SimpleViewHolder holder, int position) { 38 | holder.itemView.setOnClickListener(mSimpleClickListener); 39 | } 40 | 41 | @Override 42 | public int getItemCount() { 43 | return 20; 44 | } 45 | 46 | static class SimpleViewHolder extends RecyclerView.ViewHolder { 47 | public SimpleViewHolder(View itemView) { 48 | super(itemView); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/animation/SimpleCustomAnimation.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.animation; 2 | 3 | import android.graphics.Matrix; 4 | import android.util.Log; 5 | import android.view.animation.Animation; 6 | import android.view.animation.Transformation; 7 | 8 | /** 9 | * Created by Clock on 2016/7/9. 10 | */ 11 | public class SimpleCustomAnimation extends Animation { 12 | 13 | private final static String TAG = SimpleCustomAnimation.class.getSimpleName(); 14 | 15 | private int mWidth, mHeight; 16 | 17 | @Override 18 | public void initialize(int width, int height, int parentWidth, int parentHeight) { 19 | Log.i(TAG, "-------------initialize-------------"); 20 | Log.i(TAG, "width:" + width); 21 | Log.i(TAG, "height:" + height); 22 | Log.i(TAG, "parentWidth:" + parentWidth); 23 | Log.i(TAG, "parentHeight:" + parentHeight); 24 | Log.i(TAG, "-------------initialize-------------"); 25 | super.initialize(width, height, parentWidth, parentHeight); 26 | this.mWidth = width; 27 | this.mHeight = height; 28 | } 29 | 30 | @Override 31 | protected void applyTransformation(float interpolatedTime, Transformation t) { 32 | Matrix matrix = t.getMatrix(); 33 | matrix.preScale(interpolatedTime, interpolatedTime);//缩放 34 | matrix.preRotate(interpolatedTime * 360);//旋转 35 | //下面的Translate组合是为了将缩放和旋转的基点移动到整个View的中心,不然系统默认是以View的左上角作为基点 36 | matrix.preTranslate(-mWidth / 2, -mHeight / 2); 37 | matrix.postTranslate(mWidth / 2, mHeight / 2); 38 | Log.i(TAG, "-------------applyTransformation-------------"); 39 | Log.i(TAG, "interpolatedTime:" + interpolatedTime);//动画持续的时间,时间比例系数(0.0 到 1.0)之间 40 | Log.i(TAG, "transformation:" + t);//控制动画效果,Transformation包含两个信息,一个Alpha值,一个Matrix矩阵,这里的Matrix默认是一个单位矩阵 41 | Log.i(TAG, "-------------applyTransformation-------------"); 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/animator/ColorEvaluator.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.animator; 2 | 3 | import android.animation.TypeEvaluator; 4 | 5 | /** 6 | * 仿照了 ArgbEvaluator 的系统实现,用于产生颜色过度渐变效果 7 | *

8 | * Created by Clock on 2016/7/22. 9 | */ 10 | public class ColorEvaluator implements TypeEvaluator { 11 | 12 | @Override 13 | public Object evaluate(float fraction, Object startValue, Object endValue) { 14 | int startInt = (Integer) startValue; 15 | int startA = (startInt >> 24) & 0xff; 16 | int startR = (startInt >> 16) & 0xff; 17 | int startG = (startInt >> 8) & 0xff; 18 | int startB = startInt & 0xff; 19 | 20 | int endInt = (Integer) endValue; 21 | int endA = (endInt >> 24) & 0xff; 22 | int endR = (endInt >> 16) & 0xff; 23 | int endG = (endInt >> 8) & 0xff; 24 | int endB = endInt & 0xff; 25 | 26 | return (int) ((startA + (int) (fraction * (endA - startA))) << 24) | 27 | (int) ((startR + (int) (fraction * (endR - startR))) << 16) | 28 | (int) ((startG + (int) (fraction * (endG - startG))) << 8) | 29 | (int) ((startB + (int) (fraction * (endB - startB)))); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/helper/CapturePhotoHelper.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.helper; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.content.pm.PackageManager; 6 | import android.content.pm.ResolveInfo; 7 | import android.net.Uri; 8 | import android.provider.MediaStore; 9 | import android.widget.Toast; 10 | 11 | import com.clock.study.R; 12 | import com.clock.utils.bitmap.BitmapUtils; 13 | 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.text.SimpleDateFormat; 17 | import java.util.Date; 18 | import java.util.List; 19 | 20 | /** 21 | * 拍照辅助类 22 | *

23 | * Created by Clock on 2016/5/21. 24 | */ 25 | public class CapturePhotoHelper { 26 | 27 | private final static String TIMESTAMP_FORMAT = "yyyy_MM_dd_HH_mm_ss"; 28 | 29 | public final static int CAPTURE_PHOTO_REQUEST_CODE = 0x1111; 30 | 31 | private Activity mActivity; 32 | /** 33 | * 存放图片的目录 34 | */ 35 | private File mPhotoFolder; 36 | /** 37 | * 拍照生成的图片文件 38 | */ 39 | private File mPhotoFile; 40 | 41 | /** 42 | * @param activity 43 | * @param photoFolder 存放生成照片的目录,目录不存在时候会自动创建,但不允许为null; 44 | */ 45 | public CapturePhotoHelper(Activity activity, File photoFolder) { 46 | this.mActivity = activity; 47 | this.mPhotoFolder = photoFolder; 48 | } 49 | 50 | /** 51 | * 拍照 52 | */ 53 | public void capture() { 54 | if (hasCamera()) { 55 | createPhotoFile(); 56 | 57 | if (mPhotoFile == null) { 58 | Toast.makeText(mActivity, R.string.camera_open_error, Toast.LENGTH_SHORT).show(); 59 | return; 60 | } 61 | 62 | Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 63 | Uri fileUri = Uri.fromFile(mPhotoFile); 64 | captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri); 65 | mActivity.startActivityForResult(captureIntent, CAPTURE_PHOTO_REQUEST_CODE); 66 | 67 | } else { 68 | Toast.makeText(mActivity, R.string.camera_open_error, Toast.LENGTH_SHORT).show(); 69 | } 70 | } 71 | 72 | /** 73 | * 创建照片文件 74 | */ 75 | private void createPhotoFile() { 76 | if (mPhotoFolder != null) { 77 | if (!mPhotoFolder.exists()) {//检查保存图片的目录存不存在 78 | mPhotoFolder.mkdirs(); 79 | } 80 | 81 | String fileName = new SimpleDateFormat(TIMESTAMP_FORMAT).format(new Date()); 82 | mPhotoFile = new File(mPhotoFolder, fileName + BitmapUtils.JPG_SUFFIX); 83 | if (mPhotoFile.exists()) { 84 | mPhotoFile.delete(); 85 | } 86 | try { 87 | mPhotoFile.createNewFile(); 88 | } catch (IOException e) { 89 | e.printStackTrace(); 90 | mPhotoFile = null; 91 | } 92 | } else { 93 | mPhotoFile = null; 94 | Toast.makeText(mActivity, R.string.not_specify_a_directory, Toast.LENGTH_SHORT).show(); 95 | } 96 | } 97 | 98 | 99 | /** 100 | * 判断系统中是否存在可以启动的相机应用 101 | * 102 | * @return 存在返回true,不存在返回false 103 | */ 104 | public boolean hasCamera() { 105 | PackageManager packageManager = mActivity.getPackageManager(); 106 | Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 107 | List list = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 108 | return list.size() > 0; 109 | } 110 | 111 | /** 112 | * 获取当前拍到的图片文件 113 | * 114 | * @return 115 | */ 116 | public File getPhoto() { 117 | return mPhotoFile; 118 | } 119 | 120 | /** 121 | * 设置照片文件 122 | * 123 | * @param photoFile 124 | */ 125 | public void setPhoto(File photoFile) { 126 | this.mPhotoFile = photoFile; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/helper/DayNightHelper.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.helper; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import com.clock.study.type.DayNight; 7 | 8 | /** 9 | * Created by Clock on 2016/8/24. 10 | */ 11 | public class DayNightHelper { 12 | 13 | private final static String FILE_NAME = "settings"; 14 | private final static String MODE = "day_night_mode"; 15 | 16 | private SharedPreferences mSharedPreferences; 17 | 18 | public DayNightHelper(Context context) { 19 | this.mSharedPreferences = context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE); 20 | } 21 | 22 | /** 23 | * 保存模式设置 24 | * 25 | * @param mode 26 | * @return 27 | */ 28 | public boolean setMode(DayNight mode) { 29 | SharedPreferences.Editor editor = mSharedPreferences.edit(); 30 | editor.putString(MODE, mode.getName()); 31 | return editor.commit(); 32 | } 33 | 34 | /** 35 | * 夜间模式 36 | * 37 | * @return 38 | */ 39 | public boolean isNight() { 40 | String mode = mSharedPreferences.getString(MODE, DayNight.DAY.getName()); 41 | if (DayNight.NIGHT.getName().equals(mode)) { 42 | return true; 43 | } else { 44 | return false; 45 | } 46 | } 47 | 48 | /** 49 | * 日间模式 50 | * 51 | * @return 52 | */ 53 | public boolean isDay() { 54 | String mode = mSharedPreferences.getString(MODE, DayNight.DAY.getName()); 55 | if (DayNight.DAY.getName().equals(mode)) { 56 | return true; 57 | } else { 58 | return false; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/manager/FolderManager.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.manager; 2 | 3 | import android.Manifest; 4 | import android.app.AlertDialog; 5 | import android.content.Context; 6 | import android.content.pm.PackageManager; 7 | import android.os.Environment; 8 | import android.support.v4.app.ActivityCompat; 9 | 10 | import com.clock.study.R; 11 | 12 | import java.io.File; 13 | 14 | /** 15 | * 目录管理器 16 | *

17 | * Created by Clock on 2016/5/27. 18 | */ 19 | public class FolderManager { 20 | 21 | /** 22 | * 应用程序在SD卡上的主目录名称 23 | */ 24 | private final static String APP_FOLDER_NAME = "AndroidStudy"; 25 | /** 26 | * 存放图片目录名 27 | */ 28 | private final static String PHOTO_FOLDER_NAME = "photo"; 29 | /** 30 | * 存放闪退日志目录名 31 | */ 32 | private final static String CRASH_LOG_FOLDER_NAME = "crash"; 33 | 34 | 35 | private FolderManager() { 36 | } 37 | 38 | /** 39 | * 获取app在sd卡上的主目录 40 | * 41 | * @return 成功则返回目录,失败则返回null 42 | */ 43 | public static File getAppFolder() { 44 | if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 45 | 46 | File appFolder = new File(Environment.getExternalStorageDirectory(), APP_FOLDER_NAME); 47 | return createOnNotFound(appFolder); 48 | 49 | } else { 50 | return null; 51 | } 52 | } 53 | 54 | /** 55 | * 获取应用存放图片的目录 56 | * 57 | * @return 成功则返回目录名,失败则返回null 58 | */ 59 | public static File getPhotoFolder() { 60 | File appFolder = getAppFolder(); 61 | if (appFolder != null) { 62 | 63 | File photoFolder = new File(appFolder, PHOTO_FOLDER_NAME); 64 | return createOnNotFound(photoFolder); 65 | 66 | } else { 67 | return null; 68 | } 69 | } 70 | 71 | /** 72 | * 获取闪退日志存放目录 73 | * 74 | * @return 75 | */ 76 | public static File getCrashLogFolder() { 77 | File appFolder = getAppFolder(); 78 | if (appFolder != null) { 79 | 80 | File crashLogFolder = new File(appFolder, CRASH_LOG_FOLDER_NAME); 81 | return createOnNotFound(crashLogFolder); 82 | } else { 83 | return null; 84 | } 85 | } 86 | 87 | /** 88 | * 创建目录 89 | * 90 | * @param folder 91 | * @return 创建成功则返回目录,失败则返回null 92 | */ 93 | private static File createOnNotFound(File folder) { 94 | if (folder == null) { 95 | return null; 96 | } 97 | 98 | if (!folder.exists()) { 99 | folder.mkdirs(); 100 | } 101 | 102 | if (folder.exists()) { 103 | return folder; 104 | } else { 105 | return null; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/clock/study/type/DayNight.java: -------------------------------------------------------------------------------- 1 | package com.clock.study.type; 2 | 3 | /** 4 | * Created by Clock on 2016/8/24. 5 | */ 6 | public enum DayNight { 7 | 8 | DAY("DAY", 0), 9 | NIGHT("NIGHT", 1); 10 | 11 | private String name; 12 | private int code; 13 | 14 | private DayNight(String name, int code) { 15 | this.name = name; 16 | this.code = code; 17 | } 18 | 19 | public int getCode() { 20 | return code; 21 | } 22 | 23 | public void setCode(int code) { 24 | this.code = code; 25 | } 26 | 27 | public String getName() { 28 | return name; 29 | } 30 | 31 | public void setName(String name) { 32 | this.name = name; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/res/animator/simple_animator.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /src/main/res/layout/activity_android_anim.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 |