注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

还不会用coil,你就out了

Coil是Android平台上又一个开源的图片加载库,尽管Android平台已经有诸如Picasso,Glide以及Fresco等非常成熟且优秀的图片加载库了,但Coil最主要的特色就是融合了当下Android开发界最主流的技术和趋势,采用Kotlin为开发语...
继续阅读 »

Coil是Android平台上又一个开源的图片加载库,尽管Android平台已经有诸如Picasso,Glide以及Fresco等非常成熟且优秀的图片加载库了,但Coil最主要的特色就是融合了当下Android开发界最主流的技术和趋势,采用Kotlin为开发语言,将协程、OKHttp、OKIO和AndroidX作为一等公民,以期打造成一个更加轻快、现代化的图片加载库。具体而言包含以下几个方面:



  • 发挥Kotlin的语言特性,利用扩展函数、内联、lambda参数以及密封类来创建简单优雅的API。

  • 利用了Kotlin协程强大的 可取消的非阻塞式异步编程和对线程最大化利用的特性。

  • 使用现代化的依赖库:OKHttp、OKIO基本上已经是目前大部分app的事实“标准”库,它们强大的特性让Coil避免了重复实现磁盘缓存和缓冲流;类似的,AndroidX-LifeCycle也是官方推荐的,Coil目前是唯一一个对其支持的图片加载库。

  • 轻量:Coil项目的代码量几乎只有Glide的1/8,更是远远小于Fresco;并且对APK仅增加了大约1500个方法(对于那些已经依赖的OKHttp和协程的app来说),和Picasso相当并显著低于Glide和Fresco。

  • 支持扩展:Coil的image-pipline主要由 Mappers , Fetchers , 和 Decoders 三个类组成,可以方便地用于自定义:扩展或覆盖默认行为,或增加对新的文件类型的支持。

  • 测试友好化:Coil的基础服务类是 ClassLoader ,它是一个接口,可以方便地编写对应的实现类进行测试;并且Coil同时提供了单例和非单例对象来支持依赖注入。

  • 没有annotation processing:annotation processing一般会降低编译速度,Coil通过Kotlin扩展函数来避免。


Coil目前支持其它图片加载库所包含的所有功能,除此之外它还有一个独特的特性:动态采样(Dynamic image sampling),简而言之就是可以在内存中只缓存了一个低质量的图片而此时需要显示同一个高质量的图片时,Coil可以先把低质量的图片作为 ImageView 的 placeHolder 并同时去磁盘缓存中读取对应的高质量图片最后以“渐进式”的方式替换并最终显示到视图中,例如最常见的从图片列表到预览大图的场景。
以上就是Coil目前的大致介绍,下面我们对Coil的API进行一个简单的使用预览和介绍。


API预览



// 加载一个基本的url(利用了扩展函数,对target无任何侵入)
imageView.load("https://www.website.com/image.jpg")

// Coil 支持加载 urls, uris, resources, drawables, bitmaps, files 等等
imageView.load(R.drawable.image)
imageView.load(File("/path/to/image.jpg"))
imageView.load(Uri.parse("content://com.android.externalstorage/image.jpg"))

// Requests的配置项可以通过load的lambda参数方式实现
imageView.load("https://www.website.com/image.jpg") {
crossfade(true)
placeholder(R.drawable.image)
transformations(CircleCropTransformation())
}

// 自定义targets,包含开始、成功和失败的回调
Coil.load(context, "https://www.website.com/image.jpg") {
target { drawable ->
// Handle the successful result.
}
}

// 通过使用挂起函数get来直接获取图片对象
val drawable = Coil.get("https://www.website.com/image.jpg")

github地址:https://github.com/coil-kt/coil

下载地址:main.zip

收起阅读 »

最快的图像加载库-FastImageCache

FastImageCache快速图像缓存是一种在 iOS 应用程序中存储和检索图像的高效、持久且最重要的快速方式。任何良好的 iOS 应用程序的用户体验的一部分是快速、平滑的滚动,而快速图像缓存有助于使这变得更容易。对于像Path这样的图形丰富的应用程序,性能...
继续阅读 »

FastImageCache

快速图像缓存是一种在 iOS 应用程序中存储和检索图像的高效、持久且最重要的快速方式。任何良好的 iOS 应用程序的用户体验的一部分是快速、平滑的滚动,而快速图像缓存有助于使这变得更容易。

对于像Path这样的图形丰富的应用程序,性能的一个重大负担是图像加载。从磁盘加载单个图像的传统方法太慢了,尤其是在滚动时。Fast Image Cache 就是专门为解决这个问题而创建的。

快速图像缓存的作用

  • 将相似大小和样式的图像存储在一起
  • 将图像数据保存到磁盘
  • 比传统方法更快地将图像返回给用户
  • 根据使用情况自动管理缓存过期
  • 利用基于模型的方法来存储和检索图像
  • 允许在将图像存储到缓存之前按模型处理图像


事实证明,从压缩的磁盘图像数据到用户可以实际看到的渲染核心动画层的过程非常昂贵。随着要显示的图像数量的增加,这种成本很容易导致帧速率显着下降。可滚动视图进一步加剧了这种情况,因为内容可以快速变化,需要快速处理时间才能保持 60FPS 的流畅。1

考虑从磁盘加载图像并将其显示在屏幕上时发生的工作流程:

  1. +[UIImage imageWithContentsOfFile:]使用Image I/OCGImageRef从内存映射数据创建一个此时,图像还没有被解码。
  2. 返回的图像被分配给一个UIImageView.
  3. 隐式CATransaction捕获这些层树修改。
  4. 在主运行循环的下一次迭代中,Core Animation 提交隐式事务,这可能涉及创建已设置为图层内容的任何图像的副本。根据图像,复制它涉及以下部分或全部步骤:2
    1. 缓冲区被分配用于管理文件 IO 和解压操作。
    2. 文件数据从磁盘读取到内存中。
    3. 压缩的图像数据被解码为其未压缩的位图形式,这通常是一个非常占用 CPU 的操作。3
    4. 然后 Core Animation 使用未压缩的位图数据来渲染图层。

这些成本很容易累积并扼杀感知的应用程序性能。特别是在滚动时,给用户呈现的用户体验不尽如人意,与 iOS 的整体体验不符。


解决方案

快速图像缓存使用各种技术最大限度地减少(或完全避免)上述大部分工作:

映射内存

快速图像缓存工作原理的核心是图像表。图像表类似于精灵表,通常用于 2D 游戏。图像表将相同尺寸的图像打包到一个文件中。该文件只打开一次,只要应用程序保留在内存中,就保持打开状态以供读取和写入。

图像表使用mmap系统调用将文件数据直接映射到内存中。没有memcpy发生。这个系统调用只是在磁盘上的数据和内存区域之间创建一个映射。

当请求图像缓存返回特定图像时,图像表(以恒定时间)在它维护的文件中找到所需图像数据的位置。该文件数据区域被映射到内存中,并创建一个新CGImageRef的后备存储映射的文件数据。

当返回的CGImageRef(包装成 a UIImage)准备好被绘制到屏幕上时,iOS 的虚拟内存系统页面中的实际文件数据。这是使用映射内存的另一个好处;VM 系统会自动为我们处理内存管理。此外,映射内存“不计入”应用程序的实际内存使用量。

以类似的方式,当图像数据被存储在图像表中时,会创建一个内存映射位图上下文。与原始图像一起,此上下文被传递到图像表的相应实体对象。该对象负责将图像绘制到当前上下文中,可选地进一步配置上下文(例如,将上下文裁剪为圆角矩形)或进行任何额外的绘制(例如,在原始图像上绘制叠加图像)。mmap将绘制的图像数据编组到磁盘,因此不会在内存中分配图像缓冲区。

未压缩的图像数据

为了避免昂贵的图像解压缩操作,图像表将未压缩的图像数据存储在它们的文件中。如果源图像被压缩,则必须首先解压缩图像表才能使用它。这是一次性成本。此外,可以利用图像格式系列为一组相似的图像格式只执行一次这种解压缩。

然而,这种方法有明显的后果。未压缩的图像数据需要更多的磁盘空间,压缩和未压缩文件大小之间的差异可能很大,尤其是对于 JPEG 等图像格式。出于这个原因,快速图像缓存最适用于较小的图像,尽管没有强制执行此操作的 API 限制。

字节对齐

对于高性能滚动,Core Animation 能够使用图像而无需首先创建副本是至关重要的。Core Animation 创建图像副本的原因之一是图像底层CGImageRef正确对齐的每行字节值必须是 的倍数8 pixels × bytes per pixel对于典型的 ARGB 图像,对齐的每行字节值是 64 的倍数。每个图像表都配置为从一开始就为 Core Animation 始终正确地对每个图像进行字节对齐。因此,当从图像表中检索图像时,它们已经处于 Core Animation 可以直接使用的形式,而无需创建副本。

为了便于项目集成,Fast Image Cache 可作为CocoaPod 使用

手动

创建图像格式

每个图像格式对应一个图像缓存将使用的图像表。可以使用相同的源图像来渲染它们存储在图像表中的图像的图像格式应该属于相同的图像格式系列有关如何确定适当的最大计数的更多信息,请参阅图像表大小

静态 NSString *XXImageFormatNameUserThumbnailSmall = @" com.mycompany.myapp.XXImageFormatNameUserThumbnailSmall " ;
静态 NSString *XXImageFormatNameUserThumbnailMedium = @" com.mycompany.myapp.XXImageFormatNameUserThumbnailMedium " ;
静态 NSString *XXImageFormatFamilyUserThumbnails = @" com.mycompany.myapp.XXImageFormatFamilyUserThumbnails " ;

FICImageFormat *smallUserThumbnailImageFormat = [[FICImageFormat
alloc ] init ];
smallUserThumbnailImageFormat.name = XXImageFormatNameUserThumbnailSmall;

smallUserThumbnailImageFormat.family = XXImageFormatFamilyUserThumbnails;

smallUserThumbnailImageFormat.style = FICImageFormatStyle16BitBGR;

smallUserThumbnailImageFormat.imageSize = CGSizeMake(
50 , 50 );
smallUserThumbnailImageFormat.maximumCount =
250 ;
smallUserThumbnailImageFormat.devices = FICImageFormatDevicePhone;

smallUserThumbnailImageFormat.protectionMode = FICImageFormatProtectionModeNone;


FICImageFormat *mediumUserThumbnailImageFormat = [[FICImageFormat
alloc ] init ];
mediumUserThumbnailImageFormat.name = XXImageFormatNameUserThumbnailMedium;

mediumUserThumbnailImageFormat.family = XXImageFormatFamilyUserThumbnails;

mediumUserThumbnailImageFormat.style = FICImageFormatStyle32BitBGRA;

mediumUserThumbnailImageFormat.imageSize = CGSizeMake(
100 , 100 );
mediumUserThumbnailImageFormat.maximumCount =
250 ;
mediumUserThumbnailImageFormat.devices = FICImageFormatDevicePhone;

mediumUserThumbnailImageFormat.protectionMode = FICImageFormatProtectionModeNone;


NSArray *imageFormats = @[smallUserThumbnailImageFormat, mediumUserThumbnailImageFormat];

配置图像缓存

一旦定义了一种或多种图像格式,就需要将它们分配给图像缓存。除了分配图像缓存的委托之外,没有其他可以在图像缓存本身上配置的内容。

FICImageCache *sharedImageCache = [FICImageCache sharedImageCache ];
sharedImageCache.delegate = self;

sharedImageCache.formats = imageFormats;


实体是符合FICEntity协议的对象实体唯一标识图像表中的条目,并且它们还负责绘制它们希望存储在图像缓存中的图像。已经定义了模型对象(可能由 Core Data 管理)的应用程序通常是合适的候选实体。

@interface  XXUser : NSObject 

@property ( nonatomic , assign , getter = isActive) BOOL active;
@property ( nonatomic , copy ) NSString *userID;
@属性非原子副本NSURL * userPhotoURL;

@结尾

这是该FICEntity协议的示例实现

- ( NSString *)UUID {
CFUUIDBytes UUIDBytes = FICUUIDBytesFromMD5HashOfString (_userID);
NSString *UUID = FICStringWithUUIDBytes (UUIDBytes);

返回UUID;
}


- (
NSString *)sourceImageUUID {
CFUUIDBytes sourceImageUUIDBytes = FICUUIDBytesFromMD5HashOfString ([_userPhotoURL absoluteString ]);
NSString *sourceImageUUID = FICStringWithUUIDBytes (sourceImageUUIDBytes);

返回sourceImageUUID;
}


- (
NSURL *)sourceImageURLWithFormatName:( NSString *)formatName {
return _sourceImageURL;
}


- (FICEntityImageDrawingBlock)drawingBlockForImage:(UIImage *)image withFormatName:(
NSString *)formatName {
FICEntityImageDrawingBlockdrawingBlock = ^(
CGContextRef context, CGSize contextSize) {
CGRect contextBounds = CGRectZero ;
上下文边界。
大小= 上下文大小
CGContextClearRect(上下文,contextBounds);

//剪辑中等缩略图,使它们具有圆角
if ([formatName isEqualToString: XXImageFormatNameUserThumbnailMedium]) {
UIBezierPath clippingPath = [
self _clippingPath ];
[剪辑
路径添加剪辑];
}


UIGraphicsPushContext(上下文);
[图像
drawInRect: contextBounds];
UIGraphicsPopContext ();
};


返回绘图块;
}

理想情况下,实体的UUID永远不应该改变。这就是为什么在应用程序使用从 API 检索的资源的情况下,它与模型对象的服务器生成的 ID 很好地对应。

一个实体的可以改变。例如,如果用户更新了他们的个人资料照片,则该照片的 URL 也应更改。保持不变,标识相同的用户,但改变了个人资料照片网址将表明,有一个新的源图像。sourceImageUUID UUID

注意:通常,最好对用于定义UUID和 的任何标识符进行哈希处理sourceImageUUIDFast Image Cache 提供了实用功能来执行此操作。由于散列可能很昂贵,因此建议仅计算一次散列(或仅在标识符更改时)并存储在实例变量中。

当要求图像缓存为特定实体和格式名称提供图像时,该实体负责提供 URL。URL 甚至不需要指向实际资源——例如,URL 可能由自定义 URL 方案构成——但它必须是一个有效的 URL。

图像缓存仅使用这些 URL 来跟踪哪些图像请求已经在进行中;正确处理对同一图像的图像缓存的多个请求,而不会浪费任何精力。选择使用 URL 作为键控图像缓存请求的基础实际上补充了许多实际应用程序设计,其中图像资源(而不是图像本身)的 URL 包含在服务器提供的模型数据中。

注意:快速图像缓存不提供任何网络请求机制。这是图像缓存委托的责任。

最后,一旦源图像可用,实体就会被要求提供一个绘图块。将存储最终图像的图像表设置文件映射位图上下文并调用实体的绘图块。这使得每个实体可以方便地决定如何处理特定图像格式的源图像。

从图像缓存中请求图像

快速图像缓存在 Cocoa 常见的按需、延迟加载设计模式下工作。

XXUser *user = [self _currentUser];
NSString *formatName = XXImageFormatNameUserThumbnailSmall;
FICImageCacheCompletionBlock completionBlock = ^(id <FICEntity> entity, NSString *formatName, UIImage *image) {
_imageView.image = image;
[_imageView.layer addAnimation:[CATransition animation] forKey:kCATransition];
};

BOOL imageExists = [sharedImageCache retrieveImageForEntity:user withFormatName:formatName completionBlock:completionBlock];

if (imageExists == NO) {
_imageView.image = [self _userPlaceholderImage];
}

统计数据

以下统计数据是从演示应用程序的运行中测得的:

方法滚动性能磁盘使用情况RPRVT 1
传统的~35FPS568KB2.40MB1.06MB+1.34MB
快速图像缓存~59FPS2.2MB1.15MB1.06MB+0.09MB


demo下载及常见问题:https://github.com/path/FastImageCache

源码下载:FastImageCache-master.zip


收起阅读 »

项目想美观么?试试它吧!!自定义加载视图:mkloader

美丽流畅的自定义加载视图 使用<com.tuyenmonkey.mkloader.MKLoader android:layout_width="wrap_content" android:layout_heigh...
继续阅读 »

美丽流畅的自定义加载视图



使用

<com.tuyenmonkey.mkloader.MKLoader
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:mk_type="<loading_type>" (Optional. Default is ClassicSpinner)
app:mk_color="<color>" (Optional. Default is #ffffff)
/>

类型


  • Sharingan
  • TwinFishesSpinner
  • ClassicSpinner
  • LineSpinner
  • FishSpinner
  • PhoneWave
  • ThreePulse
  • FourPulse
  • FivePulse
  • Worm
  • Whirlpool
  • Radar

安装

Gradle


dependencies {
compile 'com.tuyenmonkey:mkloader:1.4.0'
}

github地址:https://github.com/nntuyen/mkloader


下载地址:master.zip


收起阅读 »

日!!聊天页面还能这么简单??ChatKit

ChatKit 是一个免费且开源的 UI 聊天组件,由 LeanCloud 官方推出,底层聊天服务基于 LeanCloud 的 IM 即时通讯服务 LeanMessage 而开发。它的最大特点是把聊天常用的一些功能配合 UI 一起提供给开发者,帮助开发者快速集...
继续阅读 »

ChatKit 是一个免费且开源的 UI 聊天组件,由 LeanCloud 官方推出,底层聊天服务基于 LeanCloud 的 IM 即时通讯服务 LeanMessage 而开发。它的最大特点是把聊天常用的一些功能配合 UI 一起提供给开发者,帮助开发者快速集成 IM 服务,轻松实现聊天功能。
+


ChatKit 开源且提供完全自由的授权协议,开发者可以对其进行任意修改、扩展和二次封装,但是 LeanCloud 并不对 ChatKit 的二次开发提供技术支持。

+


获取项目


git clone git@github.com:leancloud/LeanCloudChatKit-Android.git

运行 Demo


获取源代码之后,用 Android Studio 打开项目,左侧的 Project 视图显示为:

+




「ChatKit-Android」Project 包含两个模块:

+



  • leancloudchatkit

    是一个封装了 LeanCloud 即时通讯的 UI lib,其目的是让开发者更快速地接入 LeanCloud 即时通讯的功能。

  • chatkitapplication

    为 Demo 项目,它是一个简单的示例项目,用来指导开发者如何使用 leancloudchatkit。


然后,请确保电脑上已经连接了一台真机或者虚拟机用作调试。

+


点击 Debug 或者 Run,第一次启动会运行 Gradle Build。建议打开全局网络代理,否则 Gradle Build 可能会因为网络原因无法完成。

+


使用方法


开发者可以将 ChatKit 导入到自己的 Project 中使用。下面我们将新建一个 Project(名为 ChatDemo) 用以导入 ChatKit。导入方式推荐通过源代码导入

+


源代码导入



  1. 浏览器访问 https://github.com/leancloud/LeanCloudChatKit-Android

  2. 执行以下命令行,将项目 clone 到本地(如 ChatKit 文件夹中,或者直接下载 zip 包自行解压缩到此文件夹下):
    git clone https://github.com/leancloud/LeanCloudChatKit-Android.git`


  3. 将文件夹 leancloudchatkit 复制到 ChatDemo 根目录;

  4. 修改 ChatDemo/settings.gradle 加入 include ':leancloudchatkit'

  5. 修改 ChatDemo/app/build.gradle,在 dependencies 中添加 compile project(":leancloudchatkit")


最后只要 Sync Project,这样 ChatKit 就算是导入到项目中了。

+


自定义使用


一、实现自己的 Application

+


ChatKit 在使用之前需要进行初始化,就像直接使用 LeanCloud 基础 SDK 时需要调用 AVOSCloud.initialize(appId, appKey) 一样。初始化逻辑应该放在 Application.onCreate 方法中实现。

+


ChatDemo 中新建一个 Class,名字叫做 ChatDemoApplication,让它继承自 Application 类,代码如下:

+


public class ChatDemoApplication extends Application {

// appId、appKey 可以在「LeanCloud 控制台 > 设置 > 应用 Key」获取
private final String APP_ID = "********";
private final String APP_KEY = "********";

@Override
public void onCreate() {
super.onCreate();
// 关于 CustomUserProvider 可以参看后面的文档
LCChatKit.getInstance().setProfileProvider(CustomUserProvider.getInstance());
LCChatKit.getInstance().init(getApplicationContext(), APP_ID, APP_KEY);
}
}

二、在 AndroidMainfest.xml 中配置 ChatDemoApplication

+


<application
...
android:name=".ChatDemoApplication" >
...
</application>

三、实现自己的用户体系

+


一般来说,聊天界面要相对复杂一些,不但要支持文字、表情、图片、语音等消息格式,还要展示用户信息,比如用户的头像、昵称等。而 LeanCloud 的消息流中只包含用户的 clientId 这一唯一标识,所以要获取头像这类额外的用户信息,就需要开发者接入自己的用户系统来实现。

+


为了保证通用性和扩展性,让开发者可以更容易将聊天界面嵌入自己的应用中,ChatKit 在设计上抽象出了一个「用户体系」的接口,需要开发者自己提供用户信息的获取方式。该接口只有一个方法需要开发者实现:

+


/**
* 用户体系的接口,开发者需要实现此接口来接入 LCChatKit
*/

public interface LCChatProfileProvider {
// 根据传入的 clientId list,查找、返回用户的 Profile 信息(id、昵称、头像)
public void fetchProfiles(List<String> userIdList, LCChatProfilesCallBack profilesCallBack);
}

为此,我们在 ChatDemo 中新建一个 Class,名字叫做 CustomUserProvider,代码如下:

+


public class CustomUserProvider implements LCChatProfileProvider {

private static CustomUserProvider customUserProvider;

public synchronized static CustomUserProvider getInstance() {
if (null == customUserProvider) {
customUserProvider
= new CustomUserProvider();
}
return customUserProvider;
}

private CustomUserProvider() {
}

private static List<LCChatKitUser> partUsers = new ArrayList<LCChatKitUser>();

// 此数据均为模拟数据,仅供参考
static {
partUsers
.add(new LCChatKitUser("Tom", "Tom", "http://www.avatarsdb.com/avatars/tom_and_jerry2.jpg"));
partUsers
.add(new LCChatKitUser("Jerry", "Jerry", "http://www.avatarsdb.com/avatars/jerry.jpg"));
partUsers
.add(new LCChatKitUser("Harry", "Harry", "http://www.avatarsdb.com/avatars/young_harry.jpg"));
partUsers
.add(new LCChatKitUser("William", "William", "http://www.avatarsdb.com/avatars/william_shakespeare.jpg"));
partUsers
.add(new LCChatKitUser("Bob", "Bob", "http://www.avatarsdb.com/avatars/bath_bob.jpg"));
}

@Override
public void fetchProfiles(List<String> list, LCChatProfilesCallBack callBack) {
List<LCChatKitUser> userList = new ArrayList<LCChatKitUser>();
for (String userId : list) {
for (LCChatKitUser user : partUsers) {
if (user.getUserId().equals(userId)) {
userList
.add(user);
break;
}
}
}
callBack
.done(userList, null);
}

public List<LCChatKitUser> getAllUsers() {
return partUsers;
}
}

当用户昵称和头像需要更新时,需要覆盖旧的 LCChatKitUser 对象并更新本地缓存:

+


    LCChatKitUser user = new LCChatKitUser("唯一 userId 不可变", "要变更的昵称", "要变更的 avatarURL");
LCIMProfileCache.getInstance().cacheUser(user);

四、打开即时通讯,并且跳转到聊天页面

+


我们支持通过两种方式来打开聊天界面:

+



  1. 通过指定另一个参与者的 clientId 的方式,开启一对一的聊天;

    此时,通过调用 intent.putExtra(LCIMConstants.PEER_ID, "peermemberId") 来传递另一参与者的 clientId。

  2. 通过指定一个已经存在的 AVIMConversation id 的方式,开启单人、多人或者开放式聊天室;

    此时,通过调用 LCIMConstants.CONVERSATION_ID, "particularConversationId") 来传递特定对话 Id。


下面的代码展示了如何通过第一种方式来开启聊天界面:

+


LCChatKit.getInstance().open("Tom", new AVIMClientCallback() {
@Override
public void done(AVIMClient avimClient, AVIMException e) {
if (null == e) {
finish
();
Intent intent = new Intent(MainActivity.this, LCIMConversationActivity.class);
intent
.putExtra(LCIMConstants.PEER_ID, "Jerry");
startActivity
(intent);
} else {
Toast.makeText(MainActivity.this, e.toString(), Toast.LENGTH_SHORT).show();
}
}
});

这样,Tom 就可以和 Jerry 愉快地聊天了。

+


接口以及组件


以下介绍在 ChatKit 中开发者常需要关注的业务逻辑组件(Interface)和界面组件(UI)。

+


用户


LCChatKitUser 是 ChatKit 封装的参与聊天的用户,它提供了如下属性:

2+























名称 描述
userId 用户在单个应用内唯一的 ID,也是调用 LCChatKit.open 时传入的 userId。
avatarUrl 用户头像的 URL
name 用户名

使用这些默认的属性基本可以满足一个聊天应用的需求,同时开发者可以通过继承 LCChatKitUser 类实现更多属性。具体用法请参考 Demo 中的 MembersAdapter.java

+


用户信息管理类


LCChatProfileProvider 接口需要用户 implements 后实现 fetchProfiles 函数,以使 ChatKit 在需要显示的时候展示用户相关的信息。

+


例如 Demo 中的 CustomUserProvider 这个类,它实现了 LCChatProfileProvider 接口,在 fetchProfiles 方法里加载了 Tom、Jerry 等人的信息。

+


核心类


LCChatKit 是 ChatKit 的核心类,具体逻辑可以参看代码,注意以下几个主要函数:

+



public void init(Context context, String appId, String appKey)

此函数用于初始化 ChatKit 的相关设置,此函数要在 Application 的 onCreate 中调用,否则可能会引起异常。

public void setProfileProvider(LCChatProfileProvider profileProvider)

此函数用于设置用户体系,因为 LeanCloud 即时通讯功能已经实现了完全剥离用户体系的功能,这里接入已有的用户体系会很方便。

public void open(final String userId, final AVIMClientCallback callback)

此函数用于开始即时通讯,open 成功后可以执行自己的逻辑,或者跳转到聊天页面。



对话列表界面


对话 AVIMConversation 是 LeanMessage 封装的用来管理对话中的成员以及发送消息的载体,不论是群聊还是单聊都是在一个对话当中;而对话列表可以作为聊天应用默认的首页显示出来,主流的社交聊天软件,例如微信,就是把最近的对话列表作为登录后的首页。

+


因此,我们也提供了对话列表 LCIMConversationListFragment 页面供开发者使用,在 Demo 项目中的 MainActivity 中的 initTabLayout 方法中演示了如何引入对话列表页面:

+


  private void initTabLayout() {
String[] tabList = new String[]{"会话", "联系人"};
final Fragment[] fragmentList = new Fragment[] {new LCIMConversationListFragment(),
new ContactFragment()};
// 以上这段代码为新建了一个 Fragment 数组,并且把 LCIMConversationListFragment 作为默认显示的第一个 Tab 页面

tabLayout
.setTabMode(TabLayout.MODE_FIXED);
for (int i = 0; i < tabList.length; i++) {
tabLayout
.addTab(tabLayout.newTab().setText(tabList[i]));
}
...
}

具体的显示效果如下:

+




聊天界面


聊天界面是显示频率最高的前端页面,ChatKit 通过 LCIMConversationFragmentLCIMConversationActivity 来实现这一界面。在 Demo 的 ContactItemHolder 界面包含了使用 LCIMConversationActivity 的实例:

+


  public void initView() {
nameView
= (TextView)itemView.findViewById(R.id.tv_friend_name);
avatarView
= (ImageView)itemView.findViewById(R.id.img_friend_avatar);

itemView
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 点击联系人,直接跳转进入聊天界面
Intent intent = new Intent(getContext(), LCIMConversationActivity.class);
// 传入对方的 Id 即可
intent
.putExtra(LCIMConstants.PEER_ID, lcChatKitUser.getUserId());
getContext
().startActivity(intent);
}
});
}

具体的显示效果如下:

+




联系人列表页面


因为 ChatKit 是与应用的用户体系完全解耦的,所以 ChatKit 中并没有包含联系人列表页面,但部分开发者可能有此需求,所以 chatkitapplication 实现了一个基于 LCChatProfileProvider 的联系人列表页面,具体代码可以参考 ContactFragment.java
具体效果如下:

+




常见问题


ChatKit 组件收费么?

ChatKit 是完全开源并且免费给开发者使用,使用聊天所产生的费用以账单为准。

+


接入 ChatKit 有什么好处?

它可以减轻应用或者新功能研发初期的调研成本,直接引入使用即可。ChatKit 从底层到 UI 提供了一整套的聊天解决方案。




收起阅读 »

罕见!!上弹窗:Alerter

alerter克服了toast和snackbar的缺点,并布局很简单 生成为了简单起见,Alerter采用了builder模式,以便于轻松集成到任何应用程序中。 可自定义的警报视图将动态添加到窗口的装饰视图中,覆盖所有内容。 安装配置allprojects {...
继续阅读 »

alerter克服了toast和snackbar的缺点,并布局很简单


生成

为了简单起见,Alerter采用了builder模式,以便于轻松集成到任何应用程序中。


可自定义的警报视图将动态添加到窗口的装饰视图中,覆盖所有内容。


安装配置

allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}

build.gradle


dependencies {
implementation 'com.github.tapadoo:alerter:$current-version'
}

使用


Activity -


Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.show()

Fragment -


Alerter.create(activity)
.setTitle("Alert Title")
.setText("Alert text...")
.show()

展示 -


Alerter.isShowing()

隐藏 -


Alerter.hide()

定制

背景颜色

Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setBackgroundColorRes(R.color.colorAccent) // or setBackgroundColorInt(Color.CYAN)
.show()


图标

Alerter.create(this@DemoActivity)
.setText("Alert text...")
.setIcon(R.drawable.alerter_ic_mail_outline)
.setIconColorFilter(0) // Optional - Removes white tint
.setIconSize(R.dimen.custom_icon_size) // Optional - default is 38dp
.show()


自定义屏幕持续时间(毫秒)

Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setDuration(10000)
.show()

无标题

Alerter.create(this@DemoActivity)
.setText("Alert text...")
.show()


添加 Click Listener

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setDuration(10000)
.setOnClickListener(View.OnClickListener {
Toast.makeText(this@DemoActivity, "OnClick Called", Toast.LENGTH_LONG).show();
})
.show()


长的文本

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("The alert scales to accommodate larger bodies of text. " +
"The alert scales to accommodate larger bodies of text. " +
"The alert scales to accommodate larger bodies of text.")
.show()


自定义进入/退出动画

  Alerter.create(this@KotlinDemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setEnterAnimation(R.anim.alerter_slide_in_from_left)
.setExitAnimation(R.anim.alerter_slide_out_to_right)
.show()

可见性回调

 Alerter.create(this@KotlinDemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.setDuration(10000)
.setOnShowListener(OnShowAlertListener {
Toast.makeText(this@KotlinDemoActivity, "Show Alert", Toast.LENGTH_LONG).show()
})
.setOnHideListener(OnHideAlertListener {
Toast.makeText(this@KotlinDemoActivity, "Hide Alert", Toast.LENGTH_LONG).show()
})
.show()

自定义字体和文本外观

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setTitleAppearance(R.style.AlertTextAppearance_Title)
.setTitleTypeface(Typeface.createFromAsset(getAssets(), "Pacifico-Regular.ttf"))
.setText("Alert text...")
.setTextAppearance(R.style.AlertTextAppearance_Text)
.setTextTypeface(Typeface.createFromAsset(getAssets(), "ScopeOne-Regular.ttf"))
.show()


退出

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.enableSwipeToDismiss()
.show()


Progress Bar

 Alerter.create(this@DemoActivity)
.setTitle("Alert Title")
.setText("Alert text...")
.enableProgress(true)
.setProgressColorRes(R.color.colorAccent)
.show()


带按钮

 Alerter.create(this@KotlinDemoActivity)
.setTitle(R.string.title_activity_example)
.setText("Alert text...")
.addButton("Okay", R.style.AlertButton, View.OnClickListener {
Toast.makeText(this@KotlinDemoActivity, "Okay Clicked", Toast.LENGTH_LONG).show()
})
.addButton("No", R.style.AlertButton, View.OnClickListener {
Toast.makeText(this@KotlinDemoActivity, "No Clicked", Toast.LENGTH_LONG).show()
})
.show()


自定义layout

 Alerter.create(this@KotlinDemoActivity, R.layout.custom_layout)
.setBackgroundColorRes(R.color.colorAccent)
.also { alerter ->
val tvCustomView = alerter.getLayoutContainer()?.tvCustomLayout
tvCustomView?.setText(R.string.with_custom_layout)
}
.show()


github地址:https://github.com/Tapadoo/Alerter
下载地址:master.zip


收起阅读 »

Android 通知栏封装方案

BaseNotification获取此框架 allprojects { repositories { ... maven { url 'https://jitpack.io' } }}dependencies ...
继续阅读 »

BaseNotification

获取此框架 

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

dependencies {
implementation 'com.github.Dboy233:BaseNotification:2.0'
}


如何使用此封装框架?

1.自定义你的Notification继承BaseNotification

public abstract class ChatChannelNotify<ChatData> extends BaseNotification<ChatData> {


public ChatChannelNotify(@NotNull ChatData mData) {
super(mData);
//使用add*函数添加对应自定义的布局 然后使用get*
addContentRemoteViews(R.layout.notify_comm_chat_layout);
}
/**
*渠道名字
*/
@NotNull
@Override
public String getChannelName() {
return NotificationConfig.CHANNEL_ID_CHAT;
}
/**
*渠道id
*/
@NotNull
@Override
public String getChannelId() {
return NotificationConfig.CHANNEL_ID_CHAT;
}

@Override
@RequiresApi(api = Build.VERSION_CODES.O)
public void configureChannel(@NotNull NotificationChannel notificationChannel) {
//配置渠道信息。如果没有这个渠道会创建这个渠道,如果有了,这个函数是不会被调用的
notificationChannel.setLockscreenVisibility(NotificationCompat.VISIBILITY_PUBLIC);
notificationChannel.setImportance(NotificationManager.IMPORTANCE_HIGH);
}


@Override
public void configureNotify(@NotNull NotificationCompat.Builder mBuilder) {
//配置你的通知属性
mBuilder.setShowWhen(true)
.setSmallIcon(getData().getSmallIcon())
.setContentTitle(getData().getContentTitle())
.setContentText(getData().getContentText())
.setTicker(getData().getContentTitle())
.setContentInfo(getData().getContentText())
.setAutoCancel(true)
.setSubText(getData().getContentText())
.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setGroup("chat");
}

/**
*如果你在构造函数使用了add**的函数添加自定义布局 在这里配置你布局展示的数据
*使用**2结尾的函数可链式调用 点击事件做了单独封装
*/
@Override
public void convert(@NotNull BaseRemoteViews mBaseRemoteViews, @NotNull ChatData data) {
ContentRemote contentRemote = mBaseRemoteViews.getContentRemote();
if (contentRemote != null) {
contentRemote
.setImageViewResource2(R.id.notify_chat_head_img, data.getIcon())
.setTextViewText2(R.id.notify_chat_title, data.getContentTitle())
.setTextViewText2(R.id.notify_chat_subtitle, data.getContentText())
.setOnClickPendingIntent2(getNotificationId(), R.id.notify_chat_layout);
}

}
/**
*设置你的通知id
*/
@Override
public int getNotificationId() {
return NotificationConfig.NOTIFICATION_ID;
}

}


2.实例化你的通知并展示

ChatChannelNotify chat=new ChatChannelNotify(new Chat())
chat.show()//显示我们的通知
//更新内容使用 chat.show(new Chat())
//取消通知 chat.cancel()


3.设置点击事件

class ChatActivity : AppCompatActivity(), PendingIntentListener {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//设置点击事件
NotificationControl.addPendingIntentListener(this)
}

/**
*通知点击事件回调
*notifId通知的id
*ViewId点击的viewId
*/
override fun onClick(notifyId: Int, viewId: Int) {
Toast.makeText(this,
"点击事件: notifyId:" + notifyId + " ViewId:" + viewId, Toast.LENGTH_LONG)
.show()
}

override fun onDestroy() {
super.onDestroy()
//销毁点击事件
NotificationControl.removePendingIntentListener(this)
}
}

原文链接:https://github.com/Dboy233/BaseNotification

代码下载:BaseNotification-master.zip



收起阅读 »

Android增量更新

APP自动增量更新抽取的Android自动更新库,目的是几行代码引入更新功能,含服务端代码,欢迎Star,欢迎Fork,谢谢~目录功能介绍流程图效果图与示例apk如何引入更新清单文件简单使用详细说明差分包生成(服务端)依赖License功能介绍 支持...
继续阅读 »

APP自动增量更新

抽取的Android自动更新库,目的是几行代码引入更新功能,含服务端代码,欢迎Star,欢迎Fork,谢谢~

目录

功能介绍

  •  支持全量更新apk,直接升级到最新版本
  •  支持增量更新,只下载补丁包升级
  •  设置仅在wifi环境下更新
  •  支持外部注入网络框架(库默认使用okhttp)
  •  支持前台或后台自动更新
  •  支持基于版本的强制更新
  •  支持对外定制更新提示和更新进度界面
  •  含发布功能后台服务端github (Node.js实现)

流程图

效果图与示例apk

示例1 示例2


如何引入

Gradle引入

step 1

Add the JitPack repository to your build file

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.itlwy:AppSmartUpdate:v1.0.7'
}


Step 2

Add the dependency

更新清单文件

该清单放置在静态服务器以供App访问,主要用于判断最新的版本,及要更新的版本资源信息等(示例见仓库根目录下的resources目录或直接访问后台代码 github),清单由服务端程序发布apk时生成,详见后台示例:github

{
"minVersion": 100, // app最低支持的版本代码(包含),低于此数值的app将强制更新
"minAllowPatchVersion": 100, // 最低支持的差分版本(包含),低于此数值的app将采取全量更新,否则采用差量
"newVersion": 101, // 当前最新版本代码
"tip": "test update", // 更新提示
"size": 1956631, // 最新apk文件大小
"apkURL": "https://raw.githubusercontent.com/itlwy/AppSmartUpdate/master/resources/app/smart-update.apk", // 最新apk 绝对url地址,也可用相对地址,如下方的"patchURL"字段
"hash": "ea97c8efa490a2eaf7d10b37e63dab0e", // 最新apk文件的md5值
"patchInfo": { // 差分包信息
"v100": { // v100表示-版本代码100的apk需要下载的差分包
"patchURL": "v100/100to101.patch", //差分包地址,相对此UpdateManifest.json文件的地址,也可用绝对地址
"tip": "101 version", // 提示
"hash": "ea97c8efa490a2eaf7d10b37e63dab0e", // 合成后apk(即版本代码101)的文件md5值
"size": 1114810 // 差分包大小
}
}
}


简单使用

1.初始化

public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
//推荐在Application中初始化
Config config = new Config.Builder()
.isDebug(true)
.build(this);
UpdateManager.getInstance().init(config);
}
}


2.调用

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button mUpdateBtn;
private String manifestJsonUrl = "https://raw.githubusercontent.com/itlwy/AppSmartUpdate/master/resources/UpdateManifest.json";
private IUpdateCallback mCallback;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mUpdateBtn = (Button) findViewById(R.id.update_btn);
mUpdateBtn.setOnClickListener(this);

}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.update_btn:
UpdateManager.getInstance().update(this, manifestJsonUrl, null);
break;

}
}
}


详细说明

注册通知回调

  • 其他activity界面需要获知后台更新情况
public void register(IUpdateCallback callback) {...}

public void unRegister(IUpdateCallback callback) {...}

public interface IUpdateCallback {

/**
* 通知无新版本需要更新,运行在主线程
*/
void noNewApp();

/**
* 自动更新准备开始时回调,运行在主线程,可做一些提示等
*/
void beforeUpdate();

/**
* 自动更新的进度回调(分增量和全量更新),运行在主线程
*
* @param percent 当前总进度百分比
* @param totalLength 更新总大小(全量为apk大小,增量为全部补丁大小和)
* @param patchIndex 当前更新的补丁索引(从1开始)
* @param patchCount 需要更新的总补丁数(当为0时表示是增量更新)
*/
void onProgress(int percent, long totalLength, int patchIndex, int patchCount);

/**
* 下载完成,准备更新,运行在主线程
*/
void onCompleted();

/**
* 异常回调,运行在主线程
*
* @param error 异常信息
*/
void onError(String error);

/**
* 用户取消了询问更新对话框
*/
void onCancelUpdate();

/**
* 取消了更新进度对话框,压入后台自动更新,此时由通知栏通知进度
*/
void onBackgroundTrigger();
}


网络框架注入

默认使用okhttp,也可由外部注入,只需实现如下的IHttpManager接口,然后通过new Config.Builder().httpManager(new OkhttpManager())注入即可

public interface IHttpManager {


IResponse syncGet(@NonNull String url, @NonNull Map<String, String> params) throws IOException;

/**
* 异步get
*
* @param url get请求地址
* @param params get参数
* @param callBack 回调
*/
void asyncGet(@NonNull String url, @NonNull Map<String, String> params, @NonNull Callback callBack);


/**
* 异步post
*
* @param url post请求地址
* @param params post请求参数
* @param callBack 回调
*/
void asyncPost(@NonNull String url, @NonNull Map<String, String> params, @NonNull Callback callBack);

/**
* 下载
*
* @param url 下载地址
* @param path 文件保存路径
* @param fileName 文件名称
* @param callback 回调
*/
void download(@NonNull String url, @NonNull String path, @NonNull String fileName, @NonNull FileCallback callback);
}


定制更新交互界面

每个应用的风格都可能是不一样的,因此这里也支持自定义弹出的提示框和进度框,详细见如下代码示例:

  1. 初始化config时需要将内部默认的弹框屏蔽掉

public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
Config config = new Config.Builder()
.isShowInternalDialog(false)
.build(this);
UpdateManager.getInstance().init(config);
}
}


  1. 自定义对话框,如下(详细代码在MainActivity.java里):

public void registerUpdateCallbak() {
mCallback = new IUpdateCallback() {
@Override
public void noNewApp() {
Toast.makeText(MainActivity.this, "当前已是最新版本!", Toast.LENGTH_LONG).show();
}

@Override
public void hasNewApp(AppUpdateModel appUpdateModel, UpdateManager updateManager, final int updateMethod) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
mDialog = builder.setTitle("自动更新提示")
.setMessage(appUpdateModel.getTip())
.setPositiveButton("更新", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
UpdateManager.getInstance().startUpdate(updateMethod);
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

}
}).create();
mDialog.show();
}

@Override
public void beforeUpdate() {
// 更新开始
mProgressDialog = new ProgressDialog(MainActivity.this);
mProgressDialog.setTitle("更新中...");
mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
mProgressDialog.setMessage("正在玩命更新中...");
mProgressDialog.setMax(100);
mProgressDialog.setProgress(0);
mProgressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
// 退到后台自动更新,进度由通知栏显示
if (UpdateManager.getInstance().isRunning()) {
UpdateManager.getInstance().onBackgroundTrigger();
}
}
});
mProgressDialog.show();
}

@Override
public void onProgress(int percent, long totalLength, int patchIndex, int patchCount) {
String tip;
if (patchCount > 0) {
tip = String.format("正在下载补丁%d/%d", patchIndex, patchCount);
} else {
tip = "正在下载更新中...";
}
mProgressDialog.setProgress(percent);
mProgressDialog.setMessage(tip);
}

@Override
public void onCompleted() {
mProgressDialog.dismiss();
}

@Override
public void onError(String error) {
Toast.makeText(MainActivity.this, error, Toast.LENGTH_LONG).show();
mProgressDialog.dismiss();
}

@Override
public void onCancelUpdate() {

}

@Override
public void onBackgroundTrigger() {
Toast.makeText(MainActivity.this, "转为后台更新,进度由通知栏提示!", Toast.LENGTH_LONG).show();
}
};
UpdateManager.getInstance().register(mCallback);
}


差分包合成(jni)

此部分采用的差分工具为开源bsdiff,用于生成.patch补丁文件,采用jni方式封装一个.so库供java调用,详见"smartupdate"库里的main/cpp目录源码,过程比较简单,就是写个jni的方法来直接调用bsdiff库,目录结构如下:

main
    -cpp
        -bzip2
-CMakeLists.txt
-patchUtils.c
-patchUtils.h
-update-lib.cpp

因为bsdiff还依赖了bzip2,所以这里涉及多个源文件编译链接问题,需要在CMakeLists.txt稍作修改:

# 将当前 "./src/main/cpp" 目录下的所有源文件保存到 "NATIVE_SRC" 中,然后在 add_library 方法调用。
aux_source_directory( . NATIVE_SRC )
# 将 "./src/main/cpp/bzip2" 目录下的子目录bzip2保存到 "BZIP2_BASE" 中,然后在 add_library 方法调用。
aux_source_directory( ./bzip2 BZIP2_BASE )
# 将 BZIP2_BASE 增加到 NATIVE_SRC 中,这样目录的源文件也加入了编译列表中,当然也可以不加到 NATIVE_SRC,直接调用add_library。
list(APPEND NATIVE_SRC ${BZIP2_BASE})

add_library( # Sets the name of the library.
update-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${NATIVE_SRC})


差分包生成

服务端见github ,使用时将manifestJsonUrl改成部署的服务器地址即可,如下示例代码片段的注释处

public class MainActivity extends AppCompatActivity {
private String manifestJsonUrl = "https://raw.githubusercontent.com/itlwy/AppSmartUpdate/master/resources/UpdateManifest.json";
// private String manifestJsonUrl = "http://192.168.2.107:8000/app/UpdateManifest.json";
...
}


依赖

  • okhttp : com.squareup.okhttp3:okhttp:3.11.0
  • gson : com.google.code.gson:gson:2.8.0
收起阅读 »

OpenCV二维码扫码优化

说明在介绍二维码的优化前,可以参考二维码基础原理了解二维码识别的相关知识。 作者相关博客Android二维码扫描优化Contents 目录概述普通优化解码优化优化相机设置难点灰色角度光照优化尝试1尝试2尝试3项目说明概述随着二维码的流行,几乎所有手持设备都支持...
继续阅读 »

说明

在介绍二维码的优化前,可以参考二维码基础原理了解二维码识别的相关知识。 作者相关博客Android二维码扫描优化

Contents 目录

概述

随着二维码的流行,几乎所有手持设备都支持二维码的扫描识别。对于大部分普通的情景下,使用zxing和zbar就可以很好的完成二维码的识别,但是如果碰到一些特殊的情景,zxing和zbar的识别率并不高。这些特殊的情景包括:

  1. 二维码是灰色的
  2. 大角度斜扫二维码
  3. 由光源引发的摄像头干扰,比如手机扫描屏幕上的二维码会出现条纹或者噪点

普通优化

在介绍难点优化之前,先介绍网上常见的优化点。这些优化点同样被使用在了作者的项目中,具体包括:

解码优化

  1. 减少解码格式
  2. 解码算法优化
  3. 减少解码数据
  4. 解码库Zbar与Zxing融合

优化相机设置

  1. 选择最佳预览尺寸/图片尺寸
  2. 设置适合的相机放大倍数
  3. 调整聚焦时间
  4. 设置自动对焦区域
  5. 调整合理扫描区域

以上普通优化均参考自智能设备上的二维码解码优化, 不过作者在实现过程中,对该链接所对应的项目实现做了一些修改,修改点包括:

  1. 旋转android后置摄像头preview数据时,根据yuv NV21数据的特点进行旋转
  2. 摄像头预览数据中,对应预览框的数据的截取,并非通过PlanarYUVLuminanceSource创建时提供的rect进行截取。而是通过opencv,将yuv数据转换成rgb之后,在进行截取
  3. 摄像头最佳预览尺寸选取时,按照设备屏宽,屏高最接近的方式选取,并没有通过屏幕宽高比进行筛选。因为部分android手机,屏幕的宽高比和摄像头所支持最接近的size的宽高比并不一致(ps:比如nexus 4,屏幕宽高为720/1280, 但是摄像头支持的最接近屏幕宽高比的preview size,都要少几个像素,印象中,好像是 716/1276, 具体多少记不清了)
  4. 支持用户双指对摄像头进行缩放,并未采用链接引用项目的zoom策略
  5. 添加zbar结合zxing一起检测的代码

难点

整个二维码扫描识别过程中,个人认为最关键的一步在于图片的二值化处理。所谓二值化,可以简单理解为给定一个阈值,低于阈值的像素标记为白色,高于阈值的像素标记为黑色。一旦进行二值化后,图片形成黑白两色图,再利用二维码原理进行定位查询并识别解码并不困难。因此能否对一个图片进行正确的二值化极大影响二维码识别率。而以下特殊情况很大程度上会干扰扫描过程中二值化过程:

灰色二维码

如上图,某些二维码是灰色的,或者由于光照的情况下色调比较淡。灰色二维码识别难度在于背景往往比二维码的灰点色度更黑,当用手机对这类灰色二维码进行扫描时,总是或多或少会有一些非二维码的背景显示在扫描框中,而这部分背景由于色调比灰点深,这会造成zxing,或者zbar二值化过程中选择阈值受到干扰,从而无法识别二维码

大角度对二维码进行扫描识别

在这种情况下如果是纯黑色的二维码影响不会太大,但如果是灰色的二维码,就非常容易受到光照的印象。 由于是斜着扫二维码,那么对于摄像头来说,二维码的两边所带来的亮度是不同的。

 

如上图例子,如果是从左侧进行扫描二维码,那么左边由于距离摄像头更近,因此更亮,而右边由于距离摄像头更远,会比较暗,此时整个预览框进行二值化时,非常容易造成右边暗的部分,全部超过阈值而被判断为黑色,进而极大影响二维码的识别

由光源引发的摄像头干扰,比如手机扫描屏幕上的二维码会出现条纹或者噪点

如果二维码位于电脑屏幕中,或者位于其他投影设备里,或者当时处于白炽灯等灯光照耀的情况下,进行扫描,摄像头有时候会受到光源频率的干扰,从而形成条纹状。而某些情况下,当摄像头zoom到一定程度是,形成的早点也会对二维码识别造成极大干扰,如下图所示作者遇到的情况:

 

这类情况,如果二维码是黑色的还好,如果二维码是灰色的,则条纹会对二维码的识别起到很大的干扰,如上边右侧图像所示

优化

针对上述难点,作者尝试通过opencv对相机的preview数据进行预处理,从而提升二维码的扫描识别,作者进行过的尝试如下:

尝试1:通过opencv方法进行预览数据的降噪

作者本想通过opencv的各种降噪滤波算法(例如mediaBlur等),去光照算法(例如RGB归一化等)等对camera预览数据进行处理。可能是作者对于opencv算法理解还不足,也可能是其他原因,测试下来效果都不太好。最好作者选择mediaBlur作为降噪算法进行保留,对于一些椒盐噪点的去除还挺有帮助

尝试2:缩小二值化阈值计算范围

考虑到灰色二维码之所以难以识别是因为背景的色调过暗,拉低了二值化阈值的计算,导致整个二维码的难以识别。因此作者将二值化阈值的计算区域缩小到预览框的2/5,然后再通过计算的阈值对整个预览区域的数据进行二值化。这个效果对于灰色二维码的识别提升很大。可惜的是,只对正面扫描的情况下有用,一旦斜着超过一定角度扫描灰色二维码,往往识别不出。

尝试3:分块对预览区进行二值化

考虑到斜扫二维码不好识别因为矩形预览区域各个部分的亮度不统一,因此作者下一步尝试是,将矩形预览框再次分成4个块,每个块取其靠近中心部分的1/5区域进行阈值计算,最后分块进行二值化后,拼接而成。具体如下示意图所示

之所以分块进行计算阈值,是考虑到不同区域的亮度是不同的,如果整体进行计算的话,会丢失部分有效信息。而之所以选取靠近中心的小部分进行阈值化计算,是因为用户行为通常会将二维码对准预览框的中心,因此中心部分,包含有效亮度信息更为精确,减少背景亮度对整体二值化的影响。这一步尝试之后,大大提升了特殊情景二维码的识别概率。至此,opencv预处理部分到此结束了。

项目说明

项目分成3个module

  • app app module提供demo activity

  • library_qrscan 二维码扫码核心功能,集成zxing,zbar,opencv预处理等功能

  • library_opencv 该module作为隐藏module,setting配置并没有include它,是方便调试opencv功能。该module可以用于生成libProcessing.so,集成在library_qrscan module中。如果用户想使用该module,请按照以下做法:

    1. 打开setting中配置,include该module
    2. 下载opencv for android版本到本地目录,并解压
    3. 修改library_opencv/src/main/jni/Android.mk 中 12行include对应本地opencv的mk路径
    4. 删除library_qrscan/libs/armeabi-v7a/libProcessing.so


收起阅读 »

图片选择器:Matisse

Matisse 是一个为Android精心设计的本地图像和视频选择器。你可以 在活动或片段中使用它 选择包含JPEG、PNG、GIF的图像和包含MPEG、MP4的视频 应用不同的主题,包括两个内置主题和自定义主题 不同的图像加载器 定义自定义筛选规则 ...
继续阅读 »

Matisse 是一个为Android精心设计的本地图像和视频选择器。你可以



  • 在活动或片段中使用它


  • 选择包含JPEG、PNG、GIF的图像和包含MPEG、MP4的视频


  • 应用不同的主题,包括两个内置主题和自定义主题


  • 不同的图像加载器


  • 定义自定义筛选规则















Zhihu Style Dracula Style Preview




下载使用

Gradle:


repositories {
jcenter()
}

dependencies {
implementation 'com.zhihu.android:matisse:$latest_version'
}

混淆

如果用的是 Glide
添加规则:

-dontwarn com.squareup.picasso.**

如果用的是 Picasso 添加规则:


-dontwarn com.bumptech.glide.**

怎样使用他呢?

权限


  • android.permission.READ_EXTERNAL_STORAGE
  • android.permission.WRITE_EXTERNAL_STORAGE

所以,如果您的目标是android6.0+,那么您需要在下一步之前处理运行时权限请求。


Simple usage snippet



启动 MatisseActivity 从 活动的 Activity or Fragment:


Matisse.from(MainActivity.this)
.choose(MimeType.allOf())
.countable(true)
.maxSelectable(9)
.addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K))
.gridExpectedSize(getResources().getDimensionPixelSize(R.dimen.grid_expected_size))
.restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)
.thumbnailScale(0.85f)
.imageEngine(new GlideEngine())
.showPreview(false) // Default is `true`
.forResult(REQUEST_CODE_CHOOSE);

主题

有两个内置的主题可以用来开始 MatisseActivity:



  • R.style.Matisse_Zhihu (白天模式)
  • R.style.Matisse_Dracula (夜间模式)

你也可以随心所欲地定义自己的主题。


接收结果

onActivityResult() 回调 Activity or Fragment:


List<Uri> mSelected;

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CHOOSE && resultCode == RESULT_OK) {
mSelected = Matisse.obtainResult(data);
Log.d("Matisse", "mSelected: " + mSelected);
}
}

github地址:https://github.com/zhihu/matisse
下载地址:matisse-master.zip


收起阅读 »

秀!秀!秀!优秀的富文本库:XRichText

一个Android富文本类库,支持图文混排,支持编辑和预览,支持插入和删除图片。 实现的原理: 使用ScrollView作为最外层布局包含LineaLayout,里面填充TextView和ImageView。删除的时候,根据光标的位置,删除TextView和I...
继续阅读 »

一个Android富文本类库,支持图文混排,支持编辑和预览,支持插入和删除图片。


实现的原理:


  • 使用ScrollView作为最外层布局包含LineaLayout,里面填充TextView和ImageView。
  • 删除的时候,根据光标的位置,删除TextView和ImageView,文本自动合并。
  • 生成的数据为list集合,可自定义处理数据格式。

注意事项


  • V1.4版本开放了图片点击事件接口和删除图片接口,具体使用方式可以参考后面的文档说明,也可以参考Demo实现。
  • V1.6版本升级RxJava到2.2.3版本,RxAndroid到2.1.0版本。设置字体大小时需要带着单位,如app:rt_editor_text_size=”16sp”。
  • V1.9.3及后续版本,xrichtext库中已去掉Glide依赖,开放接口可以自定义图片加载器。具体使用方式可以参考后面的文档说明,也可以参考Demo实现。
  • Demo中图片选择器为知乎开源库Matisse,适配Android 7.0系统使用FileProvider获取图片路径。
  • 开发环境更新为 AS 3.4.2 + Gradle 4.4 + compileSDK 28 + support library 28.0.0,导入项目报版本错误时,请手动修改为自己的版本。
  • 请参考Demo的实现,进行了解本库。可以使用Gradle引入,也可以下载源码进行修改。
  • 如有问题,欢迎提出。欢迎加入QQ群交流:745215148。

截图预览

笔记列表
文字笔记详情
编辑笔记
图片笔记详情

使用方式

1. 作为module导入

把xrichtext作为一个module导入你的工程。


2. gradle依赖

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

dependencies {
implementation 'com.github.sendtion:XRichText:1.9.4'
}

如果出现support版本不一致问题,请排除XRichText中的support库,或者升级自己的support库为28.0.0版本。
使用方式:

implementation ('com.github.sendtion:XRichText:1.9.4') {
exclude group: 'com.android.support'
}

具体使用

在xml布局中添加基于EditText编辑器(可编辑)


<com.sendtion.xrichtext.RichTextEditor
android:id="@+id/et_new_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rt_editor_text_line_space="6dp"
app:rt_editor_image_height="500"
app:rt_editor_image_bottom="10"
app:rt_editor_text_init_hint="在这里输入内容"
app:rt_editor_text_size="16sp"
app:rt_editor_text_color="@color/grey_900"/>

在xml布局中添加基于TextView编辑器(不可编辑)


<com.sendtion.xrichtext.RichTextView
android:id="@+id/tv_note_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:rt_view_text_line_space="6dp"
app:rt_view_image_height="0"
app:rt_view_image_bottom="10"
app:rt_view_text_size="16sp"
app:rt_view_text_color="@color/grey_900"/>

自定义属性

具体参考Demo



  • RichTextView


    rt_view_image_height        图片高度,默认为0自适应,可以设置为固定数值,如500、800等
    rt_view_image_bottom 上下两张图片的间隔,默认10
    rt_view_text_size 文字大小,使用sp单位,如16sp
    rt_view_text_color 文字颜色,使用color资源文件
    rt_view_text_line_space 字体行距,跟TextView使用一样,比如6dp
  • RichTextEditor


    rt_editor_image_height      图片高度,默认为500,可以设置为固定数值,如500、800等,0为自适应高度
    rt_editor_image_bottom 上下两张图片的间隔,默认10
    rt_editor_text_init_hint 默认提示文字,默认为“请输入内容”
    rt_editor_text_size 文字大小,使用sp单位,如16sp
    rt_editor_text_color 文字颜色,使用color资源文件
    rt_editor_text_line_space 字体行距,跟TextView使用一样,比如6dp

生成数据

我把数据保存为了html格式,生成字符串存储到了数据库。


String noteContent = getEditData();

private String getEditData() {
List<RichTextEditor.EditData> editList = et_new_content.buildEditData();
StringBuffer content = new StringBuffer();
for (RichTextEditor.EditData itemData : editList) {
if (itemData.inputStr != null) {
content.append(itemData.inputStr);
} else if (itemData.imagePath != null) {
content.append("<img src=\"").append(itemData.imagePath).append("\"/>");
}
}
return content.toString();
}

显示数据

et_new_content.post(new Runnable() {
@Override
public void run() {
showEditData(content);
}
});

protected void showEditData(String content) {
et_new_content.clearAllLayout();
List<String> textList = StringUtils.cutStringByImgTag(content);
for (int i = 0; i < textList.size(); i++) {
String text = textList.get(i);
if (text.contains("<img")) {
String imagePath = StringUtils.getImgSrc(text);
int width = ScreenUtils.getScreenWidth(this);
int height = ScreenUtils.getScreenHeight(this);
et_new_content.measure(0,0);
Bitmap bitmap = ImageUtils.getSmallBitmap(imagePath, width, height);
if (bitmap != null){
et_new_content.addImageViewAtIndex(et_new_content.getLastIndex(), bitmap, imagePath);
} else {
et_new_content.addEditTextAtIndex(et_new_content.getLastIndex(), text);
}
et_new_content.addEditTextAtIndex(et_new_content.getLastIndex(), text);
}
}
}

图片点击事件

tv_note_content.setOnRtImageClickListener(new RichTextView.OnRtImageClickListener() {
@Override
public void onRtImageClick(String imagePath) {
ArrayList<String> imageList = StringUtils.getTextFromHtml(myContent, true);
int currentPosition = imageList.indexOf(imagePath);
showToast("点击图片:"+currentPosition+":"+imagePath);
// TODO 点击图片预览
}
});

图片加载器使用

请在Application中设置,经测试在首页初始化会出现问题。Demo仅供参考,具体实现根据您使用的图片加载器而变化。


XRichText.getInstance().setImageLoader(new IImageLoader() {
@Override
public void loadImage(String imagePath, ImageView imageView, int imageHeight) {
//如果是网络图片
if (imagePath.startsWith("http://") || imagePath.startsWith("https://")){
Glide.with(getApplicationContext()).asBitmap().load(imagePath).dontAnimate()
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
if (imageHeight > 0) {//固定高度
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, imageHeight);//固定图片高度,记得设置裁剪剧中
lp.bottomMargin = 10;//图片的底边距
imageView.setLayoutParams(lp);
Glide.with(getApplicationContext()).asBitmap().load(imagePath).centerCrop()
.placeholder(R.mipmap.img_load_fail).error(R.mipmap.img_load_fail).into(imageView);
} else {//自适应高度
Glide.with(getApplicationContext()).asBitmap().load(imagePath)
.placeholder(R.mipmap.img_load_fail).error(R.mipmap.img_load_fail).into(new TransformationScale(imageView));
}
}
});
} else { //如果是本地图片
if (imageHeight > 0) {//固定高度
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, imageHeight);//固定图片高度,记得设置裁剪剧中
lp.bottomMargin = 10;//图片的底边距
imageView.setLayoutParams(lp);

Glide.with(getApplicationContext()).asBitmap().load(imagePath).centerCrop()
.placeholder(R.mipmap.img_load_fail).error(R.mipmap.img_load_fail).into(imageView);
} else {//自适应高度
Glide.with(getApplicationContext()).asBitmap().load(imagePath)
.placeholder(R.mipmap.img_load_fail).error(R.mipmap.img_load_fail).into(new TransformationScale(imageView));
}
}
}
});

GitHub地址:https://github.com/sendtion/XRichText
下载地址:XRichText-master.zip


收起阅读 »

一步步封装实现自己的网络请求框架 3.0

一、ReactiveHtt协程这个概念已经出现很多年了,但 Kotlin 协程是在 2018 年才发布了 1.0 版本,被 Android 开发者所熟知还要再往后一段时间,协程的意义不是本篇文章所应该探讨的,但如果你去了解下协程能给我们带来的开发效益,我相信你...
继续阅读 »
一、ReactiveHtt

协程这个概念已经出现很多年了,但 Kotlin 协程是在 2018 年才发布了 1.0 版本,被 Android 开发者所熟知还要再往后一段时间,协程的意义不是本篇文章所应该探讨的,但如果你去了解下协程能给我们带来的开发效益,我相信你是会喜欢上它的

闲话说完,这里先贴上 3.0 版本的 GitHub 链接:ReactiveHttp

3.0 版本的技术栈已更新为了 Kotlin + Jetpack + Coroutines + Retrofit,已托管到 jitpack.io,感兴趣的读者可以直接远程导入依赖

allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}

dependencies {
implementation 'com.github.leavesC:ReactiveHttp:latest_version'
}


二、能给你带来什么

ReactiveHttp 能够给你带来什么?

  • 更现代化的技术栈。Kotlin + Jetpack + Retrofit 现在应该是大多数 Android 开发者选用的最基础组件了,Kotlin 协程会相对比较少人接触,但我觉得协程也是未来的主流方向之一,毕竟连 Retrofit 也原生支持 Kotlin 协程了,本库会更加符合大多数开发者的现实需求
  • 极简的设计理念。ReactiveHttp 目前仅包含十二个 Kotlin 文件,设计上遵循约定大于配置的理念,大多数配置项都可以通过方法复写的形式来实现自定义
  • 极简的使用方式。只需要持有一个 RemoteDataSource 对象,开发者就可以在任意地方发起异步请求和同步请求了。此外,进行网络请求 Callback 自然是必不可少的,ReactiveHttp 提供了多个事件回调:onStart、onSuccess、onSuccessIO、onFailed、onFailToast、onCancelled、onFinally 等,按需声明即可,甚至可以完全不用实现
  • 支持通用性的自动化行为。对于网络请求来说,像 showLoading、dismissLoading、showToast 等行为是具有通用性的,我们肯定不希望每个网络请求都要来手动调用方法触发以上操作,而是希望能够在发起网络请求的过程中自动完成。ReactiveHttp 就提供了自动完成以上通用性 UI 行为的功能 ,且每个操作均和生命周期相绑定,避免了常见的内存泄漏和 NPE 问题,并提供了交由外部使用者来自定义各种其它行为的入口
  • 极低的接入成本。ReactiveHttp 并不强制要求外部必须继承于任何 BaseViewModel 或者是 BaseActivity/BaseFragment,外部只要通过实现 IUIActionEventObserverIViewModelActionEvent 两个接口即可享受 ReactiveHttp 带来的各个益处。当然,如果你不需要 ReactiveHttp 的自动化行为的话,也可以不实现任何接口
  • 支持多个(两个/三个)接口同时并发请求,在网络请求成功时同步回调,所以理论上多个接口的总耗时就取决于耗时最长的那个接口,从而缩短用户的等待时间,提升用户体验

ReactiveHttp 不能带给你什么?

  • ReactiveHttp 本身的应用领域是专注于接口请求的,所以对于接口的返回值形式带有强制约束,且没有封装文件下载、文件上传等功能
  • 肯定有,但还没想到

ReactiveHttp 目前已经在我司项目上稳定运行一年多了,在这过程中我也是在逐步优化,使其能够更加适用于外部不同环境的需求,到目前为止我觉得也是已经足够稳定了,希望对你有所帮助 😇😇

三、架构说明

现在应该有百分之九十以上的 Android 客户端的网络请求是直接或间接依靠 OkHttp 来完成的吧?本文所说的网络请求框架就是指在 OkHttp 之上所做的一层封装。原生的 OkHttp 在使用上并不方便,甚至可以说是有点繁琐。Retrofit 在易用性上有所提升,但是如果直接使用的话也并不算多简洁。所以我们往往都是会根据项目架构自己来对 OkHttp 或者 Retrofit 进行多一层封装,ReactiveHttp 的实现出发点即是如此

此外,现在大多数项目都引用到了 Jetpack 这一套组件来实现 MVVM 架构的吧?ReactiveHttp 将 Jetpack 和 Retrofit 关联了起来,使得网络请求过程更加符合“响应式”概念,并提供了更加可靠的生命周期安全性和自动化行为

一般的网络请求框架是只专注于完成网络请求并透传出结果,ReactiveHttp 不太一样,ReactiveHttp 在这个基础上还实现了将网络请求和 ViewModel 以及 Activity/Fragment 相绑定的功能,ReactiveHttp 希望做到的是能够尽量完成大多数的通用行为,且每个层次均不强依赖于特定父类

Google 官方曾推出过一份最佳应用架构指南。当中,每个组件仅依赖于其下一级的组件。ViewModel 是关注点分离原则的一个具体实现,是作为用户数据的承载体处理者而存在的,Activity/Fragment 仅依赖于 ViewModel,ViewModel 就用于响应界面层的输入和驱动界面层变化,Repository 用于为 ViewModel 提供一个单一的数据来源及数据存储域,Repository 可以同时依赖于持久性数据模型和远程服务器数据源

ReactiveHttp 的设计思想类似于 Google 推荐的最佳应用架构指南

  • BaseRemoteDataSource 作为数据提供者处于最下层,只用于向上层提供数据,提供了多个同步请求和异步请求方法,和 BaseReactiveViewModel 之间依靠 IUIActionEvent 接口来联系
  • BaseReactiveViewModel 作为用户数据的承载体和处理者,包含了多个和网络请求事件相关的 LiveData 用于驱动界面层的 UI 变化,和 BaseReactiveActivity 之间依靠 IViewModelActionEvent 接口来联系
  • BaseReactiveActivity 包含与系统和用户交互的逻辑,其负责响应 BaseReactiveViewModel 中的数据变化,提供了和 BaseReactiveViewModel 进行绑定的方法

上文有说到,ReactiveHttp 提供了在网络请求过程中自动完成 showLoading、dismissLoading、showToast 等行为的能力。首先,BaseRemoteDataSource 在网络请求过程中会通过 IUIActionEvent 接口来通知 BaseReactiveViewModel 需要触发的行为,从而连锁触发 ShowLoadingLiveData、DismissLoadingLiveData、ShowToastLiveData 值的变化,BaseReactiveActivity 就通过监听 LiveData 值的变化来完成 UI 层操作

四、惯常做法

以下步骤应该是大部分应用目前进行网络请求时的惯常做法了

服务端返回给移动端的数据使用具有特定格式的 Json 来进行通信,用整数 status 来标明本次请求是否成功,在失败时则直接 showToast(msg)data则需要用泛型来声明了,最终就对应移动端的一个泛型类,类似于 HttpWrapBean

{
"status":200,
"msg":"success",
"data":""
}

data class HttpWrapBean(val status: Int, val msg: String, val data: T)

interface ApiService {

@POST("api1")
fun api1(): ObservableInt>>

@GET("api2")
fun api2(): Call>

}


然后在 interface 中声明 Api 接口,这也是使用 Retrofit 的惯常用法。根据项目中的实际情况,开发者可能是使用 Call 或者 Observable 作为每个接口返回值的最外层的数据包装类,然后再使用 HttpWrapBean 来作为具体数据类的包装类

然后项目中使用的是 RxJava,那么就需要像以下这样来完成网络请

val retrofit = Retrofit.Builder()
.baseUrl("https://xxx.com")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
val service = retrofit.create(ApiService::class.java)
val call: ObservableInt>> = service.api1()
call.subscribe(object : ConsumerInt>> {
override fun accept(userBean: HttpWrapBean?) {

}

}, object : Consumer {
override fun accept(t: Throwable?) {

}
})


五、简单入门

ReactiveHttp 在使用上会比上面给出的例子简单很多,下面就来看下通过 ReactiveHttp 如何完成网络请求

ReactiveHttp 需要知道网络请求的结果,但不知道外部会使用什么字段名来标识 HttpWrapBean 中的三个值,所以需要外部实现 IHttpWrapBean 接口来进行标明。例如,你可以这样来实现:

data class HttpWrapBean(val status: Int, val msg: String, val data: T) : IHttpWrapBean {

override val httpCode: Int
get() = status

override val httpMsg: String
get() = msg

override val httpData: T
get() = data

//网络请求是否成功
override val httpIsSuccess: Boolean
get() = status == 200

}


suspend来修饰接口方法,且不需要其它的外层包装类。suspend是 kotlin 协程引入的,当用该关键字修饰接口方法时,Retrofit 内部就会使用协程的方式来完成该网络请求

interface ApiService {

@GET("config/district")
suspend fun getProvince(): HttpWrapBean>

}


ReactiveHttp 提供了 RemoteExtendDataSource 交由外部来继承实现。RemoteExtendDataSource 包含了所有的网络请求方法,外部仅需要根据实际情况来实现三个必要的字段和方法即可

  • releaseUrl。即应用的 BaseUrl
  • createRetrofit。用于创建 Retrofit,开发者可以在这里自定义 OkHttpClient
  • showToast。当网络请求失败时,通过该方法来向用户提示失败原因

例如,你可以像以下这样来实现你自己项目的专属 DataSource,当中就包含了开发者整个项目的全局网络请求配置

class SelfRemoteDataSource(iActionEvent: IUIActionEvent?) : RemoteExtendDataSource(iActionEvent, ApiService::class.java) {

companion object {

private val httpClient: OkHttpClient by lazy {
createHttpClient()
}

private fun createHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.readTimeout(1000L, TimeUnit.MILLISECONDS)
.writeTimeout(1000L, TimeUnit.MILLISECONDS)
.connectTimeout(1000L, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(FilterInterceptor())
.addInterceptor(MonitorInterceptor(MainApplication.context))
return builder.build()
}

}

/**
* 由子类实现此字段以便获取 release 环境下的接口 BaseUrl
*/
override val releaseUrl: String
get() = "https://restapi.amap.com/v3/"

/**
* 允许子类自己来实现创建 Retrofit 的逻辑
* 外部无需缓存 Retrofit 实例,ReactiveHttp 内部已做好缓存处理
* 但外部需要自己判断是否需要对 OKHttpClient 进行缓存
* @param baseUrl
*/
override fun createRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.client(httpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

override fun showToast(msg: String) {
Toast.makeText(MainApplication.context, msg, Toast.LENGTH_SHORT).show()
}

}


之后,我们就可以依靠 SelfRemoteDataSource 在任意地方发起网络请求了,按需声明 Callback 方法。此外,由于使用到了扩展函数,所以 SelfRemoteDataSource 中可以直接调用 ApiService 中的接口方法,无需特意引用和导包

六、进阶使用

上述在使用 SelfRemoteDataSource 发起网络请求时虽然调用的是 enqueueLoading 方法,但实际上并不会弹出 loading 框,因为完成 ShowLoading、DismissLoading、ShowToast 等 UI 行为是需要 RemoteDataSource、ViewModel 和 Activity 这三者一起进行配合的,即 SelfRemoteDataSource 需要和其它两者关联上,将需要触发的 UI 行为反馈给 Activity

这可以通过直接继承于 BaseReactiveActivity 和 BaseReactiveViewModel 来实现,也可以通过实现相应接口来完成关联。当然,如果你不需要 ReactiveHttp 的各个自动化行为的话,也可以不做以下任何改动

总的来说,ReactiveHttp 具有极低的接入成本

1、BaseReactiveActivity

BaseReactiveActivity 是 ReactiveHttp 提供的一个默认 BaseActivity,其实现了 IUIActionEventObserver 接口,用于提供一些默认参数和默认行为,例如 CoroutineScope 和 showLoading。但在大多数情况下,我们自己的项目是不会去继承外部 Activity 的,而是会有一个自己实现的全局统一的 BaseActivity,所以如果你不想继承 BaseReactiveActivity 的话,可以自己来实现 IUIActionEventObserver 接口,就像以下这样

@SuppressLint("Registered")
abstract class BaseActivity : AppCompatActivity(), IUIActionEventObserver {

protected inline fun getViewModel(
factory: ViewModelProvider.Factory? = null,
noinline initializer: (VM.(lifecycleOwner: LifecycleOwner) -> Unit)? = null
): Lazy where VM : ViewModel, VM : IViewModelActionEvent {
return getViewModel(VM::class.java, factory, initializer)
}

override val lifecycleSupportedScope: CoroutineScope
get() = lifecycleScope

override val lContext: Context?
get() = this

override val lLifecycleOwner: LifecycleOwner
get() = this

private var loadDialog: ProgressDialog? = null

override fun showLoading(job: Job?) {
dismissLoading()
loadDialog = ProgressDialog(lContext).apply {
setCancelable(true)
setCanceledOnTouchOutside(false)
//用于实现当弹窗销毁的时候同时取消网络请求
// setOnDismissListener {
// job?.cancel()
// }
show()
}
}

override fun dismissLoading() {
loadDialog?.takeIf { it.isShowing }?.dismiss()
loadDialog = nul
}


2、BaseReactiveViewModel

类似地,BaseReactiveViewModel 是 ReactiveHttp 提供的一个默认的 BaseViewModel,其实现了 IViewModelActionEvent 接口,用于接收 RemoteDataSource 发起的 UI 层行为。如果你不希望继承于 BaseReactiveViewModel 的话,可以自己来实现 IViewModelActionEvent 接口,就像以下这样

open class BaseViewModel : ViewModel(), IViewModelActionEvent {

override val lifecycleSupportedScope: CoroutineScope
get() = viewModelScope

override val showLoadingEventLD = MutableLiveData()

override val dismissLoadingEventLD = MutableLiveData()

override val showToastEventLD = MutableLiveData()

override val finishViewEventLD = MutableLiveData()

}


3、关联上

完成以上两步后,开发者就可以像如下所示这样将 RemoteDataSource、ViewModel 和 Activity 这三者给关联起来。WeatherActivity 通过 getViewModel 方法来完成 WeatherViewModel 的初始化和内部多个 UILiveData 的绑定,并在 lambda 表达式中完成对 WeatherViewModel 内部和具体业务相关的 DataLiveData 的数据监听,至此所有自动化行为就都已经绑定上了

class WeatherViewModel : BaseReactiveViewModel() {

private val remoteDataSource by lazy {
SelfRemoteDataSource(this)
}

val forecastsBeanLiveData = MutableLiveData()

fun getWeather(city: String) {
remoteDataSource.enqueue({
getWeather(city)
}) {
onSuccess {
if (it.isNotEmpty()) {
forecastsBeanLiveData.value = it[0]
}
}
}
}

}

class WeatherActivity : BaseReactiveActivity() {

private val weatherViewModel by getViewModel {
forecastsBeanLiveData.observe(this@WeatherActivity, {
showWeather(it)
})
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_weather)
weatherViewModel.getWeather("adCode")
}

private fun showWeather(forecastsBean: ForecastsBean) {

}

}


七、其它

1、BaseRemoteDataSource

RemoteExtendDataSource 提供了许多个可以进行复写的方法,既可用于配置 OkHttp 的各个网络请求参数,也用于交由外部进行流程控制。例如,你可以这样来实现自己项目的 BaseRemoteDataSource

class BaseRemoteDataSource(iActionEvent: IUIActionEvent?) : RemoteExtendDataSource(iActionEvent, ApiService::class.java) {

companion object {

private val httpClient: OkHttpClient by lazy {
createHttpClient()
}

private fun createHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.readTimeout(1000L, TimeUnit.MILLISECONDS)
.writeTimeout(1000L, TimeUnit.MILLISECONDS)
.connectTimeout(1000L, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(FilterInterceptor())
.addInterceptor(MonitorInterceptor(MainApplication.context))
return builder.build()
}
}

/**
* 由子类实现此字段以便获取 release 环境下的接口 BaseUrl
*/
override val releaseUrl: String
get() = HttpConfig.BASE_URL_MAP

/**
* 允许子类自己来实现创建 Retrofit 的逻辑
* 外部无需缓存 Retrofit 实例,ReactiveHttp 内部已做好缓存处理
* 但外部需要自己判断是否需要对 OKHttpClient 进行缓存
* @param baseUrl
*/
override fun createRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.client(httpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

/**
* 如果外部想要对 Throwable 进行特殊处理,则可以重写此方法,用于改变 Exception 类型
* 例如,在 token 失效时接口一般是会返回特定一个 httpCode 用于表明移动端需要去更新 token 了
* 此时外部就可以实现一个 BaseException 的子类 TokenInvalidException 并在此处返回
* 从而做到接口异常原因强提醒的效果,而不用去纠结 httpCode 到底是多少
*/
override fun generateBaseException(throwable: Throwable): BaseHttpException {
return if (throwable is BaseHttpException) {
throwable
} else {
LocalBadException(throwable)
}
}

/**
* 用于由外部中转控制当抛出异常时是否走 onFail 回调,当返回 true 时则回调,否则不回调
* @param httpException
*/
override fun exceptionHandle(httpException: BaseHttpException): Boolean {
return true
}

/**
* 用于将网络请求过程中的异常反馈给外部,以便记录
* @param throwable
*/
override fun exceptionRecord(throwable: Throwable) {
Log.e("SelfRemoteDataSource", throwable.message ?: "")
}

/**
* 用于对 BaseException 进行格式化,以便在请求失败时 Toast 提示错误信息
* @param httpException
*/
override fun exceptionFormat(httpException: BaseHttpException): String {
return when (httpException.realException) {
null -> {
httpException.errorMessage
}
is ConnectException, is SocketTimeoutException, is UnknownHostException -> {
"连接超时,请检查您的网络设置"
}
else -> {
"请求过程抛出异常:" + httpException.errorMessage
}
}
}

override fun showToast(msg: String) {
Toast.makeText(MainApplication.context, msg, Toast.LENGTH_SHORT).show()
}

}


此外,开发者可以直接在自己的 BaseViewModel 中声明一个 BaseRemoteDataSource 变量实例,所有子 ViewModel 都全局统一使用同一份 DataSource 配置。如果有某些特定接口需要使用不同的 BaseUrl 的话,也可以再多声明一个 BaseRemoteDataSource

open class BaseViewModel : BaseReactiveViewModel() {

/**
* 正常来说单个项目中应该只有一个 RemoteDataSource 实现类,即全局使用同一份配置
* 但父类也应该允许子类使用一个独有的 RemoteDataSource,即允许子类复写此字段
*/
protected open val remoteDataSource by lazy {
BaseRemoteDataSource(this)
}

}


2、BaseHttpException

BaseHttpException 是 ReactiveHttp 对网络请求过程中发生的各类异常情况的包装类,任何透传到外部的异常信息均会被封装为 BaseHttpException 类型。BaseHttpException 有两个默认子类,分别用于表示服务器异常和本地异

/**
* @param errorCode 服务器返回的错误码 或者是 HttpConfig 中定义的本地错误码
* @param errorMessage 服务器返回的异常信息 或者是 请求过程中抛出的信息,是最原始的异常信息
* @param realException 用于当 code 是本地错误码时,存储真实的运行时异常
*/
open class BaseHttpException(val errorCode: Int, val errorMessage: String, val realException: Throwable?) : Exception(errorMessage) {

companion object {

/**
* 此变量用于表示在网络请求过程过程中抛出了异常
*/
const val CODE_ERROR_LOCAL_UNKNOWN = -1024520

}

/**
* 是否是由于服务器返回的 code != successCode 导致的异常
*/
val isServerCodeBadException: Boolean
get() = this is ServerCodeBadException

/**
* 是否是由于网络请求过程中抛出的异常(例如:服务器返回的 Json 解析失败)
*/
val isLocalBadException: Boolean
get() = this is LocalBadException

}

/**
* API 请求成功了,但 code != successCode
* @param errorCode
* @param errorMessage
*/
class ServerCodeBadException(errorCode: Int, errorMessage: String) : BaseHttpException(errorCode, errorMessage, null) {

constructor(bean: IHttpWrapBean<*>) : this(bean.httpCode, bean.httpMsg)

}

/**
* 请求过程抛出异常
* @param throwable
*/
class LocalBadException(throwable: Throwable) : BaseHttpException(CODE_ERROR_LOCAL_UNKNOWN, throwable.message?: "", throwable)


有时候开发者需要对某些异常情况进行特殊处理,此时就可以来实现自己的 BaseHttpException 子类。例如,在 token 失效时接口一般是会返回特定一个 httpCode 用于表明移动端需要去更新 token 了,此时开发者就可以实现一个 BaseHttpException 的子类 TokenInvalidException 并在 BaseRemoteDataSource 中进行返回,从而做到接口异常原因强提醒的效果,而不用去纠结 httpCode 到底是多少

class TokenInvalidException : BaseHttpException(CODE_TOKEN_INVALID, "token已失效", null)

open class BaseRemoteDataSource(iActionEvent: IUIActionEvent?) : RemoteExtendDataSource(iActionEvent, ApiService::class.java) {

companion object {

private val httpClient: OkHttpClient by lazy {
createHttpClient()
}

private fun createHttpClient(): OkHttpClient {
val builder = OkHttpClient.Builder()
.readTimeout(1000L, TimeUnit.MILLISECONDS)
.writeTimeout(1000L, TimeUnit.MILLISECONDS)
.connectTimeout(1000L, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(FilterInterceptor())
.addInterceptor(MonitorInterceptor(MainApplication.context))
return builder.build()
}
}

/**
* 由子类实现此字段以便获取 release 环境下的接口 BaseUrl
*/
override val releaseUrl: String
get() = "https://restapi.amap.com/v3/"

/**
* 允许子类自己来实现创建 Retrofit 的逻辑
* 外部无需缓存 Retrofit 实例,ReactiveHttp 内部已做好缓存处理
* 但外部需要自己判断是否需要对 OKHttpClient 进行缓存
* @param baseUrl
*/
override fun createRetrofit(baseUrl: String): Retrofit {
return Retrofit.Builder()
.client(httpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

/**
* 如果外部想要对 Throwable 进行特殊处理,则可以重写此方法,用于改变 Exception 类型
* 例如,在 token 失效时接口一般是会返回特定一个 httpCode 用于表明移动端需要去更新 token 了
* 此时外部就可以实现一个 BaseException 的子类 TokenInvalidException 并在此处返回
* 从而做到接口异常原因强提醒的效果,而不用去纠结 httpCode 到底是多少
*/
override fun generateBaseException(throwable: Throwable): BaseHttpException {
if (throwable is ServerCodeBadException && throwable.errorCode == BaseHttpException.CODE_TOKEN_INVALID) {
return TokenInvalidException()
}
return if (throwable is BaseHttpException) {
throwable
} else {
LocalBadException(throwable)
}
}

/**
* 用于由外部中转控制当抛出异常时是否走 onFail 回调,当返回 true 时则回调,否则不回调
* @param httpException
*/
override fun exceptionHandle(httpException: BaseHttpException): Boolean {
return httpException !is TokenInvalidException
}

override fun showToast(msg: String) {
Toast.makeText(MainApplication.context, msg, Toast.LENGTH_SHORT).show()
}
}


收起阅读 »

Android基于微信 xlog 开源 日志框架

前言之前写过一个 日志框架LogHelper ,是基于 Logger 开源库封装的,当时的因为项目本身的日志不是很多,完全可以使用,最近和其他公司合作,在一个新的项目上反馈,说在 大量log 的情况下会影响到手机主体功能的使用。从而让我对之前的日志行为做了一个...
继续阅读 »

前言

之前写过一个 日志框架LogHelper ,是基于 Logger 开源库封装的,当时的因为项目本身的日志不是很多,完全可以使用,最近和其他公司合作,在一个新的项目上反馈,说在 大量log 的情况下会影响到手机主体功能的使用。从而让我对之前的日志行为做了一个深刻的反省随后在开发群中咨询了其他开发的小伙伴,如果追求性能,可以研究一下 微信的 xlog ,也是本篇博客的重点

xlog 是什么

xlog 是什么 这个问题 我这也是在【腾讯Bugly干货分享】微信mars 的高性能日志模块 xlog得到了答案
简单来说 ,就是腾讯团队分享的基于 c/c++ 高可靠性高性能的运行期日志组件

官网的 sample

知道了他是什么,就要只要他是怎么用的,打开github 找到官网Tencent/mars
使用非常简单

下载库

dependencies {
compile 'com.tencent.mars:mars-xlog:1.2.3'
}


使用

System.loadLibrary("c++_shared");
System.loadLibrary("marsxlog");

final String SDCARD = Environment.getExternalStorageDirectory().getAbsolutePath();
final String logPath = SDCARD + "/marssample/log";

// this is necessary, or may crash for SIGBUS
final String cachePath = this.getFilesDir() + "/xlog"

//init xlog
if (BuildConfig.DEBUG) {
Xlog.appenderOpen(Xlog.LEVEL_DEBUG, Xlog.AppenderModeAsync, cachePath, logPath, "MarsSample", 0, "");
Xlog.setConsoleLogOpen(true);

} else {
Xlog.appenderOpen(Xlog.LEVEL_INFO, Xlog.AppenderModeAsync, cachePath, logPath, "MarsSample", 0, "");
Xlog.setConsoleLogOpen(false);
}

Log.setLogImp(new Xlog());


OK 实现了他的功能

不要高兴的太早,后续的问题都头大

分析各个方法的作用

知道了最简单的用法,就想看看他支持哪些功能

按照官网的demo 首先分析一下appenderOpen

appenderOpen(int level, int mode, String cacheDir, String logDir, String nameprefix, int cacheDays, String pubkey)

level

日志级别 没啥好说的 XLog 中已经写得很清楚了

public static final int LEVEL_ALL = 0;
public static final int LEVEL_VERBOSE = 0;
public static final int LEVEL_DEBUG = 1;
public static final int LEVEL_INFO = 2;
public static final int LEVEL_WARNING = 3;
public static final int LEVEL_ERROR = 4;
public static final int LEVEL_FATAL = 5;
public static final int LEVEL_NONE = 6;


值得注意的地方 debug 版本下建议把控制台日志打开,日志级别设为 Verbose 或者 Debug, release 版本建议把控制台日志关闭,日志级别使用 Info.

public static native void setLogLevel(int logLevel);

这个在官网的 接入指南

这里也可以使用

方法设置

mode

写入的模式

  • public static final int AppednerModeAsync = 0;

异步写入

  • public static final int AppednerModeSync = 1;

同步写入

同步写入,可以理解为实时的日志,异步则不是

Release版本一定要用 AppednerModeAsync, Debug 版本两个都可以,但是使用 AppednerModeSync 可能会有卡顿

这里也可以使用

public static native void setAppenderMode(int mode);

方法设置

cacheDir 设置缓存目录

缓存目录,当 logDir 不可写时候会写进这个目录,可选项,不选用请给 "", 如若要给,建议给应用的 /data/data/packname/files/log 目录。

会在目录下生成后缀为 .mmap3 的缓存文件,

logDir 设置写入的文件目录

真正的日志,后缀为 .xlog

日志写入目录,请给单独的目录,除了日志文件不要把其他文件放入该目录,不然可能会被日志的自动清理功能清理掉。

nameprefix 设置日志文件名的前缀

日志文件名的前缀,例如该值为TEST,生成的文件名为:TEST_20170102.xlog。

cacheDays

一般情况下填0即可。非0表示会在 _cachedir 目录下存放几天的日志。

这里的描述比较晦涩难懂,当我设置这个参数非0 的时候 会发现 原本设置在 logDir 目录下的文件 出现在了 cacheDir

例如 正常应该是

文件结构

- cacheDir
- log.mmap3
- logDir
- log_20200710.xlog
- log_20200711.xlog


变成这样

- cacheDir
- log.mmap3
- log_20200710.xlog
- log_20200711.xlog
- logDir

全部到了 cacheDir 下面

cacheDays 的意思是 在多少天以后 从缓存目录移到日志目录

pubkey 设置加密的 pubkey

这里涉及到了日志的加密与解密,下面会专门介绍

setMaxFileSize 设置文件大小

在 Xlog 下有一个 native 方法

	public static native void setMaxFileSize(long size);

他表示 最大文件大小,这里需要说一下,原本的默认设置 是一天一个日志文件在 appender.h 描述的很清楚

/*
* By default, all logs will write to one file everyday. You can split logs to multi-file by changing max_file_size.
*
* @param _max_byte_size Max byte size of single log file, default is 0, meaning do not split.
*/
void appender_set_max_file_size(uint64_t _max_byte_size);


默认情况下,所有日志每天都写入一个文件。可以通过更改max_file_size将日志分割为多个文件。单个日志文件的最大字节大小,默认为0,表示不分割

当超过设置的文件大小以后。文件会变成如下目录结构

- cacheDir
- log.mmap3
- logDir
- log_20200710.xlog
- log_20200710_1.xlog
- log_20200710_2.xlog


在 appender.cc 对应的有如下逻辑,

static long __get_next_fileindex(const std::string& _fileprefix, const std::string& _fileext) {
...
return (filesize > sg_max_file_size) ? index + 1 : index;

setConsoleLogOpen 设置是否在控制台答应日志

···java public static native void setConsoleLogOpen(boolean isOpen); ···

设置是否在控制台答应日志

setErrLogOpen

这个方法是没用的,一开始以为哪里继承的有问题,在查看源码的时候发现 他是一个空方法,没有应用

使用的话会导致程序异常,在自己编译的so 中我就把它给去掉了

setMaxAliveTime 设置单个文件最大保留时间

public static native void setMaxAliveTime(long duration);

置单个文件最大保留时间 单位是秒,这个方法有3个需要注意的地方,

  • 必须在 appenderOpen 方法之前才有效
  • 最小的时间是 一天
  • 默认的时间是10天

在 appender.cc 中可以看到

static const long kMaxLogAliveTime = 10 * 24 * 60 * 60;    // 10 days in second
static const long kMinLogAliveTime = 24 * 60 * 60; // 1 days in second
static long sg_max_alive_time = kMaxLogAliveTime;
....
void appender_set_max_alive_duration(long _max_time) {
if (_max_time >= kMinLogAliveTime) {
sg_max_alive_time = _max_time;
}
}

默认的时间是10天

appenderClose

在 文档中介绍说是在 程序退出时关闭日志 调用appenderClose的方法

然而在实际情况中 Application 类的 onTerminate() 只有在模拟器中才会生效,在真机中无效的,

如果在程序退出的时候没有触发 appenderClose 那么在下一次启动的时候,xlog 也会把日志写入到文件中

所以如何触发呢?

建议尽可能的去触发他 例如用户双击back 退出的情况下 你肯定是知道的 如果放在后台被杀死了,这个时候也真的没办法刷新,也没关系,上面也说了,再次启动的时候会刷新到日志中,

appenderFlush

当日志写入模式为异步时,调用该接口会把内存中的日志写入到文件。

isSync : true 为同步 flush,flush 结束后才会返回。 isSync : false 为异步 flush,不等待 flush 结束就返回。

日志文件的加密

这一块单独拿出来说明,是因为之前使用上遇到了坑

首先是这个 入参 PUB_KEY,一脸懵,是个啥,

在 mars/blob/master/mars/log/crypt/gen_key.py 这个就是能够获取到 PUB_KEY 的方法

运行如下

$ python gen_key.py
WARNING: Executing a script that is loading libcrypto in an unsafe way. This will fail in a future version of macOS. Set the LIBRESSL_REDIRECT_STUB_ABORT=1 in the environment to force this into an error.
save private key
471e607b1bb3760205f74a5e53d2764f795601e241ebc780c849e7fde1b4ce40

appender_open's parameter:
300330b09d9e771d6163bc53a4e23b188ac9b2f5c7150366835bce3a12b0c8d9c5ecb0b15274f12b2dffae7f4b11c3b3d340e0521e8690578f51813c93190e1e

上面的 private key 自己保存好

appender_open's parameter: 就是需要的 PUB_KEY

日志文件的解密

上面已经知道如何加密了,现在了解一下如何解密

下载pyelliptic1

Xlog 加密使用指引中能够看到

需要下载 pyelliptic1.5.7 然后编译 否则下面的命令会失败

直接解密脚本

xlog 很贴心的给我们提供了两个脚本

使用 decode_mars_nocrypt_log_file.py 解压没有加密的

python decode_mars_nocrypt_log_file [path]

使用 decode_mars_crypt_log_file.py 加密的文件

在使用之前需要将 脚本中的

PRIV_KEY = "145aa7717bf9745b91e9569b80bbf1eedaa6cc6cd0e26317d810e35710f44cf8"
PUB_KEY = "572d1e2710ae5fbca54c76a382fdd44050b3a675cb2bf39feebe85ef63d947aff0fa4943f1112e8b6af34bebebbaefa1a0aae055d9259b89a1858f7cc9af9df1"

改成上面自己获取到的 key 否则是解压不出来的

python decode_mars_crypt_log_file.py ~/Desktop/log/log_20200710.xlog

直接生成一个

- cacheDir
- log.mmap3
- logDir
- log_20200710.xlog
- log_20200710.xlog.log

也可以自定义名字

python decode_mars_crypt_log_file.py ~/Desktop/log/log_20200710.xlog ~/Desktop/log/1.log
- cacheDir
- log.mmap3
- logDir
- log_20200710.xlog
- 1.log

修改日志的格式

打开我们解压好的日志查看

^^^^^^^^^^Oct 14 2019^^^20:27:59^^^^^^^^^^[17223,17223][2020-07-24 +0800 09:49:19]
get mmap time: 3
MARS_URL:
MARS_PATH: master
MARS_REVISION: 85b19f92
MARS_BUILD_TIME: 2019-10-14 20:27:57
MARS_BUILD_JOB:
log appender mode:0, use mmap:1
cache dir space info, capacity:57926635520 free:52452691968 available:52452691968
log dir space info, capacity:57926635520 free:52452691968 available:52452691968
[I][2020-07-24 +8.0 09:49:21.179][17223, 17223][TAG][, , 0][======================> 1
[I][2020-07-24 +8.0 09:49:21.180][17223, 17223][TAG][, , 0][======================> 2
[I][2020-07-24 +8.0 09:49:21.180][17223, 17223][TAG][, , 0][======================> 3
[I][2020-07-24 +8.0 09:49:21.180][17223, 17223][TAG][, , 0][======================> 4
[I][2020-07-24 +8.0 09:49:21.181][17223, 17223][TAG][, , 0][======================> 5
[I][2020-07-24 +8.0 09:49:21.181][17223, 17223][TAG][, , 0][======================> 6
[I][2020-07-24 +8.0 09:49:21.182][17223, 17223][TAG][, , 0][======================> 7
[I][2020-07-24 +8.0 09:49:21.182][17223, 17223][TAG][, , 0][======================> 8
[I][2020-07-24 +8.0 09:49:21.182][17223, 17223][TAG][, , 0][======================> 9
[I][2020-07-24 +8.0 09:49:21.183][17223, 17223][TAG][, , 0][======================> 10
[I][2020-07-24 +8.0 09:49:21.183][17223, 17223][TAG][, , 0][======================> 11
[I][2020-07-24 +8.0 09:49:21.183][17223, 17223][TAG][, , 0][======================> 12
[I][2020-07-24 +8.0 09:49:21.184][17223, 17223][TAG][, , 0][======================> 13
[I][2020-07-24 +8.0 09:49:21.184][17223, 17223][TAG][, , 0][======================> 14
[I][2020-07-24 +8.0 09:49:21.185][17223, 17223][TAG][, , 0][======================> 15
[I][2020-07-24 +8.0 09:49:21.185][17223, 17223][TAG][, , 0][======================> 16
[I][2020-07-24 +8.0 09:49:21.185][17223, 17223][TAG][, , 0][======================> 17


我擦泪 除了我们需要的信息以外,还有这么多杂七杂八的信息,如何去掉,并且自己定义一下格式

这里就需要自己去编译 so 了,好在 xlog 已经给我们提供了很好的编译代码

对应的文档 本地编译

对于编译这块按照文档来就好了 需要注意的是

  • 一定要用 ndk-r20 不要用最新版本的 21
  • 一定用 Python2.7 mac 自带 不用要 Python3

去掉头文件

首先我们去到这个头文件,对于一个日志框架来着,这个没啥用

^^^^^^^^^^Oct 14 2019^^^20:27:59^^^^^^^^^^[17223,17223][2020-07-24 +0800 09:49:19]
get mmap time: 3
MARS_URL:
MARS_PATH: master
MARS_REVISION: 85b19f92
MARS_BUILD_TIME: 2019-10-14 20:27:57
MARS_BUILD_JOB:
log appender mode:0, use mmap:1
cache dir space info, capacity:57926635520 free:52452691968 available:52452691968
log dir space info, capacity:57926635520 free:52452691968 available:52452691968

在本机下载好的 mars 下,找到 appender.cc 将头文件去掉

修改日志格式

默认的格式很长

[I][2020-07-24 +8.0 09:49:21.179][17223, 17223][TAG][, , 0][======================> 1

[日志级别][时间][pid,tid][tag][filename,strFuncName,line][日志内容

是一个这样结构

比较乱,我们想要的日志 就时间,级别,日志内容 就行了

找到 formater.cc

将原本的

int ret = snprintf((char*)_log.PosPtr(), 1024, "[%s][%s][%" PRIdMAX ", %" PRIdMAX "%s][%s][%s, %s, %d][",  // **CPPLINT SKIP**
_logbody ? levelStrings[_info->level] : levelStrings[kLevelFatal], temp_time,
_info->pid, _info->tid, _info->tid == _info->maintid ? "*" : "", _info->tag ? _info->tag : "",
filename, strFuncName, _info->line);


改成

int ret = snprintf((char*)_log.PosPtr(), 1024,     "[%s][%s]",  // **CPPLINT SKIP**
temp_time, _logbody ? levelStrings[_info->level] : levelStrings[kLevelFatal] );


就行了

然后从新编译,将so 翻入项目 在看一下现在的效果

[2020-07-24 +8.0 11:47:42.597][I]======================>9

ok 打完收工

简单的封装一下

基本上分析和实现了我们需要的功能,那么把这部分简单的封装一下

放上核心的 Builder 源码可在下面自行查看

package com.allens.xlog

import android.content.Context
import com.tencent.mars.xlog.Log
import com.tencent.mars.xlog.Xlog

class Builder(context: Context) {

companion object {
//日志的tag
var tag = "log_tag"
}

//是否是debug 模式
private var debug = true


//是否打印控制台日志
private var consoleLogOpen = true


//是否每天一个日志文件
private var oneFileEveryday = true

//默认的位置
private val defCachePath = context.getExternalFilesDir(null)?.path + "/mmap"

// mmap 位置 默认缓存的位置
private var cachePath = defCachePath

//实际保存的log 位置
private var logPath = context.getExternalFilesDir(null)?.path + "/logDir"

//文件名称前缀 例如该值为TEST,生成的文件名为:TEST_20170102.xlog
private var namePreFix = "log"

//写入文件的模式
private var model = LogModel.Async

//最大文件大小
//默认情况下,所有日志每天都写入一个文件。可以通过更改max_file_size将日志分割为多个文件。
//单个日志文件的最大字节大小,默认为0,表示不分割
// 最大 当文件不能超过 10M
private var maxFileSize = 0L

//日志级别
//debug 版本下建议把控制台日志打开,日志级别设为 Verbose 或者 Debug, release 版本建议把控制台日志关闭,日志级别使用 Info.
private var logLevel = LogLevel.LEVEL_INFO

//通过 python gen_key.py 获取到的公钥
private var pubKey = ""

//单个文件最大保留时间 最小 1天 默认时间 10
private var maxAliveTime = 10

//缓存的天数 一般情况下填0即可。非0表示会在 _cachedir 目录下存放几天的日志。
//原来缓存日期的意思是几天后从缓存目录移到日志目录
private var cacheDays = 0

fun setCachePath(cachePath: String): Builder {
this.cachePath = cachePath
return this
}

fun setLogPath(logPath: String): Builder {
this.logPath = logPath
return this
}


fun setNamePreFix(namePreFix: String): Builder {
this.namePreFix = namePreFix
return this
}

fun setModel(model: LogModel): Builder {
this.model = model
return this
}

fun setPubKey(key: String): Builder {
this.pubKey = key
return this
}

//原来缓存日期的意思是几天后从缓存目录移到日志目录 默认 0 即可
//如果想让文件保留多少天 用 [setMaxAliveTime] 方法即可
//大于 0 的时候 默认会放在缓存的位置上 [cachePath]
fun setCacheDays(days: Int): Builder {
if (days < 0) {
this.cacheDays = 0
} else {
this.cacheDays = days
}
return this
}

fun setDebug(debug: Boolean): Builder {
this.debug = debug
return this
}

fun setLogLevel(level: LogLevel): Builder {
this.logLevel = level
return this
}

fun setConsoleLogOpen(consoleLogOpen: Boolean): Builder {
this.consoleLogOpen = consoleLogOpen
return this
}


fun setTag(logTag: String): Builder {
tag = logTag
return this
}


/**
* [isOpen] true 设置每天一个日志文件
* false 那么 [setMaxFileSize] 生效
*/
fun setOneFileEveryday(isOpen: Boolean): Builder {
this.oneFileEveryday = isOpen
return this
}

fun setMaxFileSize(maxFileSize: Float): Builder {
when {
maxFileSize < 0 -> {
this.maxFileSize = 0L
}
maxFileSize > 10 -> {
this.maxFileSize = (10 * 1024 * 1024).toLong()
}
else -> {
this.maxFileSize = (maxFileSize * 1024 * 1024).toLong()
}
}
return this
}

/**
* [day] 设置单个文件的过期时间 默认10天 在程序启动30S 以后会检查过期文件
* 过期时间依据 当前系统时间 - 文件最后修改时间计算
* 默认 单个文件保存 10
*/
fun setMaxAliveTime(day: Int): Builder {
when {
day < 0 -> {
this.maxAliveTime = 0
}
day > 10 -> {
this.maxAliveTime = 10
}
else -> {
this.maxAliveTime = day
}
}
return this
}

fun init() {

if (!debug) {
//判断如果是release 就强制使用 异步
model = LogModel.Async
//日志级别使用 Info
logLevel = LogLevel.LEVEL_INFO
}

if (cachePath.isEmpty()) {
//cachePath这个参数必传,而且要data下的私有文件目录,例如 /data/data/packagename/files/xlog, mmap文件会放在这个目录,如果传空串,可能会发生 SIGBUS 的crash。
cachePath = defCachePath
}


android.util.Log.i(tag, "Xlog=========================================>")
android.util.Log.i(
tag,
"info" + "\n"
+ "level:" + logLevel.level + "\n"
+ "model:" + model.model + "\n"
+ "cachePath:" + cachePath + "\n"
+ "logPath:" + logPath + "\n"
+ "namePreFix:" + namePreFix + "\n"
+ "cacheDays:" + cacheDays + "\n"
+ "pubKey:" + pubKey + "\n"
+ "consoleLogOpen:" + consoleLogOpen + "\n"
+ "maxFileSize:" + maxFileSize + "\n"
)

android.util.Log.i(tag, "Xlog=========================================<")
Xlog.setConsoleLogOpen(consoleLogOpen)
//每天一个日志文件
if (oneFileEveryday) {
Xlog.setMaxFileSize(0)
} else {
Xlog.setMaxFileSize(maxFileSize)
}

Xlog.setMaxAliveTime((maxAliveTime * 24 * 60 * 60).toLong())

Xlog.appenderOpen(
logLevel.level,
model.model,
cachePath,
logPath,
namePreFix,
cacheDays,
pubKey
)
Log.setLogImp(Xlog())
}


}


下载

Step 1. Add the JitPack repository to your build file Add it in your root build.gradle at the end of repositories:

allprojects {
repositories {
...
maven { url 'https://www.jitpack.io' }
}
}

Step 2. Add the dependency

	dependencies {
implementation 'com.github.JiangHaiYang01:XLogHelper:Tag'
}

添加 abiFilter

android {
compileSdkVersion 30
buildToolsVersion "30.0.1"

defaultConfig {
...
ndk {
abiFilter "armeabi-v7a"
}
}

...
}

使用

初始化,建议放在 Application 中

XLogHelper.create(this)
.setModel(LogModel.Async)
.setTag("TAG")
.setConsoleLogOpen(true)
.setLogLevel(LogLevel.LEVEL_INFO)
.setNamePreFix("log")
.setPubKey("572d1e2710ae5fbca54c76a382fdd44050b3a675cb2bf39feebe85ef63d947aff0fa4943f1112e8b6af34bebebbaefa1a0aae055d9259b89a1858f7cc9af9df1")
.setMaxFileSize(1f)
.setOneFileEveryday(true)
.setCacheDays(0)
.setMaxAliveTime(2)
.init()

XLogHelper.i("======================> %s", i)
XLogHelper.e("======================> %s", i)


代码下载:

收起阅读 »

独乐乐不如众乐乐,你的项目还在纠结用日志打印log么?Android开发okhttp3便捷拦截监听

SimpleInterceptorSimpleInterceptor 是Android OkHttp客户端的的拦截接口工具,为的是方便测试或开发,快速查找问题。 环境要求 Android 4.1+OkHttp 3.x or 4.xandroidx git地址...
继续阅读 »

SimpleInterceptor

SimpleInterceptor 是Android OkHttp客户端的的拦截接口工具,为的是方便测试或开发,快速查找问题。


在这里插入图片描述
在这里插入图片描述

环境要求


  1. Android 4.1+
  2. OkHttp 3.x or 4.x
  3. androidx

git地址

github地址 :https://github.com/smartbackme/SimpleInterceptor
国内访问地址: https://gitee.com/dileber/SimpleInterceptor
如果觉得不错 github 给个星
警告



使用此拦截器时生成和存储的数据可能包含敏感信息,如授权或Cookie头,以及请求和响应主体的内容。
由此,其只能用于调试过程,不可发布到线上


配置
project : build.gradle

buildscript {
repositories {
maven { url 'https://www.jitpack.io' }
}

版本于okhttp关联:
如果app 集成的是okhttp3 3.+版本那么请选用 3.0版本代码
如果app 集成的是okhttp3 4.+版本那么请选用 4.0版本代码

okhttp3 3.+
dependencies {

debugImplementation 'com.github.smartbackme.SimpleInterceptor:simpleinterceptor-debug:3.0'
releaseImplementation 'com.github.smartbackme.SimpleInterceptor:simpleinterceptor-release:3.0'
}
or

okhttp3 4.+
dependencies {

debugImplementation 'com.github.smartbackme.SimpleInterceptor:simpleinterceptor-debug:4.0'
releaseImplementation 'com.github.smartbackme.SimpleInterceptor:simpleinterceptor-release:4.0'
}

使用:



OkHttpClient.Builder()
.addInterceptor(SimpleInterceptor(context))
.build()

下载地址:dileber-SimpleInterceptor-master.zip


收起阅读 »

Android炫酷的粒子动画!

一、总述ParticleTextView 是一个 Android 平台的自定义 view 组件,可以用彩色粒子组成指定的文字,并配合多种动画效果和配置属性,呈现出丰富的视觉效果。二、使用1. 引入依赖compile 'yasic.library.Particl...
继续阅读 »

一、总述

ParticleTextView 是一个 Android 平台的自定义 view 组件,可以用彩色粒子组成指定的文字,并配合多种动画效果和配置属性,呈现出丰富的视觉效果。


二、使用

1. 引入依赖

compile 'yasic.library.ParticleTextView:particletextview:0.0.6'

2. 加入到布局文件中

    <com.yasic.library.particletextview.View.ParticleTextView
android
:id="@+id/particleTextView"
android
:layout_width="match_parent"
android
:layout_height="match_parent" />

3. 实例化配置信息类 ParticleTextViewConfig

ParticleTextView particleTextView = (ParticleTextView) findViewById(R.id.particleTextView);
RandomMovingStrategy randomMovingStrategy = new RandomMovingStrategy();
ParticleTextViewConfig config = new ParticleTextViewConfig.Builder()
.setRowStep(8)
.setColumnStep(8)
.setTargetText("Random")
.setReleasing(0.2)
.setParticleRadius(4)
.setMiniDistance(0.1)
.setTextSize(150)
.setMovingStrategy(randomMovingStrategy)
.instance();
particleTextView.setConfig(config);

4. 启动动画

particleTextView.startAnimation();

5. 暂停动画

particleTextView1.stopAnimation();

三、API说明

粒子移动轨迹策略 MovingStrategy

移动轨迹策略继承自抽象类 MovingStrategy,可以自己继承并实现其中的 setMovingPath 方法,以下是自带的几种移动轨迹策略

  • CornerStrategy

  • HorizontalStrategy

  • BidiHorizontalStrategy


  • VerticalStrategy


  • BidiVerticalStrategy

配置信息类 ParticleTextViewConfig

配置信息类采用工厂模式创建,以下属性均为可选属性。

  • 设置显示的文字
setTargetText(String targetText)
  • 设置文字大小
setTextSize(int textSize)
  • 设置粒子半径
setParticleRadius(float radius)
  • 设置横向和纵向像素采样间隔

采样间隔越小生成的粒子数目越多,但绘制帧数也随之降低,建议结合文字大小与粒子半径进行调节。

setColumnStep(int columnStep)
setRowStep(int rowStep)
  • 设置粒子运动速度
setReleasing(double releasing)

指定时刻,粒子的运动速度由下列公式决定,其中 Vx 和 Vy 分别是 X 与 Y 方向上的运动速度,target 与 source 分别是粒子的目的坐标与当前坐标

Vx = (targetX - sourceX) * releasing
Vy = (targetY - sourceY) * releasing
  • 设置最小判决距离

当粒子与目的坐标距离小于最小判决距离时将直接移动到目的坐标,从而减少不明显的动画过程。

setMiniDistance(double miniDistance)
  • 设置粒子颜色域

默认使用完全随机的颜色域

setParticleColorArray(String[] particleColorArray)
  • 设置粒子移动轨迹策略

默认使用随机分布式策略

setMovingStrategy(MovingStrategy movingStrategy)
  • 设置不同路径间动画的间隔时间

delay < 0 时动画不循环

setDelay(Long delay)

ParticleTextView类

  • 指定配置信息类
setConfig(ParticleTextViewConfig config)
  • 开启动画
void startAnimation()
  • 停止动画
void stopAnimation()
  • 获取动画是否暂停

暂停是指动画完成了一段路径后的暂留状态

boolean isAnimationPause()
  • 获取动画是否停止

停止是指动画完成了一次完整路径后的停止状态

boolean isAnimationStop()

ParticleTextSurfaceView 类

继承自 SurfaceView 类,利用子线程进行 Canvas 绘制,在多个组件渲染情况下效率更高。所有 API 与 ParticleTextView 类一致。


代码下载:ParticleTextView-master.zip

收起阅读 »

Topbar的扩展:AwesomeBar

AwesomeBar该控件时Topbar的一个扩展,类似于Actionbar或者Toolbar。可结合DrawerLayout使用。 效果如下:gradle配置如下module的build.gradledependencies { compile 'c...
继续阅读 »

AwesomeBar

该控件时Topbar的一个扩展,类似于Actionbar或者Toolbar。可结合DrawerLayout使用。

效果如下:


gradle配置如下

module的build.gradle

dependencies {
compile 'com.github.florent37:awesomebar:1.0.0'
}

用法

<com.github.florent37.awesomebar.AwesomeBar
android:id="@+id/awesomeBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:elevation="4dp"/>
awesomeBar = (AwesomeBar) findViewById(R.id.awesomeBar);
awesomeBar.addAction(R.drawable.awsb_ic_edit_animated, "A");
awesomeBar.addAction(R.drawable.awsb_ic_edit_animated, "b");
awesomeBar.addAction(R.drawable.awsb_ic_edit_animated, "c");

awesomeBar.setActionItemClickListener(new AwesomeBar.ActionItemClickListener() {
@Override
public void onActionItemClicked(int position, ActionItem actionItem) {
switch (position) {
case 0:
toast.setText("A");
break;
case 1:
toast.setText("B");
break;
case 2:
toast.setText("C");
break;
}
toast.show();
}
});

awesomeBar.setOnMenuClickedListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
toast.setText("menu");
toast.show();
}
});

github地址:https://github.com/florent37/AwesomeBar


Github地址:https://github.com/florent37/AwesomeBar

下载地址:AwesomeBar-master.zip

收起阅读 »

便捷相机:CameraFragment

CameraFragmentCameraFragment可以帮助你快速实现打开相机视图,并提供便捷的API来捕获图片。 效果如下:使用说明:初始化//you can configure the fragment ...
继续阅读 »

CameraFragment

CameraFragment可以帮助你快速实现打开相机视图,并提供便捷的API来捕获图片。

效果如下:


使用说明:

初始化

  1. //you can configure the fragment by the configuration builder
  2. CameraFragment cameraFragment = CameraFragment.newInstance(new Configuration.Builder().build());
  3. getSupportFragmentManager().beginTransaction()
  4.                 .replace(R.id.content, cameraFragment, FRAGMENT_TAG)
  5.                 .commit();

你可以直接使用下面的代码拍照或者录制视频:

  1. cameraFragment.takePhotoOrCaptureVideo(callback);

切换Flash 模式enable / disabled ( AUTO / OFF / ON )

  1. cameraFragment.toggleFlashMode();

改变Camera类型(前置或者后置):

  1. cameraFragment.switchCameraTypeFrontBack();

设置Camera行为(拍照还是录制视频):

  1. cameraFragment.switchActionPhotoVideo();

还可以设置大小(分辨率):

  1. cameraFragment.openSettingDialog();

Result

在CameraFragmentResultListener中得到录制(或者拍照)的结果

  1. cameraFragment.setResultListener(new CameraFragmentResultListener() {
  2.        @Override
  3.        public void onVideoRecorded(byte[] bytes, String filePath) {
  4.                 //called when the video record is finished and saved
  5.                 startActivityForResult(PreviewActivity.newIntentVideo(MainActivity.this, filePath));
  6.        }
  7.        @Override
  8.        public void onPhotoTaken(byte[] bytes, String filePath) {
  9.                 //called when the photo is taken and saved
  10.                 startActivity(PreviewActivity.newIntentPhoto(MainActivity.this, filePath));
  11.        }
  12. });

Camera Listener

  1. cameraFragment.setStateListener(new CameraFragmentStateListener() {
  2.     //when the current displayed camera is the back
  3.     void onCurrentCameraBack();
  4.     //when the current displayed camera is the front
  5.     void onCurrentCameraFront();
  6.     //when the flash is at mode auto
  7.     void onFlashAuto();
  8.     //when the flash is at on
  9.     void onFlashOn();
  10.     //when the flash is off
  11.     void onFlashOff();
  12.     //if the camera is ready to take a photo
  13.     void onCameraSetupForPhoto();
  14.     //if the camera is ready to take a video
  15.     void onCameraSetupForVideo();
  16.     //when the camera state is "ready to record a video"
  17.     void onRecordStateVideoReadyForRecord();
  18.     //when the camera state is "recording a video"
  19.     void onRecordStateVideoInProgress();
  20.     //when the camera state is "ready to take a photo"
  21.     void onRecordStatePhoto();
  22.     //after the rotation of the screen / camera
  23.     void shouldRotateControls(int degrees);
  24.     void onStartVideoRecord(File outputFile);
  25.     void onStopVideoRecord();
  26. });

Github地址:https://github.com/florent37/CameraFragment

下载地址:CameraFragment-master.zip

收起阅读 »

水平展示日历控件:HorizontalCalendar

HorizontalCalendar该库是一个水平展示日历的控件,也是通过RecycerView来实现的。 效果如下:配置 模块中 build.gradle: repositories { jcenter() } dep...
继续阅读 »

HorizontalCalendar

该库是一个水平展示日历的控件,也是通过RecycerView来实现的。

效果如下:


配置


模块中 build.gradle:


repositories {
jcenter()
}

dependencies {
compile 'devs.mulham.horizontalcalendar:horizontalcalendar:1.3.4'
}


使用



  • 添加 HorizontalCalendarView 到你的layout


<android.support.design.widget.AppBarLayout>
............

<devs.mulham.horizontalcalendar.HorizontalCalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
app:textColorSelected="#FFFF"/>

android.support.design.widget.AppBarLayout>



  • 定义你的开始和结束设置日历范围的日期:



/* starts before 1 month from now */
Calendar startDate = Calendar.getInstance();
startDate.add(Calendar.MONTH, -1);

/* ends after 1 month from now */
Calendar endDate = Calendar.getInstance();
endDate.add(Calendar.MONTH, 1);


  • 可以用建造者模式构建


HorizontalCalendar horizontalCalendar = new HorizontalCalendar.Builder(this, R.id.calendarView)
.range(startDate, endDate)
.datesNumberOnScreen(5)
.build();


  • Fragment中使用:


HorizontalCalendar horizontalCalendar = new HorizontalCalendar.Builder(rootView, R.id.calendarView)
...................


  • 监听日期改变监听器


horizontalCalendar.setCalendarListener(new HorizontalCalendarListener() {
@Override
public void onDateSelected(Calendar date, int position) {
//do something
}
});


  • 监听滑动和长按


horizontalCalendar.setCalendarListener(new HorizontalCalendarListener() {
@Override
public void onDateSelected(Calendar date, int position) {

}

@Override
public void onCalendarScroll(HorizontalCalendarView calendarView,
int dx, int dy) {

}

@Override
public boolean onDateLongClicked(Calendar date, int position) {
return true;
}
});

定制



  • layout:


<devs.mulham.horizontalcalendar.HorizontalCalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:textColorNormal="#bababa"
app:textColorSelected="#FFFF"
app:selectorColor="#c62828" //default to colorAccent
app:selectedDateBackground="@drawable/myDrawable"/>


  • Activity 或者 Fragment 使用 HorizontalCalendar.Builder:


HorizontalCalendar horizontalCalendar = new HorizontalCalendar.Builder(this, R.id.calendarView)
.range(Calendar startDate, Calendar endDate)
.datesNumberOnScreen(int number) // Number of Dates cells shown on screen (default to 5).
.configure() // starts configuration.
.formatTopText(String dateFormat) // default to "MMM".
.formatMiddleText(String dateFormat) // default to "dd".
.formatBottomText(String dateFormat) // default to "EEE".
.showTopText(boolean show) // show or hide TopText (default to true).
.showBottomText(boolean show) // show or hide BottomText (default to true).
.textColor(int normalColor, int selectedColor) // default to (Color.LTGRAY, Color.WHITE).
.selectedDateBackground(Drawable background) // set selected date cell background.
.selectorColor(int color) // set selection indicator bar's color (default to colorAccent).
.end() // ends configuration.
.defaultSelectedDate(Calendar date) // Date to be selected at start (default to current day `Calendar.getInstance()`).
.build();


更多的自定义


builder.configure()
.textSize(float topTextSize, float middleTextSize, float bottomTextSize)
.sizeTopText(float size)
.sizeMiddleText(float size)
.sizeBottomText(float size)
.colorTextTop(int normalColor, int selectedColor)
.colorTextMiddle(int normalColor, int selectedColor)
.colorTextBottom(int normalColor, int selectedColor)
.end()

月份 模式


水平日历只能显示月  添加模式(HorizontalCalendar.mode.MONTHS)例如:


horizontalCalendar = new HorizontalCalendar.Builder(this, R.id.calendarView)
.range(Calendar startDate, Calendar endDate)
.datesNumberOnScreen(int number)
.mode(HorizontalCalendar.Mode.MONTHS)
.configure()
.formatMiddleText("MMM")
.formatBottomText("yyyy")
.showTopText(false)
.showBottomText(true)
.textColor(Color.LTGRAY, Color.WHITE)
.end()
.defaultSelectedDate(defaultSelectedDate)

事件


可以为每个日期提供事件列表,这些事件将在日期下用圆圈表示:


builder.addEvents(new CalendarEventsPredicate() {

@Override
public List<CalendarEvent> events(Calendar date) {
// test the date and return a list of CalendarEvent to assosiate with this Date.
}
})

重新配置


初始化后可以更改水平日历配置:



  • 更改日历日期范围:



horizontalCalendar.setRange(Calendar startDate, Calendar endDate);


  • 更改默认(未选定)项目样式:



horizontalCalendar.getDefaultStyle()
.setColorTopText(int color)
.setColorMiddleText(int color)
.setColorBottomText(int color)
.setBackground(Drawable background);


  • 改变选中样式


horizontalCalendar.getSelectedItemStyle()
.setColorTopText(int color)
..............


  • 更改格式、文本大小和选择器颜色:



horizontalCalendar.getConfig()
.setSelectorColor(int color)
.setFormatTopText(String format)
.setSizeTopText(float size)
..............

重要的


一定要调用horizontalCalendar.refresh();完成更改后


特征



  • 禁用特定HorizontalCalendarPredicate, 也可以使用指定禁用日期的唯一样式CalendarItemStyle:


builder.disableDates(new HorizontalCalendarPredicate() {
@Override
public boolean test(Calendar date) {
return false; // return true if this date should be disabled, false otherwise.
}

@Override
public CalendarItemStyle style() {
return null; // create and return a new Style for disabled dates, or null if no styling needed.
}
})


  • 选择特定的日期通过编程方式选择是否播放动画:



horizontalCalendar.selectDate(Calendar date, boolean immediate); // set immediate to false to ignore animation.
// or simply
horizontalCalendar.goToday(boolean immediate);


  • 检查日历中是否包含日期:



horizontalCalendar.contains(Calendar date);


  • 检查两个日期是否相等(年、月、日):



Utils.isSameDate(Calendar date1, Calendar date2);


  • 获取天两个日期之间:



Utils.daysBetween(Calendar startInclusive, Calendar endExclusive);


Github地址:https://github.com/Mulham-Raee/HorizontalCalendar

下载地址:Horizontal-Calendar-master.zip

收起阅读 »

二维的RecyclerView控件:excelPanel

excelPanel提供一个二维的RecyclerView控件。 效果如下:导入到项目中compile 'cn.zhouchaoyuan:excelpanel:1.0.5' 使用 1、添加xml<cn.zhouchaoyuan.excelpanel.Ex...
继续阅读 »

excelPanel

提供一个二维的RecyclerView控件。

效果如下:


导入到项目中

compile 'cn.zhouchaoyuan:excelpanel:1.0.5'


使用


1、添加xml

<cn.zhouchaoyuan.excelpanel.ExcelPanel
android:id="@+id/content_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:left_cell_width="@dimen/room_status_cell_length"
app:normal_cell_width="@dimen/room_status_cell_length"
app:top_cell_height="@dimen/room_status_cell_length" />

配置app属性


app:left_cell_width //left header cell's width, not support wrap_content
app:normal_cell_width //container cell's width, not support wrap_content
app:top_cell_height //top header cell's height, not support wrap_content


2、定义自定义适配器

适配器必须扩展BaseExcelPanelAdapter并重写七个方法,如下所示:

public class Adapter extends BaseExcelPanelAdapter<RowTitle, ColTitle, Cell>{

public Adapter(Context context) {
super(context);
}

//=========================================normal cell=========================================
@Override
public RecyclerView.ViewHolder onCreateCellViewHolder(ViewGroup parent, int viewType) {
return null;
}

@Override
public void onBindCellViewHolder(RecyclerView.ViewHolder holder, int verticalPosition, int horizontalPosition) {

}

//=========================================top cell===========================================
@Override
public RecyclerView.ViewHolder onCreateTopViewHolder(ViewGroup parent, int viewType) {
return null;
}

@Override
public void onBindTopViewHolder(RecyclerView.ViewHolder holder, int position) {

}

//=========================================left cell===========================================
@Override
public RecyclerView.ViewHolder onCreateLeftViewHolder(ViewGroup parent, int viewType) {
return null;
}

@Override
public void onBindLeftViewHolder(RecyclerView.ViewHolder holder, int position) {

}

//=========================================top left cell=======================================
@Override
public View onCreateTopLeftView() {
return null;
}
}


3、使用你的适配器

//==============================
private List<RowTitle> rowTitles;
private List<ColTitle> colTitles;
private List<List<Cell>> cells;
private ExcelPanel excelPanel;
private CustomAdapter adapter;
private View.OnClickListener blockListener
//..........................................
excelPanel = (ExcelPanel) findViewById(R.id.content_container);
adapter = new CustomAdapter(this, blockListener);
excelPanel.setAdapter(adapter);
excelPanel.setOnLoadMoreListener(this);//your Activity or Fragment implement ExcelPanel.OnLoadMoreListener
adapter.setAllData(colTitles, rowTitles, cells);
adapter.enableFooter();//load more, you can also call disableFooter()----default
adapter.enableHeader();//load history, you can also call disableHeader()----default













如果使用setOnLoadMoreListener(…)和enableHeader(),则必须调用addHistorySize(int)来告诉ExcelPanel添加了多少数据。

Github地址:https://github.com/zhouchaoyuan/excelPanel

下载地址:excelPanel-master.zip

收起阅读 »

多媒体选择器库:boxing

boxingboxing是一个多媒体选择器库。 可以选择一张或者多张图片,提供预览和裁剪功能。 同样支持gif图,选择视频和图像压缩功能。 (B站出品哦!!)效果如下:集成步骤很简单根据自己的需求添加对应的依赖就可以了,我的和官方demo一样用到了ucro...
继续阅读 »

boxing

boxing是一个多媒体选择器库。

可以选择一张或者多张图片,提供预览和裁剪功能。

同样支持gif图,选择视频和图像压缩功能。

(B站出品哦!!)

效果如下:


集成步骤很简单根据自己的需求添加对应的依赖就可以了,我的和官方demo一样用到了ucrop裁剪库,还有glide相关的,所以图片加载都是用的glide


  compile('com.yalantis:ucrop:2.2.0') {
exclude group: 'com.android.support'
exclude group: 'com.squareup.okio'
exclude group: 'com.squareup.okhttp3'
}
compile 'com.bilibili:boxing-impl:0.8.0'
compile 'jp.wasabeef:glide-transformations:2.0.1'
compile 'com.github.bumptech.glide:glide:3.7.0'

初始化图片加载(必选)

BoxingMediaLoader.getInstance().init(new IBoxingMediaLoader()); // 需要实现IBoxingMediaLoader

初始化图片裁剪(可选)

BoxingCrop.getInstance().init(new IBoxingCrop()); // 需要实现 IBoxingCrop


//进入选择图片的页面
public void pickIcon(View view) {
String cachePath = BoxingFileHelper.getCacheDir(this);
if (TextUtils.isEmpty(cachePath)) {
Toast.makeText(getApplicationContext(), R.string.boxing_storage_deny, Toast.LENGTH_SHORT).show();
return;
}
Uri destUri = new Uri.Builder()
.scheme("file")
.appendPath(cachePath)
.appendPath(String.format(Locale.US, "%s.png", System.currentTimeMillis()))
.build();
BoxingConfig singleCropImgConfig = new BoxingConfig(BoxingConfig.Mode.SINGLE_IMG).needCamera(R.mipmap.camera_white).withCropOption(new BoxingCropOption(destUri))
.withMediaPlaceHolderRes(R.mipmap.ic_default_image);
Boxing.of(singleCropImgConfig).withIntent(this, MyBoxingActivity.class).start(this, REQUEST_CODE);
}

//得到裁剪后的结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
final ArrayList<BaseMedia> medias = Boxing.getResult(data);
if (requestCode == REQUEST_CODE && medias != null && medias.size() > 0) {
BaseMedia baseMedia = medias.get(0);
String path = baseMedia.getPath();
Log.e("onActivityResult", "onActivityResult: " + path);
Glide.with(this)
.load(path)
.dontAnimate()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.error(R.mipmap.user_icon)
.placeholder(R.mipmap.user_icon)
.bitmapTransform(new CropCircleTransformation(this))
.into(head);

}
}
}

接下来简单介绍下ucrop裁剪库的相关属性设置,需要注意的是清单文件要配置UCropActivity


   UCrop.Options crop = new UCrop.Options();
crop.setCompressionFormat(Bitmap.CompressFormat.PNG);//设置裁剪的质量
crop. setHideBottomControls(true);//影藏图片下面的操作控制的界面
crop.withMaxResultSize(cropConfig.getMaxWidth(), cropConfig.getMaxHeight());//最终的剪裁尺寸
crop.withAspectRatio(cropConfig.getAspectRatioX(), cropConfig.getAspectRatioY());//剪裁的比例
crop.setStatusBarColor(ActivityCompat.getColor(context, R.color.colorPrimary));//设置状态栏颜色
crop.setToolbarColor(context.getResources().getColor(R.color.boxing_black1));//是指toolbar颜色
crop.setShowCropGrid(false);//是否显示网格线
UCrop.of(uri, cropConfig.getDestination())
.withOptions(crop)
.start(context, fragment, requestCode);

Github地址:https://github.com/Bilibili/boxing

下载地址:boxing-master.zip

收起阅读 »

快速实现TabLayout和CoordinatorLayout:CoordinatorTabLayout

说明:CoordinatorTabLayout是一个自定义组合控件,可快速实现TabLayout与CoordinatorLayout相结合的样式 继承至CoordinatorLayout, 在该组件下面使用了CollapsingToolbarLayout包含T...
继续阅读 »

说明:CoordinatorTabLayout是一个自定义组合控件,可快速实现TabLayout与CoordinatorLayout相结合的样式 继承至CoordinatorLayout, 在该组件下面使用了CollapsingToolbarLayout包含TabLayout


该库可以帮你快速实现TabLayout和CoordinatorLayout的组合效果。

效果如下:


用法

Step 1

在gradle文件中加入下面的依赖:

1.dependencies {
2.compile 'cn.hugeterry.coordinatortablayout:coordinatortablayout:1.0.5'
3.}


Step 2

在你自己的XML中使用它:

01.<cn.hugeterry.coordinatortablayout.CoordinatorTabLayout xmlns:android="http://schemas.android.com/apk/res/android"
02.xmlns:app="http://schemas.android.com/apk/res-auto"
03.android:id="@+id/coordinatortablayout"
04.android:layout_width="match_parent"
05.android:layout_height="match_parent">
06. 
07.<android.support.v4.view.ViewPager
08.android:id="@+id/vp"
09.android:layout_width="match_parent"
10.android:layout_height="match_parent"
11.app:layout_behavior="@string/appbar_scrolling_view_behavior" />
12.</cn.hugeterry.coordinatortablayout.CoordinatorTabLayout>

Step 3

在使用它的界面添加以下设置:
1.setTitle(String title):设置Toolbar标题
2.setupWithViewPager(ViewPager viewPager):将写好的viewpager设置到该控件当中
3.setImageArray(int[] imageArray):根据tab数量设置好头部的图片数组,并传到该控件当中

01.//构建写好的fragment加入到viewpager中
02.initFragments();
03.initViewPager();
04.//头部的图片数组
05.mImageArray = new int[]{
06.R.mipmap.bg_android,
07.R.mipmap.bg_ios,
08.R.mipmap.bg_js,
09.R.mipmap.bg_other};
10. 
11.mCoordinatorTabLayout = (CoordinatorTabLayout) findViewById(R.id.coordinatortablayout);
12.mCoordinatorTabLayout.setTitle("Demo")
13..setImageArray(mImageArray)
14..setupWithViewPager(mViewPager);


大功告成,好好享用吧

更多功能

添加折叠后的颜色变化效果

setImageArray(int[] imageArray, int[] colorArray):如果你想要有头部折叠后的颜色变化,可将之前设置好的图片数组以及根据tab数量设置的颜色数组传到该控件当中

1.mColorArray = new int[]{
2.android.R.color.holo_blue_light,
3.android.R.color.holo_red_light,
4.android.R.color.holo_orange_light,
5.android.R.color.holo_green_light};
6.mCoordinatorTabLayout.setImageArray(mImageArray, mColorArray);

添加返回

setBackEnable(Boolean canBack):设置Toolbar的返回按钮

01.@Override
02.protected void onCreate(Bundle savedInstanceState) {
03....
04.mCoordinatorTabLayout.setBackEnable(true);
05....
06.}
07.@Override
08.public boolean onOptionsItemSelected(MenuItem item) {
09.if (item.getItemId() == android.R.id.home) {
10.finish();
11.}
12.return super.onOptionsItemSelected(item);
13.}

获取子控件

getActionBar():获取该组件中的ActionBar getTabLayout():获取该组件中的TabLayout

Github地址:https://github.com/hugeterry/CoordinatorTabLayout

下载地址:CoordinatorTabLayout-master.zip

收起阅读 »

http拦截神器:Chuck

前言:Chuck是Android OkHttp客户端的一个简单的应用内HTTP检查器。Chuck拦截并持久化应用程序中的所有HTTP请求和响应,并提供用于检查其内容的UI。使用Chuck的应用程序将显示一个通知,显示正在进行的HTTP活动的摘要。点击通知启动完...
继续阅读 »

前言:Chuck是Android OkHttp客户端的一个简单的应用内HTTP检查器。Chuck拦截并持久化应用程序中的所有HTTP请求和响应,并提供用于检查其内容的UI。


使用Chuck的应用程序将显示一个通知,显示正在进行的HTTP活动的摘要。点击通知启动完整的Chuck UI。应用程序可以选择性地抑制通知,并直接从自己的界面中启动Chuck UI。HTTP交互及其内容可以通过共享意图导出。

主要的Chuck活动是在它自己的任务中启动的,允许它与使用Android7.x多窗口支持的主机应用程序UI一起显示。

警告:使用此拦截器时生成和存储的数据可能包含敏感信息,如授权或Cookie头,以及请求和响应主体的内容。它用于开发过程中,而不是发布版本或其他生产部署中。

如果你使用OkHttp作为网络请求库,那么这个Chuck库可以帮助你拦截留存所有的HTTP请求和相应信息。同时也提供UI来显示拦截的信息

效果如下:


安装
配置:

dependencies {
debugCompile 'com.readystatesoftware.chuck:library:1.0.4'
releaseCompile 'com.readystatesoftware.chuck:library-no-op:1.0.4'
}


在您的应用程序代码中,创建一个ChuckInterceptor实例(您需要为它提供一个上下文,因为Android)并在构建OkHttp客户端时将其添加为拦截器:

OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new ChuckInterceptor(context))
.build();


就这样!Chuck现在将记录OkHttp客户端进行的所有HTTP交互。您可以选择通过在拦截器实例上调用showNotification(false)来禁用通知,并使用Chuck.getLaunchIntent()中的意图直接在应用程序中启动Chuck UI。

Github地址:https://github.com/jgilfelt/chuck

下载地址:chuck-master.zip


收起阅读 »

预览帧画面库:PreviewSeekBar

PreviewSeekBar其实大家用PC优酷看视频的时候,鼠标放到进度条撒花姑娘就可以预览到所指向的帧画面。 一个叫[Ruben Sousa](https://medium.com/@rubensousa)的哥们做出了一个库并开源。 效果如下L:使用说明:...
继续阅读 »

PreviewSeekBar

其实大家用PC优酷看视频的时候,鼠标放到进度条撒花姑娘就可以预览到所指向的帧画面。

一个叫[Ruben Sousa](https://medium.com/@rubensousa)的哥们做出了一个库并开源。

效果如下L:


使用说明:

Build

  1. dependencies {
  2.     compile 'com.github.rubensousa:previewseekbar:0.3'
  3. }

Add the following XML:

  1. <com.github.rubensousa.previewseekbar.PreviewSeekBarLayout
  2.       android:id="@+id/previewSeekBarLayout"
  3.       android:layout_width="match_parent"
  4.       android:layout_height="wrap_content"
  5.       android:orientation="vertical">
  6.       <FrameLayout
  7.           android:id="@+id/previewFrameLayout"
  8.           android:layout_width="@dimen/video_preview_width"
  9.           android:layout_height="@dimen/video_preview_height">
  10.           <View
  11.               android:id="@+id/videoView"
  12.               android:layout_width="match_parent"
  13.               android:layout_height="match_parent"
  14.               android:layout_gravity="start"
  15.               android:background="@color/colorPrimary" />
  16.       </FrameLayout>
  17.       <com.github.rubensousa.previewseekbar.PreviewSeekBar
  18.           android:id="@+id/previewSeekBar"
  19.           android:layout_width="match_parent"
  20.           android:layout_height="wrap_content"
  21.           android:layout_below="@id/previewFrameLayout"
  22.           android:layout_marginTop="25dp"
  23.           android:max="800" />
  24. </com.github.rubensousa.previewseekbar.PreviewSeekBarLayout>

你需要在PreviewSeekBarLayout中至少添加一个PreviewSeekBar和一个FrameLayout,否则会出现异常。

PreviewSeekBarLayout继承自RelativeLayout因此还可以添加别的视图或者布局。

为seekBar添加一个标准的OnSeekBarChangeListener:

  1. // setOnSeekBarChangeListener was overridden to do the same as below
  2. seekBar.addOnSeekBarChangeListener(this);

实现你自己的预览逻辑:

  1. @Override
  2. public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
  3.     // I can't help anymore
  4. }

Github地址:https://github.com/rubensousa/PreviewSeekBar

下载地址:Store-feature-rx2.zip

收起阅读 »

异步数据加载和缓存数据的库:Store

StoreStore是一个异步数据加载和缓存数据的库。 实现一个 Disk Cache 需要以下几个步骤:在 Retrofit 的 API 下@GET("/v1/events")Single getEventsResponseBody();两点需要注意,一是要...
继续阅读 »

Store

Store是一个异步数据加载和缓存数据的库。

实现一个 Disk Cache 需要以下几个步骤:

在 Retrofit 的 API 下

@GET("/v1/events")
Single getEventsResponseBody();
两点需要注意,一是要用 Single,而是要用 ResponseBody
    创建 fetcher
private fun fetcher(): Single {
return service.getEventsResponseBody().map({ it.source() })
}
创建 Store
private fun provideStore(): Store {
return StoreBuilder.parsedWithKey()
.fetcher { fetcher() }
.persister(FileSystemPersister.create(FileSystemFactory.create(filesDir)) { key -> key })
.parser(JacksonParserFactory.createSourceParser(Events::class.java))
.open()
}
    使用 Store
store.get("cache_key") // get or fetch


Github地址:https://github.com/NYTimes/Store

下载地址:Store-feature-rx2.zip

收起阅读 »

Toast增强库

StyleableToast这也是一个Toast增强库。 设置背景颜色的吐司。设置吐司和存档的圆角半径不同的形状。设置吐司给你所有的透明固体或透明的吐司。设置笔划宽度和中风颜色在你的吐司。设置一个图标旁边的面包文本。你的图标上设置一个旋转的动画效果(见下面的例...
继续阅读 »

StyleableToast

这也是一个Toast增强库。
  • 设置背景颜色的吐司。
  • 设置吐司和存档的圆角半径不同的形状。
  • 设置吐司给你所有的透明固体或透明的吐司。
  • 设置笔划宽度和中风颜色在你的吐司。
  • 设置一个图标旁边的面包文本。
  • 你的图标上设置一个旋转的动画效果(见下面的例子01) 从Api 16 +工作

效果如下:


使用

1,在app/build.gradle文件中添加如下代码:

compile 'com.muddzdev:styleabletoast:1.0.6'

2,布局文件:main.xml就是几个按钮就不贴出来了,直接上一个是动态刷新的那个布局和飞行模式:
飞行模式:ic_airplanemode_inactive_black_24dp.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#ffffff"
android:pathData="M13,9V3.5c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,2.67 10,3.5v3.68l7.83,7.83L21,16v-2l-8,-5zM3,5.27l4.99,4.99L2,14v2l8,-2.5V19l-2,1.5V22l3.5,-1 3.5,1v-1.5L13,19v-3.73L18.73,21 20,19.73 4.27,4 3,5.27z"/>
</vector>

动态刷新: ic_autorenew_black_24dp.xml

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#fff"
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z"/>
</vector>

3,使用方式:
1>在styles.xml文件中添加样式:

<style name="StyleableToast">

<item name="android:textColor">@color/red</item>
<item name="android:colorBackground">@color/gray</item>
<!--<item name="android:fontFamily">fonts/dosis.otf"</item>-->
<item name="android:textStyle">bold</item>
<!--<item name="android:strokeWidth">2</item>-->
<!--<item name="android:strokeColor">@color/colorPrimary</item>-->
<!--<item name="android:radius">@dimen/toastRadius</item>-->
<item name="android:alpha">255</item>
<item name="android:icon">@drawable/ic_file_download</item>
</style>


使用:

case R.id.btn_toast_style_05 :
StyleableToast.makeText(this, "Picture saved in gallery", Toast.LENGTH_LONG, R.style.StyleableToast).show();
break;

2>,使用code:

private StyleableToast st;

case R.id.btn_toast_style_01 :
st = new StyleableToast(this, "加载中...", Toast.LENGTH_LONG);
st.setBackgroundColor(Color.parseColor("#ff5a5f"));
st.setTextColor(Color.WHITE);
st.setIcon(R.drawable.ic_autorenew_black_24dp);
st.spinIcon();
st.setMaxAlpha();
st.show();
break;


3> .Builder

private StyleableToast stBuilder;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initView();

stBuilder = new StyleableToast.Builder(this,"已关闭飞行模式")
.withBackgroundColor(Color.parseColor("#865aff"))
.withIcon(R.drawable.ic_airplanemode_inactive_black_24dp)
.withMaxAlpha()
.build();
}
...
case R.id.btn_toast_style_02 :
stBuilder.show();
break;

Github地址:https://github.com/Muddz/StyleableToast

下载地址:StyleableToast-master.zip


收起阅读 »

Toasty 一个自定义toast库

Toasty这是一个自定义Toast的库。 效果图:1. 添加这个在你的Project的 build.gradle 文件( 不是 你的Module的 build.gradle 文件):allprojects {repositories {...maven { ...
继续阅读 »

Toasty

这是一个自定义Toast的库。

效果图:


1. 添加这个在你的Project的 build.gradle 文件( 不是 你的Module的 build.gradle 文件):

allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}


依赖
添加到你的Module的build.gradle文件:

dependencies {
...
compile 'com.github.GrenderG:Toasty:1.2.5'
}


配置
这一步是可选的,但如果你想要,你可以配置一些Toasty参数。把这个地方放在你的应用程序中

Toasty.Config.getInstance()
.setErrorColor(@ColorInt int errorColor) // optional
.setInfoColor(@ColorInt int infoColor) // optional
.setSuccessColor(@ColorInt int successColor) // optional
.setWarningColor(@ColorInt int warningColor) // optional
.setTextColor(@ColorInt int textColor) // optional
.tintIcon(boolean tintIcon) // optional (apply textColor also to the icon)
.setToastTypeface(@NonNull Typeface typeface) // optional
.setTextSize(int sizeInSp) // optional
.apply(); // required
To display an error Toast:

Toasty.error(yourContext, "This is an error toast.", Toast.LENGTH_SHORT, true).show();
To display a success Toast:

Toasty.success(yourContext, "Success!", Toast.LENGTH_SHORT, true).show();
To display an info Toast:

Toasty.info(yourContext, "Here is some info for you.", Toast.LENGTH_SHORT, true).show();
To display a warning Toast:

Toasty.warning(yourContext, "Beware of the dog.", Toast.LENGTH_SHORT, true).show();
To display the usual Toast:

Toasty.normal(yourContext, "Normal toast w/o icon").show();
To display the usual Toast with icon:

Toasty.normal(yourContext, "Normal toast w/ icon", yourIconDrawable).show();
You can also create your custom Toasts with the custom() method:

Toasty.custom(yourContext, "I'm a custom Toast", yourIconDrawable, tintColor, duration, withIcon,
shouldTint).show();

Github地址:https://github.com/GrenderG/Toasty

下载地址:Toasty-master.zip

收起阅读 »

Lottie动画库

Lottie效果图如下:本文主要介绍动画开源库 Lottie 在 Android 中的使用。 前言 在日常APP开发中,为了提升用户感官舒适度等原因,我们会在APP中加入适量动画。 而传统手写动画方式往往存在诸多问题: 动画复杂而实现困难 ...
继续阅读 »

Lottie

效果图如下:


本文主要介绍动画开源库 Lottie 在 Android 中的使用。



前言


在日常APP开发中,为了提升用户感官舒适度等原因,我们会在APP中加入适量动画。
而传统手写动画方式往往存在诸多问题:



  • 动画复杂而实现困难


  • 图片素材占用体积过大


  • 不同Native平台都需各自实现,开发成本高


  • 不同Native平台实现的最终效果不一致


  • 后期视觉联调差异化大




难道就没有一种简便且高效的方案来减缓或解决上述问题吗?


答: 有的,那就是本文要介绍的主角 Lottie


一、Lottie 是什么?



Lottie是Airbnb开源的一个面向IOS、Android、React Native的动画库,可以解析用 Adobe After Effects 制作动画后通过 Bodymovin 插件导出的 Json 数据文件并在移动端原生渲染。



通俗点说,它是一款动画开源库,通过解析特定的Json文件或Json文本,可直接在移动端上渲染出复杂的动画效果。


参考图释




二、Lottie 能干什么?


它可以简便高效的实现复杂动画,替代传统低效的手写动画方式。


动画展示:



上方的动画是通过Lottie直接实现的。


如果我们使用手写代码方式实现,可以说是很难!


而通过 Lottie 实现时,需要的仅是一个Json文件或一段Json文本,并通过简洁的代码集成即可。


集成代码可能精简到如下模样:









1
2
3
4
5
6
7
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:lottie_rawRes="@raw/anim_lottie"
app:lottie_loop="true"
app:lottie_autoPlay="true" />

三、为什么使用 Lottie?



  • 简便,开发成本低
    相对于传统方式,动画不再是全部手写,所需做得只是嵌入XML并配置动画属性,集成快,开发时间少,开发成本低。


  • 不同 Native 平台效果基本一致
    渲染交由Lottie引擎内部实现,无需开发者处理平台差异,多平台共用同一个动画配置文件,效果一致性高。


  • 占用包体积小
    Lottie配置文件由Json文本串构成,相对于图片,占用体积更小。


  • 动画效果不失真
    传统图片拉伸或压缩导致失真,而Lottie为矢量图展示,不会出现失真情况。


  • 动画效果可以云端控制
    由于Lottie动画基于Json文件或文本解析,因此可以实现云端下发,实现动态加载,动态控制动画样式。



四、如何使用 Lottie?


Lottie 仅支持用 Gradle 构建配置,最低支持安卓版本 16。


1. 添加依赖到 build.gradle









1
2
3
4
5
6
7
dependencies {
implementation 'com.airbnb.android:lottie:2.5.4'
}

dependencies {
implementation "com.airbnb.android:lottie:${全局版本变量}"
}

2. 添加 Adobe After Effects 导出的动画 Json 文件


通常由视觉工程师确认动效后通过AE导出, 我们只需将该Json文件保存至 /raw 或 /assets文件夹下。


3. XML中嵌入基本布局









1
2
3
4
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

4. 加载播放动画,两类方式可选


① XML中静态配置, 举例:









1
2
3
4
5
6
7
8
9
10
11
12
13
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animation_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

//加载方式如下2种任选其一
app:lottie_rawRes="@raw/hello_world"
app:lottie_fileName="hello_world.json"

//循环播放
app:lottie_loop="true"
//加载完毕后自动播放
app:lottie_autoPlay="true" />

② 代码动态配置, 举例:


如下代码会在后台异步加载动画文件,并在加载完成后开始渲染动画。









1
2
3
4
LottieAnimationView animationView = ...;
animationView.setAnimation(R.raw.hello_world);
animationView.loop(true);
animationView.playAnimation();

五、其他拓展使用


1. 直接解析Json文本串加载动画









1
2
3
4
5
6
7
8
9
10
JsonReader jsonReader = new JsonReader(new StringReader(jsonStr));
lottieView.setAnimation(jsonReader);
lottieView.playAnimation();

Cancellable cancellable = LottieComposition.Factory.fromJsonString(jsonStr, composition -> {
lottieView.setComposition(composition);
lottieView.playAnimation();
});
//必要时取消进行中的异步操作
cancellable.cancel();

2. 配合网络下载,实现下载Json配置并动态加载









1
2
3
4
5
6
7
8
9
10
11
12
13
Call<ResponseBody> call = RetrofitComponent.fetchLottieConfig();//伪代码
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
//String json = response.body().string();
//交由Lottie处理...
}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
//do something.
}
});

3. 动画加载监听器


根据业务需要进行动画过程监听:









1
2
3
4
5
6
7
8
9
10
11
animationView.addAnimatorUpdateListener((animation) -> {
// do something.
});
animationView.playAnimation();
...
if (animationView.isAnimating()) {
// do something.
}
...
animationView.setProgress(0.5f);
...

4. LottieDrawable 的使用









1
2
3
4
5
6
7
8
LottieDrawable drawable = new LottieDrawable();
LottieComposition.Factory.fromAssetFileName(getApplicationContext(), "lottie_pin_jump.json", composition -> {
drawable.setComposition(composition);
//目前显示为静态图
ivLottie.setImageDrawable(drawable);
//调用start()开始播放动画
drawable.start();
});



六、需要考虑的问题



  1. 由于是依赖于AE做的动画,估计以后不只是要求视觉设计师精通AE,连前端开发都需要了解AE

  2. Lottie 对 Json 文件的支持待完善,目前存在部分AE导出成 Json 文件无法渲染或渲染不佳

  3. 支持的功能存在限制

Github地址:https://github.com/airbnb/lottie-android

下载地址:lottie-android-master.zip

收起阅读 »

Android 动态化多语言框架,支持语言包的动态下发、升级、删除,一处安装,到处使用

MLang 动态化多语言框架MLang 是 MultiLanguage 的简写,是一款动态化的多语言框架。设计优雅 语言包存储格式为 xml 格式,和 res 下的 strings.xml 一致 零依赖,完全使用系统 api 和系统的 xml 解析器 不持有 ...
继续阅读 »


MLang 动态化多语言框架

MLang 是 MultiLanguage 的简写,是一款动态化的多语言框架。

设计优雅

  •  语言包存储格式为 xml 格式,和 res 下的 strings.xml 一致
  •  零依赖,完全使用系统 api 和系统的 xml 解析器
  •  不持有 context,无内存泄漏
  •  静态方法 + 单例模式,一处安装,到处使用

动态化语言包

  •  动态下发语言包
  •  语言包的增加、升级、删除
  •  语言包内部任意字符串的增加、升级、删除
  •  自定义语言包的存储路径

完全兼容

  •  跟随系统语言
  •  时间格式跟随系统的 24 小时制
  •  处理各种语言的时区、时间格式化问题
  •  处理各种语言的复数格式化问题
  •  处理各种语言的阅读顺序问题(从左到右、从右到左)

1. 使用

使用字符串

// 本地和云端都存在的字符串
MyLang.getString("local_string", R.string.local_string)

// 云端存在 remote_string_only
// 但本地没有 R.string.remote_string_only,用 R.string.fallback_string 代替
MyLang.getString("remote_string_only", R.string.fallback_string)

使用语言包

//应用一种语言(这里自动处理了语言包的升级、语言包内部字符串的升级)
MyLang.getInstance().applyLanguage(Context, LocaleInfo, force=true, init=false);

//删除一种语言
MyLang.getInstance().deleteLanguage(Context, LocaleInfo);

LocaleInfo 可以在以下地方找到

//1. 所有云端的语言包
MyLang.getInstance().remoteLanguages

//2. 所有下载到本地、可用的语言包
MyLang.getInstance().languages

//3. 所有非官方的语言包
MyLang.getInstance().unofficialLanguages

//4. 除内置支持的语言外,另外安装的云端的语言包
MyLang.getInstance().otherLanguages

2. 安装

2.1. 引入

//build.gradle
allprojects {
repositories {
google()
jcenter()
maven { url "https://github.com/LinXueyuanStdio/MLang/raw/main/dist/" }
}
}

//app/build.gradle
implementation 'com.timecat.component:MLang:2.0.2'

2.2. 在 Application 中初始化,并监听系统语言的更改(如果跟随系统语言的话):

public class MyApplication extends Application {
@SuppressLint("StaticFieldLeak")
public static volatile Context applicationContext;
public static volatile Handler applicationHandler;

@Override
public void onCreate() {
super.onCreate();
applicationContext = this;
applicationHandler = new Handler(applicationContext.getMainLooper());
MyLang.init(applicationContext);
}

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
MyLang.onConfigurationChanged(newConfig);
}
}

其中建议自己新建一个静态类 MyLang 来代理 MLang。 这样有两个好处:

  1. 隔绝 MLang 的 api 变化,提高兼容性和稳定性。
  2. 使用更简洁。MLang 不持有 context,但每次获取字符串为空时,需要 context 来兜底,获取本地的字符串。在自己的 MyLang 默认提供 application Context,可以不用到处提供 context,更简洁。
public class MyLang {
private static File filesDir;
private static LangAction action;
public static void init(@NonNull Context applicationContext) {
filesDir = applicationContext.getCacheDir();
action = new MyLangAction();
getInstance();
}
public static MLang getInstance() {
return MLang.getInstance(MyApplication.applicationContext, filesDir, action);
}
public static void onConfigurationChanged(@NonNull Configuration newConfig) {
getInstance().onDeviceConfigurationChange(getContext(), newConfig);
}
}

3. 设计

3.1. 单例模式接收 3 个参数,context,fileDir,action

  1. context:MLang 内部不持有该 context。该 context 用于注册时区广播(根据时区来格式化字符串中的时间)、 判断系统当前时间是否 24 小时制等等。
  2. filesDir:持久化语言包文件的存储地址。语言包文件是 xml 格式,和 res 下的 strings.xml 一样。
  3. action:action 包含了应用语言包、切换语言等等需要的所有回调,即 LangAction 接口。
MLang.getInstance(context, filesDir, action);

3.2. LangAction 接口定义了 2 个东西

  1. 当前语言的设置存储。 MLang 根据语言 id (string) 来识别当前语言。语言 id 需要持久化。 所以设计了下面两个方法,可以自行决定持久化的方式(SharedPreferences、MMKV、SQLite等等)。
    void saveLanguageKeyInLocal(String language);
    @Nullable String loadLanguageKeyInLocal();
  2. 必要的网络接口。
    void langpack_getDifference(String lang_pack, String lang_code, int from_version, @NonNull final LangAction.GetDifferenceCallback callback)
    void langpack_getLanguages(@NonNull final LangAction.GetLanguagesCallback callback)
    void langpack_getLangPack(String lang_code, @NonNull final LangAction.GetLangPackCallback callback)

LangAction 的注释如下:

public interface LangAction {
/**
* SharedPreferences preferences = Utilities.getGlobalMainSettings();
* SharedPreferences.Editor editor = preferences.edit();
* editor.putString("language", language);
* editor.commit();
* @param language localeInfo.getKey() 语言 id
*/
void saveLanguageKeyInLocal(String language);

/**
* SharedPreferences preferences = Utilities.getGlobalMainSettings();
* String lang = preferences.getString("language", null);
* @return @Nullable lang 语言 id
*/
String loadLanguageKeyInLocal();

/**
* 在其他线程网络请求,在主线程或UI线程调用callback
* 这里设计成这样,是因为这个方法里支持异步执行
* 您需要在合适的时机手动调用 callback,且只能调用一次
* @param lang_pack 语言包名字
* @param lang_code 语言包版本名称
* @param from_version 语言包版本号
* @param callback @NonNull 在主线程或UI线程调用
*/
void langpack_getDifference(String lang_pack, String lang_code, int from_version, GetDifferenceCallback callback);

/**
* 在其他线程网络请求,在主线程或UI线程调用callback
* 这里设计成这样,是因为这个方法里支持异步执行
* 您需要在合适的时机手动调用 callback,且只能调用一次
* @param callback @NonNull 在主线程或UI线程调用
*/
void langpack_getLanguages(GetLanguagesCallback callback);

/**
* 在其他线程网络请求,在主线程或UI线程调用callback
* 这里设计成这样,是因为这个方法里支持异步执行
* 您需要在合适的时机手动调用 callback,且只能调用一次
* @param lang_code 语言包版本名称
* @param callback @NonNull 在主线程或UI线程调用
*/
void langpack_getLangPack(String lang_code, GetLangPackCallback callback);

interface GetLanguagesCallback {
/**
* 必须在UI线程或者主线程调用
* 所有可用的语言包
* @param languageList 语言包列表
*/
void onLoad(List<LangPackLanguage> languageList);
}

interface GetDifferenceCallback {
/**
* 必须在UI线程或者主线程调用
* 如果服务端没有实现增量分发的功能,可以用完整的语言包代替
* @param languageList 增量的语言包
*/
void onLoad(LangPackDifference languageList);
}

interface GetLangPackCallback {
/**
* 必须在UI线程或者主线程调用
* @param languageList 完整的语言包
*/
void onLoad(LangPackDifference languageList);
}

}

实现LangAction的一个示例如下:

public class MyLangAction implements LangAction {
@Override
public static void saveLanguageKeyInLocal(String language) {
SharedPreferences preferences = getContext().getSharedPreferences("language_locale", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("language", language);
editor.apply();
}

@Override
@Nullable
public static String loadLanguageKeyInLocal() {
SharedPreferences preferences = getContext().getSharedPreferences("language_locale", Context.MODE_PRIVATE);
return preferences.getString("language", null);
}
@Override
public void langpack_getDifference(String lang_pack, String lang_code, int from_version, @NonNull final LangAction.GetDifferenceCallback callback) {
Server.request_langpack_getDifference(lang_pack, lang_code, from_version, new Server.GetDifferenceCallback() {
@Override
public void onNext(final LangPackDifference difference) {
callback.onLoad(difference);
}
});
}

@Override
public void langpack_getLanguages(@NonNull final LangAction.GetLanguagesCallback callback) {
Server.request_langpack_getLanguages(new Server.GetLanguagesCallback() {
@Override
public void onNext(final List<LangPackLanguage> languageList) {
callback.onLoad(languageList);
}
});
}

@Override
public void langpack_getLangPack(String lang_code, @NonNull final LangAction.GetLangPackCallback callback) {
Server.request_langpack_getLangPack(lang_code, new Server.GetLangPackCallback() {
@Override
public void onNext(final LangPackDifference difference) {
callback.onLoad(difference);
}
});
}
}

3.3. 服务器语言包的结构

模拟的服务器数据

语言包实体

  • LangPackLanguage(name, version, ...)

语言包的数据

  • LangPackDifference(name, version, List, ...)
  • LangPackString(key: String, value: String)
public class Server {
public static LangPackLanguage chineseLanguage() {
LangPackLanguage langPackLanguage = new LangPackLanguage();
langPackLanguage.name = "chinese";
langPackLanguage.native_name = "简体中文";
langPackLanguage.lang_code = "zh";
langPackLanguage.base_lang_code = "zh";
return langPackLanguage;
}
public static LangPackDifference chinesePackDifference() {
LangPackDifference difference = new LangPackDifference();
difference.lang_code = "zh";
difference.from_version = 0;
difference.version = 1;
difference.strings = chineseStrings();
return difference;
}
public static ArrayList<LangPackString> chineseStrings() {
ArrayList<LangPackString> list = new ArrayList<>();
list.add(new LangPackString("LanguageName", "中文简体"));
list.add(new LangPackString("LanguageNameInEnglish", "Chinese"));
list.add(new LangPackString("local_string", "中文的云端字符串"));
list.add(new LangPackString("remote_string_only", "本地缺失,云端存在的字符串"));
return list;
}
}

4. 进阶配置

MLang.isRTL = false; //是否从右到左阅读(默认 false)
MLang.is24HourFormat = false; //是否 24 小时制(默认 false) 

MLang.USE_CLOUD_STRINGS = true; //是否使用云端字符串(默认 true)

代码下载:MLang-main.zip


收起阅读 »

一个模仿即刻App用户头像拖动效果的工具类

SnakeViewMakerSnakeViewMaker 是一个模仿即刻App里用户头像拖动效果的工具类。调用方法:1.创建 SnakeViewMaker; SnakeViewMaker snakeViewMaker = new SnakeViewMak...
继续阅读 »

SnakeViewMaker

SnakeViewMaker 是一个模仿即刻App里用户头像拖动效果的工具类。

image

调用方法:

1.创建 SnakeViewMaker;

    SnakeViewMaker snakeViewMaker = new SnakeViewMaker(MainActivity.this);

2.绑定

    snakeViewMaker
.addTargetView(imageView) // 绑定目标View
.attachToRootLayout((ViewGroup) findViewById(R.id.root)); // 绑定Activity/Fragment的根布局

3.其他相关API

    snakeViewMaker.detachSnake();                // 解除绑定
snakeViewMaker.updateSnakeImage(); // 当目标View的视图发生变化时,调用此方法用以更新Snake视图状态
snakeViewMaker.interceptTouchEvent(true); // Snake拖动过程中是否需要屏蔽其他onTouch事件,默认屏蔽
snakeViewMaker.setVisibility(View.VISIBLE); // 控制可见性
snakeViewMaker.setClickable(true); // 控制可点击
snakeViewMaker.setEnabled(true); // 控制可触摸

注意事项

1.目前不支持LinearLayout根布局

2.加载本地图片可直接调用。网络图片需要在图片加载完成后才能调用,不然可能出现绑定不成功的情况

例如,用glide加载网络图片时,调用时机如下:

    snakeViewMaker = new SnakeViewMaker(MainActivity.this);
Glide.with(this).load(url).asBitmap()
.into(new BitmapImageViewTarget(imageView) {
@Override
protected void setResource(Bitmap resource) {
super.setResource(resource);
snakeViewMaker.addTargetView(imageView)
.attachToRootLayout((ViewGroup) findViewById(R.id.root));
}
});


代码下载:SnakeViewMaker-master.zip

收起阅读 »

后台返回数据错误json解析总报错怎么办!这个框架解决你的烦恼!

集成步骤在项目根目录下的 build.gradle 文件中加入buildscript { ...... } allprojects { repositories { // JitPack 远程仓库:https...
继续阅读 »

集成步骤

  • 在项目根目录下的 build.gradle 文件中加入
buildscript {
......
}
allprojects {
repositories {
// JitPack 远程仓库:https://jitpack.io
maven { url 'https://jitpack.io' }
}
}
  • 在项目 app 模块下的 build.gradle 文件中加入
android {
// 支持 JDK 1.8
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
// Gson 解析容错:https://github.com/getActivity/GsonFactory
implementation 'com.github.getActivity:GsonFactory:5.2'
// Json 解析框架:https://github.com/google/gson
implementation 'com.google.code.gson:gson:2.8.5'
}

使用文档

  • 请使用框架返回的 Gson 对象来代替项目中的 Gson 对象
// 获取单例的 Gson 对象(已处理容错)
Gson gson = GsonFactory.getSingletonGson();
  • 因为框架中的 Gson 对象已经对解析规则进行了容错处理

其他 API

// 设置自定义的 Gson 对象
GsonFactory.setSingletonGson(Gson gson);

// 创建一个 Gson 构建器(已处理容错)
GsonBuilder gsonBuilder = GsonFactory.newGsonBuilder();

// 注册类型适配器
GsonFactory.registerTypeAdapterFactory(TypeAdapterFactory factory);

// 注册构造函数创建器
GsonFactory.registerInstanceCreator(Type type, InstanceCreator<?> creator);

// 设置 Json 解析容错监听
GsonFactory.setJsonCallback(new JsonCallback() {

@Override
public void onTypeException(TypeToken<?> typeToken, String fieldName, JsonToken jsonToken) {
// Log.e("GsonFactory", "类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken);
// 上报到 Bugly 错误列表
CrashReport.postCatchedException(new IllegalArgumentException("类型解析异常:" + typeToken + "#" + fieldName + ",后台返回的类型为:" + jsonToken));
}
});

容错介绍

  • 目前支持容错的数据类型有:

    • Bean 类

    • 数组集合

    • String(字符串)

    • boolean / Boolean(布尔值)

    • int / Integer(整数,属于数值类)

    • long /Long(长整数,属于数值类)

    • float / Float(单精度浮点数,属于数值类)

    • double / Double(双精度浮点数,属于数值类)

    • BigDecimal(精度更高的浮点数,属于数值类)

  • 基本涵盖 99.99% 的开发场景,可以运行 Demo 中的单元测试用例来查看效果:

数据类型容错的范围数据示例
bean集合、字符串、布尔值、数值[]""false0
集合bean、字符串、布尔值、数值{}""false0
字符串bean、集合、布尔值、数值{}[]false0
布尔值bean、集合、字符串、数值{}[]""0
数值bean、集合、字符串、布尔值{}[]""false
  • 大家可能觉得 Gson 解析容错没什么,那是因为我们对 Gson 解析失败的场景没有了解过:

    • 类型不对:后台有数据时返回 JsonObject,没数据返回 [],Gson 会直接抛出异常

    • 措手不及:如果客户端定义的是整数,但是后台返回浮点数,Gson 会直接抛出异常

    • 意想不到:如果客户端定义的是布尔值,但是后台返回的是 0 或者 1,Gson 会直接抛出异常

  • 以上情况框架已经做了容错处理,具体处理规则如下:

    • 如果后台返回的类型和客户端定义的类型不匹配,框架就不解析这个字段

    • 如果客户端定义的是整数,但后台返回浮点数,框架就对数值进行取整并赋值给字段

    • 如果客户端定义布尔值,但是后台返回整数,框架则将非 0 的数值则赋值为 true,否则为 false

常见疑问解答

  • Retrofit + RxJava 怎么替换?
Retrofit retrofit = new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(GsonFactory.getSingletonGson()))
.build();
  • 有没有必要处理 Json 解析容错?

我觉得非常有必要,因为后台返回的数据结构是什么样我们把控不了,但是有一点是肯定的,我们都不希望它崩,因为一个接口的失败导致整个 App 崩溃退出实属不值得,但是 Gson 很敏感,动不动就崩。

  • 我们后台用的是 Java,有必要处理容错吗?

如果你们的后台用的是 PHP,那我十分推荐你使用这个框架,因为 PHP 返回的数据结构很乱,这块经历过的人都懂,没经历过的人怎么说都不懂。

如果你们的后台用的是 Java,那么可以根据实际情况而定,例如我现在的公司用的就是 Java 后台,但是 Bugly 有上报一个关于 Gson 解析的 Crash,所以后台的话只能信一半。


代码下载:GsonFactory-master.zip

收起阅读 »

精简快速的图片加载框架!

Demo效果图说明支持加载网络图片(String格式url)/本地资源(mipmap和drawable)/网络.9图片/gif加载/自定义样式(圆形/圆角/centerCrop)/dataBindingv1.1.0起支持读取zip中图片加载至任意View中,无...
继续阅读 »

Demo效果图

话不多说先放图

说明

支持加载网络图片(String格式url)/本地资源(mipmap和drawable)/网络.9图片/gif加载/自定义样式(圆形/圆角/centerCrop)/dataBinding

v1.1.0起支持读取zip中图片加载至任意View中,无需解压.

更多使用方法和示例代码请下载demo源码查看

github : BaseImageLoader

设计说明

根据BaseImageLoader持有图片View层的contextBaseImageConfig类实现Glide原生的生命周期感知和多样化的自定义配置加载

BaseImageConfig使用建造者模式,使用更灵活更方便,也可自行继承BaseImageConfig减少类名长度和实现自定义功能

主要功能

  • loadImage 动态配置config加载你需求的资源图片
  • loadImageAs 获取网络url返回的资源,可获取drawable/bitmap/file/gif四种文件格式,可控知否获取资源的同时加载到View上
  • clear 取消加载或清除内存/储存中的缓存
  • BaseImageView 与动态config完全相同功能的自定义ImageView,支持xml中自定义属性配置各种加载需求
  • autoLoadImage 开发者自行指定zip压缩包的路径.并绑定当前View的根布局,配合View的tag字段自动加载zip中符合tag中图片名称的图片

添加依赖

implementation 'com.alex:BaseImageLoader:1.1.0'

使用的依赖库

  • api 'com.github.bumptech.glide:glide:4.11.0'
  • annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

开发者如需剔除重复依赖自行处理

使用说明

1.添加权限

需要添问网络和内存读写权限

2.项目通用配置

功能配置全部可选,如不配置则:

默认内存缓存大小20mb

默认bitmap池缓存30mb

默认硬盘缓存250mb

默认缓存文件夹名称BaseImageLoaderCache

默认缓存策略为AUTOMATIC,自动模式

    BaseImageSetting.getInstance()
.setMemoryCacheSize(30)//设置内存缓存大小 单位mb
.setBitmapPoolSize(50)//设置bitmap池缓存大小 单位mb
.setDiskCacheSize(80)//设置储存储存缓存大小 单位mb
.setLogLevel(Log.ERROR)//设置log等级
.setPlaceholder(R.drawable.ic_baseline_adb_24)//设置通用占位图片,全项目生效
.setErrorPic(R.mipmap.ic_launcher)//设置通用加载错误图片,全项目生效
.setCacheFileName("BaseImageLoaderDemo")//设置储存缓存文件夹名称,api基于Glide v4
.setCacheStrategy(CacheStrategy.AUTOMATIC)//设置缓存策略
.setCacheSize(50)//设置自动加载图片缓存数量,默认50
;

3.使用

1.获取BaseImageLoader对象

根据开发者项目设计模式,MVC/MVP/MVVM自行获取BaseImageLoader类对象,并自行管理生命周期.

BaseImageLoader自行提供单例,BaseImageLoader.getInstance();

2.加载至ImageView(包括但不限于任何继承于View或ViewGroup的view)

    BaseImageLoader mImageLoader = new BaseImageLoader();
mImageLoader.loadImage(this, ImageConfig.builder()
.url(Uri.parse(imageUrl))//url
.imageView(img1)//imageView
.placeholder(R.drawable.ic_baseline_adb_24)//占位图
.errorPic(R.mipmap.ic_launcher)//加载错误图片
.cacheStrategy(CacheStrategy.ALL)//缓存策略
.centerCrop(true)//centerCrop
.crossFade(true)//淡出淡入
.isCircle(true)//是否圆形显示
.setAsBitmap(true)//是否以bitmap加载图片,默认为drawable格式
.setRadius(30)//设置通用圆角,单位dp
.setTopRightRadius(10)//左上圆角,单位dp
.setTopLeftRadius(20)//右上圆角,单位dp
.setBottomRightRadius(30)//左下圆角,单位dp
.setBottomLeftRadius(40)//右下圆角,单位dp
.show());

注意:

避免过度绘制和二次绘制,其中优先级

isCircle(true) > setRadius(int) > setTopRightRadius(int) = setTopLeftRadius(int) = setBottomRightRadius(int) = setBottomLeftRadius(int)

  1. 设置isCircle(true)会使通用圆角设置不生效,减少绘制次数

  2. 设置setRadius()会使分别控制单独圆角不生效,减少绘制次数

3.资源文件直出

方法一:

    /**
* 加载图片同时获取不同格式的资源
* @param context {@link Context}
* @param url 资源url或资源文件
* @param listener 获取的资源回调结果
*/
void loadImageAs(@NonNull Context context, @NonNull Object url, @NonNull L listener);

/**
* 根据图片类型直出对象
* 需要根据参数类型判断获取的字段,比如使用OnBitmapResult,就只有getBitmap方法不为null
* 根据是否传入imageView是否直接显示图片,如果想自己处理过资源再加载则不传入imageView
*
*/
mImageLoader.loadImageAs(this, imageUrlTest, new OnBitmapResult() {
@Override
public void OnResult(ImageResult result) {
Log.e("result", result.getBitmap() + "");
}
});

方法二:

    /**
*
* @param context {@link Context}
* @param url 资源url或资源文件
* @param imageView 显示的imageView
* @param listener 获取的资源回调结果
*/
void loadImageAs(@NonNull Context context, @NonNull Object url, @Nullable ImageView imageView, @NonNull L listener);

/**
* 加载图片且获得bitmap格式图片 且以 imageView.setImageBitmap(bitmap) 模式加载图片
*/
mImageLoader.loadImageAs(this, imageUrlTest, img14, new OnBitmapResult() {
@Override
public void OnResult(ImageResult result) {
Log.e("result", result.getBitmap() + "");
}
});

/**
* 使用File类型获取result时,默认result.getFile()是在设置的cache目录中
* 加载图片且获得File文件 但是以Glide默认方式加载图片(drawable格式) imageView.setImageDrawable(drawable);
*/
mImageLoader.loadImageAs(this, imageUrlTest, img14, new OnFileResult() {
@Override
public void OnResult(ImageResult result) {
Log.e("result", result.getFile() + "");
}
});

/**
* 加载gif且获得gif文件 以 imageView.setImageDrawable(GifDrawable); 模式加载图片
*/
mImageLoader.loadImageAs(this, gifUrl, img14, new OnGifResult() {
@Override
public void OnResult(ImageResult result) {
Log.e("result", result.getGif() + "");
}
});

/**
* 加载图片且获得drawable格式图片 以Glide默认方式加载图片(drawable格式) imageView.setImageDrawable(drawable);
*/
mImageLoader.loadImageAs(this, imageUrlTest, img14, new OnDrawableResult() {
@Override
public void OnResult(ImageResult result) {
Log.e("result", result.getDrawable() + "");
}
});

4.自定义BaseImageView

xml中:

    <me.alex.baseimageloader.view.BaseImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:asBitmap="true"
app:bottomLeftRadius="40dp"
app:bottomRightRadius="30dp"
app:cacheStrategy="ALL"
app:errorPic="@mipmap/ic_launcher"
app:isCenterCrop="true"
app:isCircle="true"
app:isCrossFade="true"
app:placeholder="@drawable/ic_baseline_adb_24"
app:radius="30dp"
app:topLeftRadius="20dp"
app:topRightRadius="10dp"
app:url="https://xxx.xxx.com/photo/xxxxxx.png" />

api与代码设置相同

支持DataBinding:

    <me.alex.baseimageloader.view.BaseImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:url="@{data.imageUrl}" />

详见demo中dataBinding简单使用 优先级规则同上

4.自动加载图片

String ZIP_FILE_PATH = me.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS).getAbsolutePath() + File.separator + "imgs.zip";

//ZIP_FILE_PATH真实路径为:/storage/emulated/0/Android/data/me.alex.baseimageloaderdemo/files/Documents/imgs.zip

ScrollView autoLoadViewGroup = findViewById(R.id.autoLoadViewGroup);

BaseImageLoader.getInstance().autoLoadImage(this, autoLoadViewGroup, ZIP_FILE_PATH);

xml中

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/autoLoadViewGroup"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:overScrollMode="never">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">

<me.alex.baseimageloader.view.BaseImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="10dp"
android:tag="img1.png" />

<me.alex.baseimageloader.view.BaseImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="10dp"
android:tag="img2.png"
app:isCircle="true" />

<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="10dp"
android:tag="img3.png" />

<LinearLayout
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="10dp"
android:tag="img1.png" />
</LinearLayout>
</ScrollView>
  • zip文件夹位置开发者自行设置,demo中是将assets中的imgs.zip复制至指定路径然后加载.
  • 配合xml中View对象的tag参数匹配zip中的文件名称.
  • 如使用BaseImageView+tag加载图片,支持自定义属性
  • Demo中加载6张图片耗时60ms左右
  • 本功能开发本意是减少apk体积 , 减少重复资源下载 , 开发者可在业务流程中自行处理zip文件的下载和存放位置 , 自行处理数据安全

参数说明

1.BaseImageSetting:

函数名入参类型参数说明
setMemoryCacheSize()int设置内存缓存大小 单位mb
setBitmapPoolSize()int设置bitmap池缓存大小 单位mb
setDiskCacheSize()int设置储存储存缓存大小 单位mb
setLogLevel()int设置框架日志打印等级,详看android.util.Log类与Glide v4文档
placeholder()intv1.0.0版本仅支持resource类型int参数
errorPic()intv1.0.0版本仅支持resource类型int参数
setCacheFileName()String设置储存缓存文件夹名称,api基于Glide v4

2.BaseImageLoader:

/**
* 加载图片 使用继承自BaseImageConfig的配置
*
* @param context {@link Context}
* @param config {@link BaseImageConfig} 图片加载配置信息
*/
void loadImage(@NonNull Context context, @NonNull T config);
/**
* 自动加载图片
* @param context {@link Context}
* @param viewGroup xml中的根标签View
* @param zipFileRealPath zip文件夹路径
*/
void autoLoadImage(@NonNull Context context, @NonNull ViewGroup viewGroup, @NonNull String zipFileRealPath);
/**
* 加载图片同时获取不同格式的资源
* @param context {@link Context}
* @param url 资源url或资源文件
* @param listener 获取的资源回调结果
*/
void loadImageAs(@NonNull Context context, @NonNull Object url, @NonNull L listener);
/**
*
* @param context {@link Context}
* @param url 资源url或资源文件
* @param imageView 显示的imageView
* @param listener 获取的资源回调结果
*/
void loadImageAs(@NonNull Context context, @NonNull Object url, @Nullable ImageView imageView, @NonNull L listener);
/**
* 停止加载 或 清除缓存
*
* @param context {@link Context}
* @param config {@link BaseImageConfig} 图片加载配置信息
*/
void clear(@NonNull Context context, @NonNull T config);

代码下载:BaseImageLoader-master.zip

收起阅读 »

一个可以现成使用的Android调查问卷代码

DWSurvey是一款方便、高效、实用的调研问卷系统,一款基于 JAVA WEB 的开源问卷表单系统。演示地址开源版服务,开放源代码,可独立部署。地址:http://www.diaowen.net企业版在线服务,功能更丰富,不需要部署,可直接发布问卷进行数据收...
继续阅读 »

DWSurvey是一款方便、高效、实用的调研问卷系统,一款基于 JAVA WEB 的开源问卷表单系统。

输入图片说明

演示地址

开源版服务,开放源代码,可独立部署。

地址:http://www.diaowen.net

企业版在线服务,功能更丰富,不需要部署,可直接发布问卷进行数据收集。

地址:https://www.surveyform.cn

安装

因为DWSurvey是基于JAVA WEB实现,所以安装与一般的JAVA WEB程序无异,配置好数据库地址就可以正常使用。

安装说明

服务器必须安装由 JAVA 1.6+、MySQL、Apache Tomcat  构成的环境

由于引用的外部jar在你本地maven仓库中可能没有,这时只要您本地有maven环境,执行下bin目录下面的文件就可以自动导入。

配置说明、数据初始化

先在您mysql导入/src/main/resources/conf/sql/目录下的dwsurvey.sql数据库脚本文件

配置文件地址

conf/application.properties

#database settings
jdbc.url=jdbc:mysql://localhost:3306/dwsurvey?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=123456,.

分别修改```jdbc.url、jdbc.username、jdbc.password```

启动访问

配置完成后,启动服务在浏览器中输入如localhost:8080/diaowen相应的地址看到登录页面,表示已经安装成功。

初始账号:service@diaowen.net 密码:123456

war包下载

如果不想自己编译可以直接使用我们已经编译好的war包安装

最新的war包下载可以前往交流QQ 群1:635994795(满)群2:301105635(满), 群3:811287103(可加) (加群时请说明来由)

下载最新的dwsurvey-oss-v***.zip(注意看后面的版本号),解压后得到diaowen.war,再考到tomcat wabapps下

打包环境版本:jdk1.8, tomcat8.5.59

外部解压命令:jar xvf diaowen.war

特色

全新体验、流程简单

pic

以一种全新的设计体验,告别繁琐的设计流程,通过简单有趣的方式,轻轻松松完成问卷设计,多种问卷样式模板选择,只为显现更精美的表单问卷.

丰富的题型

丰富的题型支持,通过拖拽即可完成题目选择,并可以随意拖动其位置,还可置入所需图片、企业LOGO、设置答题逻辑,一份优美的问卷就是这么简单。

问卷表单静态化

对于问卷表单系统,因为所有的表单字段都是后台数据库来维护,所以对于每一次答卷请求,如果都从后端数据库去取每一题及选项的话,必定会对性能造成不小影响。

所以在发布的表单问卷时会对数据进行的页面静态化,生成一个真实的表单存档。


代码下载:DWSurvey-master.zip

收起阅读 »

每次上线都出问题?现在他来了,上线之前测一测.

仿文墨天机命盘界面,自定义view 宫格实现是通过drawline画直线,拿到View的width和height的1/4,按照对应宫格宽度和高度进行偏移划线;因为中间的是占了4个小宫格矩形位置组成的大宫格矩形,因此需要分部分处理,观察图形后发现按照从上到下分为...
继续阅读 »

仿文墨天机命盘界面,自定义view 宫格实现是通过drawline画直线,拿到View的width和height的1/4,按照对应宫格宽度和高度进行偏移划线;因为中间的是占了4个小宫格矩形位置组成的大宫格矩形,因此需要分部分处理,观察图形后发现按照从上到下分为4部分最为合适,最上边申、酉、戌、亥四个宫位划分为TopTopArea,中上部分的未、子两个宫位划分为TopCenterArea,中下部分的午、丑两个宫位分为BottomCenterArea,最下边巳、辰、卯、寅四个宫位划分为BottomBottomArea。 x方向也是屏幕宽度方向,按照宫格所占宽度划线偏移。y方向也就是屏幕高度方向,从上往下按照宫格所占高度偏移划线。 宫格中的文字部分,按照申宫调用drawText绘制出文字,并通过Paint设置文字的大小,文字的位置,以及文字的颜色,酉、戌、亥宫中的文字布局位置通过申宫中的文字位置按照宫位所处的位置偏移即可,其它宫位原理亦如此,具体查看demo中的代码实现。

中宫需要注意的是它会有多行文字的显示,但是drawText 只能实现单行文字效果,那么要想实现多行代码效果,需要用到官方提供另外一个专门用来实现多行文字的函数StaticLayout,我们要展示的数据 val centerStrFromText ="姓名:匿名 ${info.性别} ${info.五行局}\n" + "真太阳时:${info.真太阳时}\n" + "钟表时间:${info.钟表时间}\n" + "农历:${info.年干}" + "${info.年支}年${info.农历月}月${info.农历日}日 ${info.时干}\n" + "命主:${info.命主} 身主:${info.身主} 子斗:${info.身宫}\n"] val centerStaticLayout = StaticLayout(centerStrFromText,mTextPaint,600,Layout.Alignment.ALIGN_NORMAL,1f,0f,true)

StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad) width 是文字区域的宽度,文字到达这个宽度后就会自动换行; align 是文字的对齐方向; spacingmult 是行间距的倍数,通常情况下填 1 就好; spacingadd 是行间距的额外增加值,通常情况下填 0 就好; includepad 是指是否在文字上下添加额外的空间,来避免某些过高的字符的绘制出现越界。 多行文字偏移,需要配合canvas的translate方法实现,调用StaticLayout的draw方法进行绘制

添加点击事件,重写onTouchEvent方法,在 MotionEvent.ACTION_UP中判断用户点击位置是不是符合所在宫位的范围内如果是改变字体颜色,调用invalidate重新绘制其它宫位显示

实现效果1 实现效果2


代码下载:FortuneTelling-master.zip

收起阅读 »

看看微信是怎么处理图片的吧!

整体实现思路图片展示:PhotoView(大图支持双击放大)图片加载:Glide(加载网络图片、本地图片、资源文件)小图变大图时的实现:动画图片的下载:插入系统相册该控件采用自定义View的方式,通过一些基本的控件的组合,来形成一个具有大图预览的控件。上代码使...
继续阅读 »

整体实现思路

图片展示:PhotoView(大图支持双击放大)
图片加载:Glide(加载网络图片、本地图片、资源文件)
小图变大图时的实现:动画
图片的下载:插入系统相册

该控件采用自定义View的方式,通过一些基本的控件的组合,来形成一个具有大图预览的控件。上代码

使用方法

(1)在布局文件中引用该view

<com.demo.gallery.view.GalleryView
android:id="@+id/photo_gallery_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
app:animDuration="300"
app:saveText="保存至相册"
app:saveTextColor="#987622"/>

(2)具体使用方法
GalleryView galleryView = findViewById(R.id.photo_gallery_view);
galleryView.showPhotoGallery(index, List, ImageView);

到这里就结束了,就是这么简单!

具体实现

(1)先从showPhotoGallery(index, List, ImageView)这个方法讲起

int index:我们想要展示的一个图片列表中的第几个
List list: 我们要传入的要展示的图片类型list(支持网络图片、资源图片、本地图片(本地图片与网络图片其实都是一个字符串地址))

public class GalleryPhotoModel {

public Object photoSource;

public GalleryPhotoModel(@DrawableRes int drawableRes) {
this.photoSource = drawableRes;
}

public GalleryPhotoModel(String path) {
this.photoSource = path;
}

}

ImageView:即你点击想要展示的那个图片

(2)对传入GalleryView的数据进行处理

/**
* @param index 想要展示的图片的索引值
* @param photoList 图片集合(URL、Drawable、Bitmap)
* @param clickImageView 点击的第一个图片
*/
public void showPhotoGallery(int index, List<GalleryPhotoModel> photoList, ImageView clickImageView) {
GalleryPhotoParameterModel photoParameter = new GalleryPhotoParameterModel();
//图片
photoParameter.photoObj = photoList.get(index).photoSource;
//图片在list中的索引
photoParameter.index = index;
int[] locationOnScreen = new int[2];
//图片位置参数
clickImageView.getLocationOnScreen(locationOnScreen);
photoParameter.locOnScreen = locationOnScreen;
//图片的宽高
int width = clickImageView.getDrawable().getBounds().width();
int height = clickImageView.getDrawable().getBounds().height();
photoParameter.imageWidth = clickImageView.getWidth();
photoParameter.imageHeight = clickImageView.getHeight();
photoParameter.photoHeight = height;
photoParameter.photoWidth = width;
//scaleType
photoParameter.scaleType = clickImageView.getScaleType();
//将第一个点击的图片参数连同整个图片列表传入
this.setVisibility(View.VISIBLE);
post(new Runnable() {
@Override
public void run() {
requestFocus();
}
});
setGalleryPhotoList(photoList, photoParameter);
}

通过传递进来的ImageView,获取被点击View参数,并拼装成参数model,再进行数据的相关处理。

(3)GalleryView的实现机制

该View的实现思路主要是:最外层是一个RelativeLayout,内部有一个充满父布局的ImageView和ViewPager。ImageView用来进行图片的动画缩放,ViewPager用来进行最后的图片的展示。其实该View最主要的地方就是通过点击ImageView到最后ViewPager的展示的动画。接下来主要是讲解一下这个地方。先看一下被点击ImageView的参数Model。GalleryPhotoParameterModel

public class GalleryPhotoParameterModel {

//索引
public int index;
// 图片的类型
public Object photoObj;
// 在屏幕上的位置
public int[] locOnScreen = new int[]{-1, -1};
// 图片的宽
public int photoWidth = 0;
// 图片的高
public int photoHeight = 0;
// ImageView的宽
public int imageWidth = 0;
// ImageView的高
public int imageHeight = 0;
// ImageView的缩放类型
public ImageView.ScaleType scaleType;

}

3.1图片放大操作

private void handleZoomAnimation() {
// 屏幕的宽高
this.mScreenRect = GalleryScreenUtil.getDisplayPixes(getContext());
//将被缩放的图片放在一个单独的ImageView上进行单独的动画处理。
Glide.with(getContext()).load(firstClickItemParameterModel.photoObj).into(mScaleImageView);
//开启动画
mScaleImageView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//开始放大操作
calculateScaleAndStartZoomInAnim(firstClickItemParameterModel);
//
mScaleImageView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
}
/**
* 计算放大比例,开启放大动画
*
* @param photoData
*/
private void calculateScaleAndStartZoomInAnim(final GalleryPhotoParameterModel photoData) {
mScaleImageView.setVisibility(View.VISIBLE);

// 放大动画参数
int translationX = (photoData.locOnScreen[0] + photoData.imageWidth / 2) - (int) (mScreenRect.width() / 2);
int translationY = (photoData.locOnScreen[1] + photoData.imageHeight / 2) - (int) ((mScreenRect.height() + GalleryScreenUtil.getStatusBarHeight(getContext())) / 2);
float scale = getImageViewScale(photoData);
// 开启放大动画
executeZoom(mScaleImageView, translationX, translationY, scale, true, new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}

@Override
public void onAnimationEnd(Animator animation) {
showOtherViews();
tvPhotoSize.setText(String.format("%d/%d", viewPager.getCurrentItem() + 1, photoList.size()));
}

@Override
public void onAnimationCancel(Animator animation) {

}

@Override
public void onAnimationRepeat(Animator animation) {

}
});
}

3.2 图片缩小操作

/**
* 计算缩小比例,开启缩小动画
*/
private void calculateScaleAndStartZoomOutAnim() {
hiedOtherViews();

// 缩小动画参数
int translationX = (firstClickItemParameterModel.locOnScreen[0] + firstClickItemParameterModel.imageWidth / 2) - (int) (mScreenRect.width() / 2);
int translationY = (firstClickItemParameterModel.locOnScreen[1] + firstClickItemParameterModel.imageHeight / 2) - (int) ((mScreenRect.height() + GalleryScreenUtil.getStatusBarHeight(getContext())) / 2);
float scale = getImageViewScale(firstClickItemParameterModel);
// 开启缩小动画
executeZoom(mScaleImageView, translationX, translationY, scale, false, new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}

@Override
public void onAnimationEnd(Animator animation) {
mScaleImageView.setImageDrawable(null);
mScaleImageView.setVisibility(GONE);
setVisibility(GONE);
}

@Override
public void onAnimationCancel(Animator animation) {}

@Override
public void onAnimationRepeat(Animator animation) {}
});
}

3.3 计算图片缩放的比例

private float getImageViewScale(GalleryPhotoParameterModel photoData) {
float scale;
float scaleX = photoData.imageWidth / mScreenRect.width();
float scaleY = photoData.photoHeight * 1.0f / mScaleImageView.getHeight();

// 横向图片
if (photoData.photoWidth > photoData.photoHeight) {
// 图片的宽高比
float photoScale = photoData.photoWidth * 1.0f / photoData.photoHeight;
// 执行动画的ImageView宽高比
float animationImageScale = mScaleImageView.getWidth() * 1.0f / mScaleImageView.getHeight();

if (animationImageScale > photoScale) {
// 动画ImageView宽高比大于图片宽高比的时候,需要用图片的高度除以动画ImageView高度的比例尺
scale = scaleY;
}
else {
scale = scaleX;
}
}
// 正方形图片
else if (photoData.photoWidth == photoData.photoHeight) {
if (mScaleImageView.getWidth() > mScaleImageView.getHeight()) {
scale = scaleY;
}
else {
scale = scaleX;
}
}
// 纵向图片
else {
scale = scaleY;
}
return scale;
}

3.4 执行动画的缩放

 /**
* 执行缩放动画
* @param scaleImageView
* @param translationX
* @param translationY
* @param scale
* @param isEnlarge
*/
private void executeZoom(final ImageView scaleImageView, int translationX, int translationY, float scale, boolean isEnlarge, Animator.AnimatorListener listener) {
float startTranslationX, startTranslationY, endTranslationX, endTranslationY;
float startScale, endScale, startAlpha, endAlpha;

// 放大
if (isEnlarge) {
startTranslationX = translationX;
endTranslationX = 0;
startTranslationY = translationY;
endTranslationY = 0;
startScale = scale;
endScale = 1;
startAlpha = 0f;
endAlpha = 0.75f;
}
// 缩小
else {
startTranslationX = 0;
endTranslationX = translationX;
startTranslationY = 0;
endTranslationY = translationY;
startScale = 1;
endScale = scale;
startAlpha = 0.75f;
endAlpha = 0f;
}

//-------缩小动画--------
AnimatorSet set = new AnimatorSet();
set.play(
ObjectAnimator.ofFloat(scaleImageView, "translationX", startTranslationX, endTranslationX))
.with(ObjectAnimator.ofFloat(scaleImageView, "translationY", startTranslationY, endTranslationY))
.with(ObjectAnimator.ofFloat(scaleImageView, "scaleX", startScale, endScale))
.with(ObjectAnimator.ofFloat(scaleImageView, "scaleY", startScale, endScale))
// ---Alpha动画---
// mMaskView伴随着一个Alpha减小动画
.with(ObjectAnimator.ofFloat(maskView, "alpha", startAlpha, endAlpha));
set.setDuration(animDuration);
if (listener != null) {
set.addListener(listener);
}
set.setInterpolator(new DecelerateInterpolator());
set.start();
}

改View的主要实现如上,在图片进行缩放的时候,要考虑的情况:短边适配、图片原尺寸的宽高、展示图片的ImageView的宽高比、横竖屏时屏幕的尺寸。在此非常感谢震哥的帮助、抱拳了!老铁。如有更多想法的小伙伴。请移步我的github GalleryView地址


代码下载:GalleryView-master.zip

收起阅读 »

这个自定义键盘能让你欲罢不能!

KingKeyboard for Android 是一个自定义键盘。内置了满足各种场景的键盘需求:包括但不限于混合、字母、数字、电话、身份证、车牌号等可输入场景。还支持自定义。集成简单,键盘可定制化。Gif 展示引入Maven:<dependency&g...
继续阅读 »

KingKeyboard for Android 是一个自定义键盘。内置了满足各种场景的键盘需求:包括但不限于混合、字母、数字、电话、身份证、车牌号等可输入场景。还支持自定义。集成简单,键盘可定制化。

Gif 展示

Image

引入

Maven:

<dependency>
<groupId>com.king.keyboard</groupId>
<artifactId>kingkeyboard</artifactId>
<version>1.0.0</version>
<type>pom</type>
</dependency>

Gradle:

//AndroidX
implementation 'com.king.keyboard:kingkeyboard:1.0.0'

Lvy:

<dependency org='com.king.keyboard' name='kingkeyboard' rev='1.0.0'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
//...
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

自定义按键值

 /*
* 在KingKeyboard的伴生对象中定义了一些核心的按键值,当您需要自定义键盘时,可能需要用到
*/

//------------------------------ 下面是定义的一些公用功能按键值
/**
* Shift键 -> 一般用来切换键盘大小写字母
*/
const val KEYCODE_SHIFT = -1
/**
* 模式改变 -> 切换键盘输入法
*/
const val KEYCODE_MODE_CHANGE = -2
/**
* 取消键 -> 关闭输入法
*/
const val KEYCODE_CANCEL = -3
/**
* 完成键 -> 长出现在右下角蓝色的完成按钮
*/
const val KEYCODE_DONE = -4
/**
* 删除键 -> 删除输入框内容
*/
const val KEYCODE_DELETE = -5
/**
* Alt键 -> 预留,暂时未使用
*/
const val KEYCODE_ALT = -6
/**
* 空格键
*/
const val KEYCODE_SPACE = 32

/**
* 无作用键 -> 一般用来占位或者禁用按键
*/
const val KEYCODE_NONE = 0

//------------------------------

/**
* 键盘按键 -> 返回(返回,适用于切换键盘后界面使用,如:NORMAL_MODE_CHANGE或CUSTOM_MODE_CHANGE键盘)
*/
const val KEYCODE_MODE_BACK = -101

/**
* 键盘按键 ->返回(直接返回到最初,直接返回到NORMAL或CUSTOM键盘)
*/
const val KEYCODE_BACK = -102

/**
* 键盘按键 ->更多
*/
const val KEYCODE_MORE = -103

//------------------------------ 下面是自定义的一些预留按键值,与共用按键功能一致,但会使用默认的背景按键

const val KEYCODE_KING_SHIFT = -201
const val KEYCODE_KING_MODE_CHANGE = -202
const val KEYCODE_KING_CANCEL = -203
const val KEYCODE_KING_DONE = -204
const val KEYCODE_KING_DELETE = -205
const val KEYCODE_KING_ALT = -206

//------------------------------ 下面是自定义的一些功能按键值,与共用按键功能一致,但会使用默认背景颜色

/**
* 键盘按键 -> 返回(返回,适用于切换键盘后界面使用,如:NORMAL_MODE_CHANGE或CUSTOM_MODE_CHANGE键盘)
*/
const val KEYCODE_KING_MODE_BACK = -251

/**
* 键盘按键 ->返回(直接返回到最初,直接返回到NORMAL或CUSTOM键盘)
*/
const val KEYCODE_KING_BACK = -252

/**
* 键盘按键 ->更多
*/
const val KEYCODE_KING_MORE = -253

/*
用户也可自定义按键值,primaryCode范围区间为-999 ~ -300时,表示预留可扩展按键值。
其中-399~-300区间为功能型按键,使用Special背景色,-999~-400自定义按键为默认背景色
*/

示例

代码示例

    //初始化KingKeyboard
kingKeyboard = KingKeyboard(this,keyboardParent)
//然后将EditText注册到KingKeyboard即可
kingKeyboard.register(editText,KingKeyboard.KeyboardType.NUMBER)

/*
* 如果目前所支持的键盘满足不了您的需求,您也可以自定义键盘,KingKeyboard对外提供自定义键盘类型。
* 自定义步骤也非常简单,只需自定义键盘的xml布局,然后将EditText注册到对应的自定义键盘类型即可
*
* 1. 自定义键盘Custom,自定义方法setKeyboardCustom,键盘类型为{@link KeyboardType#CUSTOM}
* 2. 自定义键盘CustomModeChange,自定义方法setKeyboardCustomModeChange,键盘类型为{@link KeyboardType#CUSTOM_MODE_CHANGE}
* 3. 自定义键盘CustomMore,自定义方法setKeyboardCustomMore,键盘类型为{@link KeyboardType#CUSTOM_MORE}
*
* xmlLayoutResId 键盘布局的资源文件,其中包含键盘布局和键值码等相关信息
*/
kingKeyboard.setKeyboardCustom(R.xml.keyboard_custom)
// kingKeyboard.setKeyboardCustomModeChange(xmlLayoutResId)
// kingKeyboard.setKeyboardCustomMore(xmlLayoutResId)
kingKeyboard.register(et12,KingKeyboard.KeyboardType.CUSTOM)
 //获取键盘相关的配置信息
var config = kingKeyboard.getKeyboardViewConfig()

//... 修改一些键盘的配置信息

//重新设置键盘配置信息
kingKeyboard.setKeyboardViewConfig(config)

//按键是否启用震动
kingKeyboard.setVibrationEffectEnabled(isVibrationEffectEnabled)

//... 等等,还有各种监听方法。更多详情,请直接使用。
    //在Activity或Fragment相应的生命周期中调用,如下所示

override fun onResume() {
super.onResume()
kingKeyboard.onResume()
}

override fun onDestroy() {
super.onDestroy()
kingKeyboard.onDestroy()
}

相关说明

  • KingKeyboard主要采用Kotlin编写实现,如果您的项目使用的是Java编写,集成时语法上可能稍微有点不同,除了结尾没有分号以外,对应类伴生对象中的常量,需要通过点伴生对象才能获取。
  //Kotlin 写法
var keyCode = KingKeyboard.KEYCODE_SHIFT
  //Java 写法
int keyCode = KingKeyboard.Companion.KEYCODE_SHIFT;

更多使用详情,请查看app中的源码使用示例

代码下载:KingKeyboard-master.zip

收起阅读 »

多个模块如何管理?用它就对了!

FragmentationA powerful library that manage Fragment for Android!为"单Activity + 多Fragment","多模块Activity + 多Fragment"架构而生,简化开发,轻松解决动...
继续阅读 »

Fragmentation

A powerful library that manage Fragment for Android!

为"单Activity + 多Fragment","多模块Activity + 多Fragment"架构而生,简化开发,轻松解决动画、嵌套、事务相关等问题。

为了更好的使用和了解该库,推荐阅读下面的文章:

Fragment全解析系列(一):那些年踩过的坑

Fragment全解析系列(二):正确的使用姿势

Demo演示:

均为单Activity + 多Fragment,第一个为简单流式demo,第二个为仿微信交互的demo(全页面支持滑动退出),第三个为仿知乎交互的复杂嵌套demo

下载APK

  

特性

1、悬浮球/摇一摇实时查看Fragment的栈视图,降低开发难度

2、内部队列机制 解决Fragment多点触控、事务高频次提交异常等问题

3、增加启动模式、startForResult等类Activity方法

4、类Android事件分发机制的Fragment BACK键机制:onBackPressedSupport()

5、提供onSupportVisible()、懒加载onLazyInitView()等生命周期方法,简化嵌套Fragment的开发过程

6、提供 Fragment转场动画 系列解决方案,动态改变动画

7、提供Activity作用域的EventBus辅助类,Fragment通信更简单、独立(需要使用EventBusActivityScope库)

8、支持SwipeBack滑动边缘退出(需要使用Fragmentation_SwipeBack库)

      

如何使用

1. 项目下app的build.gradle中依赖:

// appcompat-v7包是必须的
compile 'me.yokeyword:fragmentation:1.3.8'

// 如果不想继承SupportActivity/Fragment,自己定制Support,可仅依赖:
// compile 'me.yokeyword:fragmentation-core:1.3.8'

// 如果想使用SwipeBack 滑动边缘退出Fragment/Activity功能,完整的添加规则如下:
compile 'me.yokeyword:fragmentation:1.3.8'
// swipeback基于fragmentation, 如果是自定制SupportActivity/Fragment,则参照SwipeBackActivity/Fragment实现即可
compile 'me.yokeyword:fragmentation-swipeback:1.3.8'

// Activity作用域的EventBus,更安全,可有效避免after onSavenInstanceState()异常
compile 'me.yokeyword:eventbus-activity-scope:1.1.0'
// Your EventBus's version
compile 'org.greenrobot:eventbus:{version}'

2. Activity extends SupportActivity或者 implements ISupportActivity:(实现方式可参考MySupportActivity)

// v1.0.0开始,不强制继承SupportActivity,可使用接口+委托形式来实现自己的SupportActivity
public class MainActivity extends SupportActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(...);
// 建议在Application里初始化
Fragmentation.builder()
// 显示悬浮球 ; 其他Mode:SHAKE: 摇一摇唤出 NONE:隐藏
.stackViewMode(Fragmentation.BUBBLE)
.debug(BuildConfig.DEBUG)
... // 更多查看wiki或demo
.install();

if (findFragment(HomeFragment.class) == null) {
loadRootFragment(R.id.fl_container, HomeFragment.newInstance()); // 加载根Fragment
}
}

3. Fragment extends SupportFragment或者 implements ISupportFragment:(实现方式可参考MySupportFragment):

// v1.0.0开始,不强制继承SupportFragment,可使用接口+委托形式来实现自己的SupportFragment
public class HomeFragment extends SupportFragment {

private void xxx() {
// 启动新的Fragment, 另有start(fragment,SINGTASK)、startForResult、startWithPop等启动方法
        start(DetailFragment.newInstance(HomeBean));
// ... 其他pop, find, 设置动画等等API, 请自行查看WIKI
    }
}


代码下载:Fragmentation-master.zip

收起阅读 »

为什么别人的状态栏那么好看,而你自己却无法实现!

这是一个为Android App 设置状态栏的工具类, 可以在4.4及其以上系统中实现 沉浸式状态栏/状态栏变色,支持设置状态栏透明度,满足你司设计师的各种要求(雾)。在此之前我写过一篇Android App 沉浸式状态栏解决方案,后来我司设计师说默认的透明度...
继续阅读 »

这是一个为Android App 设置状态栏的工具类, 可以在4.4及其以上系统中实现 沉浸式状态栏/状态栏变色,支持设置状态栏透明度,满足你司设计师的各种要求(雾)。

在此之前我写过一篇Android App 沉浸式状态栏解决方案,后来我司设计师说默认的透明度太深了,让我改浅一点,然后在想了一些办法之后给解决了。本着不重复造轮子的原则,索性整理成一个工具类,方便需要的开发者。

项目 GitHub 地址

Sample 下载

下载 StatusBarUtil-Demo

特性

  1. 设置状态栏颜色

    StatusBarUtil.setColor(Activity activity, int color)

  2. 设置状态栏半透明

    StatusBarUtil.setTranslucent(Activity activity, int statusBarAlpha)

  3. 设置状态栏全透明

    StatusBarUtil.setTransparent(Activity activity)

  4. 为包含 DrawerLayout 的界面设置状态栏颜色(也可以设置半透明和全透明)

    StatusBarUtil.setColorForDrawerLayout(Activity activity, DrawerLayout drawerLayout, int color)

  5. 为使用 ImageView 作为头部的界面设置状态栏透明

    StatusBarUtil.setTranslucentForImageView(Activity activity, int statusBarAlpha, View needOffsetView)

  6. 在 Fragment 中使用

  7. 为滑动返回界面设置状态栏颜色

    推荐配合 bingoogolapple/BGASwipeBackLayout-Android: Android Activity 滑动返回 这个库一起使用。

    StatusBarUtil.setColorForSwipeBack(Activity activity, @ColorInt int color, int statusBarAlpha)

  8. 通过传入 statusBarAlpha 参数,可以改变状态栏的透明度值,默认值是112。

使用

  1. 在 build.gradle 文件中添加依赖, StatusBarUtil 已经发布在 JCenter:

    compile 'com.jaeger.statusbarutil:library:1.4.0'
  2. 在 setContentView() 之后调用你需要的方法,例如:

    setContentView(R.layout.main_activity);
    ...
    StatusBarUtil.setColor(MainActivity.this, mColor);
  3. 如果你在一个包含 DrawerLayout 的界面中使用, 你需要在布局文件中为 DrawerLayout 添加 android:fitsSystemWindows="true" 属性:

    <android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    ...

    </android.support.v4.widget.DrawerLayout>
  4. 滑动返回界面设置状态栏颜色:

    建议配合 bingoogolapple/BGASwipeBackLayout-Android: Android Activity 滑动返回 库一起使用。

    StatusBarUtil.setColorForSwipeBack(Activity activity, @ColorInt int color, int statusBarAlpha)
  5. 当你设置了 statusBarAlpha 值时,该值需要在 0 ~ 255 之间

  6. 在 Fragment 中的使用可以参照 UseInFragmentActivity.java 来实现

收起阅读 »

还在自己写Adapter?再不看你就out了

base-adapterAndroid 万能的Adapter for ListView,RecyclerView,GridView等,支持多种Item类型的情况。引入ForRecyclerViewcompile 'com.zhy:base-rvadapter:...
继续阅读 »

base-adapter

Android 万能的Adapter for ListView,RecyclerView,GridView等,支持多种Item类型的情况。

引入

ForRecyclerView

compile 'com.zhy:base-rvadapter:3.0.3'

ForListView

compile 'com.zhy:base-adapter:3.0.3'

使用

##(1)简单的数据绑定(ListView与其使用方式一致)

首先看我们最常用的单种Item的书写方式:

mRecyclerView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
@Override
public void convert(ViewHolder holder, String s)
{
holder.setText(R.id.id_item_list_title, s);
}
});

是不是相当方便,在convert方法中完成数据、事件绑定即可。

只需要简单的将Adapter继承CommonAdapter,复写convert方法即可。省去了自己编写ViewHolder等大量的重复的代码。

  • 可以通过holder.getView(id)拿到任何控件。
  • ViewHolder中封装了大量的常用的方法,比如holder.setText(id,text),holder.setOnClickListener(id,listener)等,可以支持使用。

效果图:

##(2)多种ItemViewType(ListView与其使用方式一致)

对于多中itemviewtype的处理参考:https://github.com/sockeqwe/AdapterDelegates ,具有极高的扩展性。

MultiItemTypeAdapter adapter = new MultiItemTypeAdapter(this,mDatas);
adapter.addItemViewDelegate(new MsgSendItemDelagate());
adapter.addItemViewDelegate(new MsgComingItemDelagate());

每种Item类型对应一个ItemViewDelegete,例如:

public class MsgComingItemDelagate implements ItemViewDelegate<ChatMessage>
{

@Override
public int getItemViewLayoutId()
{
return R.layout.main_chat_from_msg;
}

@Override
public boolean isForViewType(ChatMessage item, int position)
{
return item.isComMeg();
}

@Override
public void convert(ViewHolder holder, ChatMessage chatMessage, int position)
{
holder.setText(R.id.chat_from_content, chatMessage.getContent());
holder.setText(R.id.chat_from_name, chatMessage.getName());
holder.setImageResource(R.id.chat_from_icon, chatMessage.getIcon());
}
}

贴个效果图:

##(3) 添加HeaderView、FooterView

mHeaderAndFooterWrapper = new HeaderAndFooterWrapper(mAdapter);

TextView t1 = new TextView(this);
t1.setText("Header 1");
TextView t2 = new TextView(this);
t2.setText("Header 2");
mHeaderAndFooterWrapper.addHeaderView(t1);
mHeaderAndFooterWrapper.addHeaderView(t2);

mRecyclerView.setAdapter(mHeaderAndFooterWrapper);
mHeaderAndFooterWrapper.notifyDataSetChanged();

类似装饰者模式,直接将原本的adapter传入,初始化一个HeaderAndFooterWrapper对象,然后调用相关API添加。

##(4) 添加LoadMore

mLoadMoreWrapper = new LoadMoreWrapper(mOriginAdapter);
mLoadMoreWrapper.setLoadMoreView(R.layout.default_loading);
mLoadMoreWrapper.setOnLoadMoreListener(new LoadMoreWrapper.OnLoadMoreListener()
{
@Override
public void onLoadMoreRequested()
{
}
});

mRecyclerView.setAdapter(mLoadMoreWrapper);

直接将原本的adapter传入,初始化一个LoadMoreWrapper对象,然后调用相关API即可。

##(5)添加EmptyView

mEmptyWrapper = new EmptyWrapper(mAdapter);
mEmptyWrapper.setEmptyView(R.layout.empty_view);

mRecyclerView.setAdapter(mEmptyWrapper );

直接将原本的adapter传入,初始化一个EmptyWrapper对象,然后调用相关API即可。

支持链式添加多种功能,示例代码:

mAdapter = new EmptyViewWrapper(
new LoadMoreWrapper(
new HeaderAndFooterWrapper(mOriginAdapter)));

一些回调

onViewHolderCreated

mListView.setAdapter(new CommonAdapter<String>(this, R.layout.item_list, mDatas)
{
@Override
public void convert(ViewHolder holder, String o, int pos)
{
holder.setText(R.id.id_item_list_title, o);
}

@Override
public void onViewHolderCreated(ViewHolder holder, View itemView)
{
super.onViewHolderCreated(holder, itemView);
//AutoUtil.autoSize(itemView)
}
});

代码下载:
baseAdapter-master.zip
收起阅读 »

为什么别人都在摸鱼就你在加班?用对工具让你事半功倍!

主要包括:缓存(图片缓存、预取缓存、网络缓存)、公共View(下拉及底部加载更多ListView、底部加载更多ScrollView、滑动一页Gallery)及Android常用工具类(网络、下载、Android资源操作、shell、文件、Json、随机数、Co...
继续阅读 »

主要包括缓存(图片缓存、预取缓存、网络缓存)、公共View(下拉及底部加载更多ListView、底部加载更多ScrollView、滑动一页Gallery)及Android常用工具类(网络、下载、Android资源操作、shell、文件、Json、随机数、Collection等等)。
示例源码:TrineaAndroidDemo
使        用:拉取代码导入IDE,右击你的工程->properties->Android,在library中选择TrineaAndroidCommon。
Api Guide:TrineaAndroidCommon API Guide

Dev Tools App

The Dev Tools App is a powerful android development tool that can help you improve efficiency greatly, It can be used to view the latest open source projects, view activity history, view manifest, decompile, color picker, extract apk or so, view app info, open or close the options in the developer options quickly, and more.

You can download it from DevTools@Google Play.

一. 缓存类

主要特性:(1).使用简单 (2).轻松获取及预取取新图片 (3).包含二级缓存 (4).可选择多种缓存算法(FIFO、LIFO、LRU、MRU、LFU、MFU等13种)或自定义缓存算法 (5).可方便的保存及初始化恢复数据 (6).省流量性能佳(有且仅有一个线程获取图片) (7).支持http请求header设置及不同类型网络处理(8).可根据系统配置初始化缓存 (9).扩展性强 (10).支持等待队列 (11)包含map的大多数接口。

1. 图片缓存

使用见:图片缓存的使用
适用:获取图片较多且图片使用频繁的应用,包含二级缓存,如新浪微博、twitter、微信头像、美丽说、蘑菇街、花瓣、淘宝等等。效果图如下:
ImageCahe

2. 图片SD卡缓存

使用见:图片SD卡缓存的使用
适用:应用中获取图片较多且图片较大的情况。需要二级缓存及ListView或GridView图片加载推荐使用上面的ImageCache。效果图如下:
ImageSDCardCache

3. 网络缓存

使用见:Android网络缓存
适用:网络获取内容不大的应用,尤其是api接口数据,如新浪微博、twitter的timeline、微信公众账号发送的内容等等。效果图如下:
HttpCache

4. 预取数据缓存

使用见:预取数据缓存
缓存类关系图如下:其中HttpCache为后续计划的http缓存 Image Cache

二. 公用的view

1. 下拉刷新及滚动到底部加载更多的Listview

使用: 下拉刷新及滚动到底部加载更多listview的使用
实现原理: http://trinea.iteye.com/blog/1562281。效果图如下:
DropDownListView

2. 滑动一页(一个Item)的Gallery

使用及实现原理:滑动一页(一个Item)的Gallery的使用。效果图如下:
ViewPager1 ViewPager2

3. 滑动到底部或顶部响应的ScrollView

使用及实现原理: 滚动到底部或顶部响应的ScrollView使用。效果图如下:
ScrollView

三. 工具类

具体介绍可见:Android常用工具类
目前包括HttpUtils、DownloadManagerProShellUtilsPackageUtils、PreferencesUtils、JSONUtils、FileUtils、ResourceUtils、StringUtils、ParcelUtils、RandomUtils、ArrayUtils、ImageUtils、ListUtils、MapUtils、ObjectUtils、SerializeUtils、SystemUtils、TimeUtils。

1. Android系统下载管理DownloadManager使用

使用示例:Android系统下载管理DownloadManager功能介绍及使用示例
功能扩展:Android下载管理DownloadManager功能扩展和bug修改 效果图如下:
downloadManagerDemo

2. Android APK root权限静默安装

使用示例:Android APK root权限静默安装

3. Android root权限

直接调用ShellUtils.execCommand方法

4. 图片工具类

(1)Drawable、Bitmap、byte数组相互转换; (2)根据url获得InputStream、Drawable、Bitmap
更多工具类介绍见Android常用工具类

Proguard

-keep class cn.trinea.android.** { *; }
-keepclassmembers class cn.trinea.android.** { *; }
-dontwarn cn.trinea.android.**

Download

Gradle:

compile 'cn.trinea.android.common:trinea-android-common:4.2.15'



代码下载:android-common-master.zip

收起阅读 »

别问我为啥用这个来扫二维码!做开发的都在用

zxing基本使用 官方提供了zxing在Android机子上的使用例子,https://github.com/zxing/zxing/tree/master/android,作为官方的例子,zxing-android考虑了各种各样的情况,包括多种解析格式、...
继续阅读 »

zxing基本使用


官方提供了zxing在Android机子上的使用例子,https://github.com/zxing/zxing/tree/master/android,作为官方的例子,zxing-android考虑了各种各样的情况,包括多种解析格式、解析得到的结果分类、长时间无活动自动销毁机制等。有时候我们需要根据自己的情况定制使用需求,因此会精简官方给的例子。在项目中,我们仅仅用来实现扫描二维码和识别图片二维码两个功能。为了实现高精度的二维码识别,在zxing原有项目的基础上,本文做了大量改进,使得二维码识别的效率有所提升。先来看看工程的项目结构。









1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.
├── QrCodeActivity.java
├── camera
│   ├── AutoFocusCallback.java
│   ├── CameraConfigurationManager.java
│   ├── CameraManager.java
│   └── PreviewCallback.java
├── decode
│   ├── CaptureActivityHandler.java
│   ├── DecodeHandler.java
│   ├── DecodeImageCallback.java
│   ├── DecodeImageThread.java
│   ├── DecodeManager.java
│   ├── DecodeThread.java
│   ├── FinishListener.java
│   └── InactivityTimer.java
├── utils
│   ├── QrUtils.java
│   └── ScreenUtils.java
└── view
└── QrCodeFinderView.java

源码比较简单,这里不做过多地讲解,大部分方法都有注释。主要分为几大块,



  • camera


主要实现相机的配置和管理,相机自动聚焦功能,以及相机成像回调(通过byte[]数组返回实际的数据)。



  • decode


图片解析相关类。通过相机扫描二维码和解析图片使用两套逻辑。前者对实时性要求比较高,后者对解析结果要求较高,因此采用不同的配置。相机扫描主要在DecodeHandler里通过串行的方式解析,图片识别主要通过线程DecodeImageThread异步调用返回回调的结果。FinishListenerInactivityTimer用来控制长时间无活动时自动销毁创建的Activity,避免耗电。



  • utils


图片二维码解析工具类,以及获取屏幕宽高的工具类。



  • view


这个包里只有一个类QrCodeFinderView,官方原本是使用这个类绘制扫描区域框,并且必须在扫描区域里才能识别二维码。我把这个类稍作修改,仅仅用来展示扫描区域,实际在相机扫描二维码的时候,只要在SurfaceView区域范围内,结果都是有效的。



  • QrCodeActivity


启动类,包含相机扫描二维码以及选择图片入口。


zxing源码存在的问题及解决方案


zxing项目源码实现了基本的二维码扫描及图片识别程序,但下载过源码并直接运行的童鞋都知道,例子存在很多的问题,包括基本的识别精准度不高、扫描区域小、部分手机存在预览图形拉伸、默认横向扫描、还有自定义扫描界面困难等问题。


资源下载:zxing

收起阅读 »

不会管理日志,还做什么开发?

Logger 基本用法 简介 Simple, pretty and powerful logger for android 为Android提供的,简单、强大而且格式美观的工具 本质就是封装系统提供的Log类,加上一些分割线易于查找不同的Log;...
继续阅读 »


Logger 基本用法


简介




Simple, pretty and powerful logger for android
为Android提供的,简单、强大而且格式美观的工具


本质就是封装系统提供的Log类,加上一些分割线易于查找不同的Log;logcat中显示的信息可配置。最初的样子如下图



包含线程信息、Log所在的类、方法及所在行数。


这里我忍不住了,就先写了我最喜欢的功能,嘎嘎嘎~~
最最最基本的依赖和简单打印在第二页


我觉得最好的功能是:Logger支持设置日志保存到本地,这样的话就可以想上传上传了。做自己的日志管理系统倍爽!
不过日志保存的位置写死了。找位置的方法是
在Logger包里的 DiskLogAdapter类的构造函数,进入build()方法里。


public DiskLogAdapter() {
formatStrategy = CsvFormatStrategy.newBuilder().build();
}

进入build(),就可以找到相应的路径


@NonNull public CsvFormatStrategy build() {
if (date == null) {
date = new Date();
}
if (dateFormat == null) {
dateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss.SSS", Locale.UK);
}
if (logStrategy == null) {
//地址在这里
String diskPath = Environment.getExternalStorageDirectory().getAbsolutePath();
String folder = diskPath + File.separatorChar + "logger";
HandlerThread ht = new HandlerThread("AndroidFileLogger." + folder);
ht.start();
Handler handler = new DiskLogStrategy.WriteHandler(ht.getLooper(), folder, MAX_BYTES);
logStrategy = new DiskLogStrategy(handler);
}
return new CsvFormatStrategy(this);
}

具体路径是:/storage/emulated/0/logger
每个文件最大为500K,源码贴出来~~~~
private static final int MAX_BYTES = 500 * 1024; // 500K averages to a 4000 lines per file


生成的文件名称为logs_0.csv 后面的数字会递增 源码贴出来~~~~
newFile = new File(folder, String.format("%s_%s.csv", fileName, newFileCount));




刚用markDown不太会用




我最喜欢的部分写完了,下面写点常规的操作吧。simple


一、依赖Logger


地址:https://github.com/orhanobut/logger
本来不想贴地址呢,github是个好东西,介于我两天前还不会用github,还是贴上吧,啦啦啦,我是莉莉的小叮当
github上介绍很详细了,但是我还是想粘贴一遍。


依赖


dependencies {
implementation 'com.orhanobut:logger:2.2.0'
}

初始化


Logger.addLogAdapter(new AndroidLogAdapter());

到这里Logger已经可以用了
Logger.d(“debug”);
Logger.e(“error”);
Logger.w(“warning”);
Logger.v(“verbose”);
Logger.i(“information”);
Logger.wtf(“What a Terrible Failure”);


下面写点我自己的Logger用法


    val formatStrategy = PrettyFormatStrategy.newBuilder()
.showThreadInfo(true) //(可选)是否显示线程信息。 默认值为true
.methodCount(1) // (可选)要显示的方法行数。 默认2
.methodOffset(5) // (可选)隐藏内部方法调用到偏移量。 默认5
.tag("doShare")//(可选)每个日志的全局标记。 默认PRETTY_LOGGER
.build()
Logger.addLogAdapter(AndroidLogAdapter(formatStrategy))//根据上面的格式设置logger相应的适配器
Logger.addLogAdapter(DiskLogAdapter())//保存到文件


资源下载:logger-master.zip


收起阅读 »

昨天我被开了,技术总监说:不会Arouter做什么架构师

ARouter,A framework for assisting in the renovation of Android componentization (帮助 Android App 进行组件化改造的路由框架) —— 支持模块间的路由、通信、解耦 官...
继续阅读 »


ARouter,A framework for assisting in the renovation of Android componentization (帮助 Android App 进行组件化改造的路由框架) —— 支持模块间的路由、通信、解耦


官方中文介绍:
https://github.com/alibaba/ARouter/blob/master/README_CN.md
(中文比英文文档,详尽得多…,良心文档啊)


基本使用


1.添加依赖

android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
//注解处理器需要的模块名,作为路径映射的前缀
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}

dependencies {
implementation 'com.alibaba:arouter-api:1.4.1'
annotationProcessor 'com.alibaba:arouter-compiler:1.2.2' //注解处理器,会将注解编译成Java类
}

2.添加注解

//注意:路劲至少两级,即xx/xx,前一个xx用于分组
@Route(path = "/test/second")
public class SecondActivity extends AppCompatActivity {
}

3.初始化SDK

一般在Application中初始化


ARouter.init(this);

4.使用

//很简单,一句话完成,可携带参数
ARouter.getInstance().build("/test/second").navigation();

原理浅析


从ARouter.getInstance().build("/test/second").navigation();出发,解释其跳转基本过程。
先上一张时序图:
在这里插入图片描述


1.ARouter.build(path)构建Postcard

ARouter只是对外统一的api接口,实现基本由_ARouter完成,所以构建Postcard也是由_ARouter,构建,build(path, extractGroup(path))中extractGroup方法,就是把/xx/xx中前面的xx转换为默认group的方法,这也是之前必须使用2级以上目录的原因。build到此就完成了,此时还没有完成映射到activity的任务,只是把path浅析了下。


2.Postcard.navigation()实现跳转

最后也是由_ARouter完成,在_ARouter.navigation时,首先调用LogisticsCenter.completion(postcard)完善postcard的信息,而completion方法,则完成了path到activity的转换关系。完善后,再调用_navigation完成最终跳转。


3.LogisticsCenter.completion(postcard)将path映射到activity

核心部分:


public synchronized static void completion(Postcard postcard) {
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
if (null == routeMeta) {
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());
if (null == groupMeta) {
throw new NoRouteFoundException();
} else {
// Load route and cache it into memory, then delete from metas.
try {
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
iGroupInstance.loadInto(Warehouse.routes);
Warehouse.groupsIndex.remove(postcard.getGroup());
} catch (Exception e) {
throw new HandlerException();
}

completion(postcard); // Reload
}
} else {
postcard.setDestination(routeMeta.getDestination()); //destination就是需要跳转的Activity.class
......
}
}

首先去Warehouse的routes中寻找RouteMeta(路由元数据)。
Warehouse可以理解为存储路由元数据的容器,包括:路由关系、拦截器、provider的映射关系等。RouteMeta既持有activity等对应跳转类信息。
首次navigation时,RouteMeta == null,故会用postcard build时的group path先找到对一个的IRouteGroup信息[IRouteGroup何时加载到Warehouse的,见下条],然后通过iGroupInstance.loadInto将改分组下的RouteMeta都加载到缓存中,这可以理解为延迟加载,降低初始化时的一些压力。加载后,再重新调用completion(postcard)。


4.IRouteGroup的加载

路由元数据RouteMeta是从实现了IRouteGroup接口的实例中load进来的,这个实现了IRouteGroup接口的类在哪?何时load进Warehouse?在LogisticsCenter.init()方法中,找到了踪迹。


public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
Set<String> routerMap;
// It will rebuild router map every times when debuggable.
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
// These class was generated by arouter-compiler.
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
if (!routerMap.isEmpty()) {
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
}

PackageUtils.updateVersion(context); // Save new version name when router map update finishes.
} else {
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}

for (String className : routerMap) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
// This one of root elements, load root.
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
}
......
}
}

首次init时,通过ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)将ROUTE_ROOT_PAKCAGE = "com.alibaba.android.arouter.routes"下的所有类,都加载进routerMap中。然后通过区分routerMap类名,实例化com.alibaba.android.arouter.routes.ARouter##Root开头的类[由于csdn对$的支持并不友好,故以下$都使用#代替],调用其loadInto将对应的IRouteGroup类都加载到Warehouse.groupsIndex中。
PS:
init方法有一些小细节,针对debug版本或者新版本,才会有一次完整的类find和load的过程,加载完后会将类路径都存入sp,之后启动从sp拿,以增加启动速度。
ClassUtils.getFileNameByPackageName方法并不简单,其中牵扯了在多dex或者特殊rom下加载类的一些处理,有兴趣的同学可以阅读源码了解。


ARouter##Root##app


public class ARouter$$Root$$app implements IRouteRoot {
public ARouter$$Root$$app() {
}

public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("test", test.class);
}
}

ARouter##Group##test


public class ARouter$$Group$$test implements IRouteGroup {
public ARouter$$Group$$test() {
}

public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/test/second", RouteMeta.build(RouteType.ACTIVITY, SecondActivity.class, "/test/second", "test", (Map)null, -1, -2147483648));
}
}

5.Route类的生成

接着上一步的解析,下面要了解的是,ARouter##Root##app,ARouter##Group##test这些编译好的类,是何时/如何生成的。这里使用的是android的apt(Annotation Processing Tool)技术,即在编译时,将注解生成为Java代码。所以在build.gradle添加依赖时,会添加注解处理器 annotationProcessor ‘com.alibaba:arouter-compiler:1.2.2’ ,具体的处理过程,可以参见arouter-compiler的RouteProcessor,RouteProcessor代码较长,这里就不详述了。感兴趣的童鞋,可以自己写一写注解处理器,com.squareup.javapoet神器了解一下。


资源下载:arouter-develop.zip

收起阅读 »

recycleview适配器,不看后悔到35岁

在我最近开发的一个Android项目当中,用到列表的地方非常多。用RecyclerView+BaseRecyclerViewAdapterHelper(开源框架)可以帮我们节省大量的代码(约节省三分之二),RecyclerView不用多说大家非常熟悉,谷歌推荐...
继续阅读 »


在我最近开发的一个Android项目当中,用到列表的地方非常多。用RecyclerView+BaseRecyclerViewAdapterHelper(开源框架)可以帮我们节省大量的代码(约节省三分之二),RecyclerView不用多说大家非常熟悉,谷歌推荐的列表控件,代替了传统的ListView,更加强大和灵活。BaseRecyclerViewAdapterHelper是一个非常强大的开源框架,它基本上可以解决我们开发中的列表布局。在这里记录一下这个框架。


框架引入


先在项目的 build.gradle中的 repositories 添加


allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}

然后在Module的 build.gradle中的 dependencies 添加


	dependencies {
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.47'
}

基本使用


首先我们在Activity中有一个RecyclerView



android:layout_width="match_parent"
android:layout_height="match_parent">

android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"/>



再新建一个item布局,item布局是一个简单的头像和名字



android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginVertical="5dp">

android:id="@+id/iv_head"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginStart="10dp"
android:layout_gravity="center_vertical"
android:src="@mipmap/ic_launcher"/>

android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_gravity="center_vertical"
android:text="张三"
android:textSize="18sp"
android:textColor="#000000"/>



再根据item所需数据,编写数据实体类型


public class User {
private String headUrl;
private String name;

public User(String headUrl, String name) {
this.headUrl = headUrl;
this.name = name;
}

public String getHeadUrl() {
return headUrl;
}

public void setHeadUrl(String headUrl) {
this.headUrl = headUrl;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

接下来就要用到我们的BaseRecyclerViewAdapterHelper框架来编写适配器


public class UserAdapter extends BaseQuickAdapter {
public UserAdapter(int layoutResId, @Nullable List data) {
super(layoutResId, data);
}

@Override
protected void convert(BaseViewHolder helper, User item) {
Glide.with(mContext).load(item.getHeadUrl()).into((ImageView)helper.getView(R.id.iv_head));
helper.setText(R.id.tv_name, item.getName());
}
}

在这里我们用到图片加载框架,非常的好用,一行代码就可以加载url图片等,这里就不详细多说,GitHub地址:https://github.com/bumptech/glide


最后一步就是在我们的Activity使用该适配器


public class MainActivity extends AppCompatActivity {
private RecyclerView recycler;

private List userList;

private UserAdapter adapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

recycler = findViewById(R.id.recycler);

//模拟数据,实际开发中一般是从后台获取数据
userList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
userList.add(new User("http://img2.imgtn.bdimg.com/it/u=3749323882,846155126&fm=26&gp=0.jpg",
"第" + i + "条"));
}

//设置布局管理
recycler.setLayoutManager(new LinearLayoutManager(this));
//创建适配器
adapter = new UserAdapter(R.layout.item_recycler, userList);
//给RecyclerView设置适配器
recycler.setAdapter(adapter);
}
}

这样就是RecyclerView+BaseRecyclerViewAdapterHelper的基本使用,效果如下
在这里插入图片描述


点击事件


使用列表那当然也少不了点击事件,不论是整个item的点击事件还是item中的子控件都可以实现。


item的点击事件


		adapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {
@Override
public void onItemClick(BaseQuickAdapter adapter, View view, int position) {
Toast.makeText(MainActivity.this, "点击了第" + position + "条", Toast.LENGTH_SHORT).show();
}
});

item的长按事件


		adapter.setOnItemLongClickListener(new BaseQuickAdapter.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(BaseQuickAdapter adapter, View view, int position) {
Toast.makeText(MainActivity.this, "长按了第" + position + "条", Toast.LENGTH_SHORT).show();
return false;
}
});

item中子控件的点击事件
首先在适配器中绑定子控件


public class UserAdapter extends BaseQuickAdapter {
public UserAdapter(int layoutResId, @Nullable List data) {
super(layoutResId, data);
}

@Override
protected void convert(BaseViewHolder helper, User item) {
Glide.with(mContext).load(item.getHeadUrl()).into((ImageView)helper.getView(R.id.iv_head));
helper.setText(R.id.tv_name, item.getName());
//绑定点击事件
helper.addOnClickListener(R.id.iv_head);
helper.addOnClickListener(R.id.tv_name);
}
}

接着在activity中就可以监听到子控件的点击事件,根据view.getId()来区分点击了哪一个子控件


		adapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() {
@Override
public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
switch (view.getId()) {
case R.id.iv_head:
Toast.makeText(MainActivity.this, "点击了第" + position + "的头像",
Toast.LENGTH_SHORT).show();
break;
case R.id.tv_name:
Toast.makeText(MainActivity.this, "点击了第" + position + "的名字",
Toast.LENGTH_SHORT).show();
break;
default:
break;
}
}
});

子控件的长按事件也是如此。


常用方法


上面介绍了基本的使用方法,还有很多常用的方法这里列举一下:



































方法名 说明
getData() 获取适配器中的数据
addData(data) 向列表中添加数据(可一条,可多条)
setData(position, data) 修改指定位置的数据
setNewData(List) 设置适配器新的数据
notifyDataSetChanged() 刷新适配器
remove(position) 删除指定数据

BaseRecyclerViewAdapterHelper还有很多其他功能,例如列表加载动画、下拉刷新、上拉加载、添加分组、自定义不同的item、拖拽item等,这里就不一一列举出来,有兴趣的可以去官方GitHub了解更多,地址如下:https://github.com/CymChad/BaseRecyclerViewAdapterHelper


资源下载:BaseRecyclerViewAdapterHelper-master (1).zip

收起阅读 »

Android开发者你好~你还在用dialog????

 https://github.com/li-xiaojun/XPopup/ 内置几种了常用的弹窗,十几种良好的动画,将弹窗和动画的自定义设计的极其简单;目前还没有出现XPopup实现不了的弹窗效果。 内置弹窗允许你使用项目已有的布局,同时还能...
继续阅读 »

 https://github.com/li-xiaojun/XPopup/



  • 内置几种了常用的弹窗,十几种良好的动画,将弹窗和动画的自定义设计的极其简单;目前还没有出现XPopup实现不了的弹窗效果。 内置弹窗允许你使用项目已有的布局,同时还能用上XPopup提供的动画,交互和逻辑封装。

  • UI动画简洁,遵循Material Design,在设计动画的时候考虑了很多细节,过渡,层级的变化

  • 交互优雅,实现了优雅的手势交互,智能的嵌套滚动,智能的输入法交互,具体看Demo

  • 适配全面屏,目前适配了小米,华为,谷歌,OPPO,VIVO,三星,魅族,一加全系全面屏手机

  • 自动监听Activity生命周期,自动释放资源。在Activity直接finish的场景也避免了内存泄漏

  • 很好的易用性,所有的自定义弹窗只需继承对应的类,实现你的布局,然后像Activity那样,在onCreate方法写逻辑即可

  • 性能优异,动画流畅;精心优化的动画,让你很难遇到卡顿场景

  • 能在应用后台弹出(需要申请悬浮窗权限,一行代码即可)

  • 支持androidx

  • 完美支持RTL布局

  • 如果你想要时间选择器和城市选择器,可以使用XPopup扩展功能库XPopupExt: https://github.com/li-xiaojun/XPopupExt

  • 设计思路: 综合常见的弹窗场景,我将其分为几类:


  • Center类型,就是在中间弹出的弹窗,比如确认和取消弹窗,Loading弹窗

  • Bottom类型,就是从页面底部弹出,比如从底部弹出的分享窗体,知乎的从底部弹出的评论列表,内部已经处理好手势拖拽和嵌套滚动

  • Attach类型,就是弹窗的位置需要依附于某个View或者某个触摸点,就像系统的PopupMenu效果一样,但PopupMenu的自定义性很差,淘宝的商品列表筛选的下拉弹窗,微信的朋友圈点赞弹窗都是这种。

  • Drawer类型,就是从窗体的坐边或者右边弹出,并支持手势拖拽;好处是与界面解耦,可以在任何界面实现DrawerLayout效果

  • ImageViewer大图浏览类型,就像掘金那样的图片浏览弹窗,带有良好的拖拽交互体验,内部嵌入了改良的PhotoView

  • FullScreen类型,全屏弹窗,看起来和Activity一样,可以设置任意的动画器;适合用来实现登录,选择性的界面效果。

  • Position自由定位弹窗,弹窗是自由的,你可放在屏幕左上角,右下角,或者任意地方,结合强大的动画器,可以实现各种效果。


 


implementation 'com.lxj:xpopup:2.0.0'

底部弹窗,自定义布局


new XPopup.Builder(this)
.asCustom(new RefundPopup(this, new RefundPopup.OnClickListener() {
@Override
public void clickConfirm() {
new XPopup.Builder(GoodsOrderDetailActivity.this)
.asCustom(new RefundReasonPopup(GoodsOrderDetailActivity.this, new RefundReasonPopup.OnClickListener() {
@Override
public void clickConfirm(String tag,String msg) {
getPresenter().applyRefund(goodsOrderId,tag,msg);
}
})).show();
}
})).show();

 



public class RefundPopup extends BottomPopupView {

private Context context;
private OnClickListener mOnClickListener;

public RefundPopup(@NonNull Context context) {
super(context);
this.context = context;
}

public RefundPopup(@NonNull Context context, OnClickListener onClickListener) {
super(context);
this.context = context;
mOnClickListener = onClickListener;
}

@Override
protected int getImplLayoutId() {
return R.layout.popup_refund;
}

protected int getPopupWidth() {
return AutoUtils.getPercentWidthSize(750);
}

@Override
protected void onCreate() {
super.onCreate();
ImageView ivBack = findViewById(R.id.ivBack);
TextView tvConfirm = findViewById(R.id.tvConfirm);
TextView tvCancel = findViewById(R.id.tvCancel);
ivBack.setOnClickListener(view -> {
dismiss();
});
tvCancel.setOnClickListener(view -> dismiss());
tvConfirm.setOnClickListener(view -> {
mOnClickListener.clickConfirm();
dismiss();
});
}

public interface OnClickListener {
void clickConfirm();
}

}

资源下载:xpopup-master.zip


收起阅读 »

Android超级高效换肤框架,让你体验无闪烁换肤

用法1. 在Application中进行初始化public class SkinApplication extends Application { public void onCreate() { super.onCreate(); // Must ...
继续阅读 »

用法

1. 在Application中进行初始化

public class SkinApplication extends Application {
public void onCreate() {
super.onCreate();
// Must call init first
SkinManager.getInstance().init(this);
SkinManager.getInstance().load();
}
}

2. 在布局文件中标识需要换肤的View

...
xmlns:skin="http://schemas.android.com/android/skin"
...
<TextView
...
skin:enable="true"
... />

3. 继承BaseActivity或者BaseFragmentActivity作为BaseActivity进行开发

4. 从.skin文件中设置皮肤

String SKIN_NAME = "BlackFantacy.skin";
String SKIN_DIR = Environment.getExternalStorageDirectory() + File.separator + SKIN_NAME;
File skin = new File(SKIN_DIR);
SkinManager.getInstance().load(skin.getAbsolutePath(),
new ILoaderListener() {
@Override
public void onStart() {
}

@Override
public void onSuccess() {
}

@Override
public void onFailed() {
}
});

5. 重设默认皮肤

SkinManager.getInstance().restoreDefaultTheme();

6. 对代码中创建的View的换肤支持

主要由IDynamicNewView接口实现该功能,在BaseActivityBaseFragmentActivityBaseFragment中已经实现该接口.

public interface IDynamicNewView {
void dynamicAddView(View view, List<DynamicAttr> pDAttrs);
}

**用法:**动态创建View后,调用dynamicAddView方法注册该View至皮肤映射表即可(如下).详见sample工程

	private void dynamicAddTitleView() {
TextView textView = new TextView(getActivity());
textView.setText("Small Article (动态new的View)");
RelativeLayout.LayoutParams param = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
param.addRule(RelativeLayout.CENTER_IN_PARENT);
textView.setLayoutParams(param);
textView.setTextColor(getActivity().getResources().getColor(R.color.color_title_bar_text));
textView.setTextSize(20);
titleBarLayout.addView(textView);

List<DynamicAttr> mDynamicAttr = new ArrayList<DynamicAttr>();
mDynamicAttr.add(new DynamicAttr(AttrFactory.TEXT_COLOR, R.color.color_title_bar_text));
dynamicAddView(textView, mDynamicAttr);
}

7. 皮肤包是什么?如何生成?

  • 皮肤包(后缀名为.skin)的本质是一个apk文件,该apk文件不包含代码,只包含资源文件
  • 在皮肤包工程中(示例工程为skin/BlackFantacy)添加需要换肤的同名的资源文件,直接编译生成apk文件,再更改后缀名为.skinj即可(防止用户点击安装)
  • 使用gradle的同学,buildandroid-skin-loader-skin工程后即可在skin-package目录下取皮肤包(修改脚本中def skinName = "BlackFantacy.skin"换成自己想要的皮肤名)

代码下载:Android-Skin-Loader-master.zip

收起阅读 »

炫酷动画统计图表库:CurveGraphView

CurveGraphView 是一个带有炫酷动画统计图表库,除了性能出色并具有许多样式选项之外,该库还支持单个平面内的多个线图。多个折线图对于比较不同股票,共同基金,加密货币等的价格非常有用。10.1 如何使用?1、在build.gradle 中添加如下依赖:...
继续阅读 »

CurveGraphView 是一个带有炫酷动画统计图表库,除了性能出色并具有许多样式选项之外,该库还支持单个平面内的多个线图。

多个折线图对于比较不同股票,共同基金,加密货币等的价格非常有用。

10.1 如何使用?

1、在build.gradle 中添加如下依赖:

dependencies {
implementation 'com.github.swapnil1104:CurveGraphView:{current_lib_ver}'
}

2、在xml文件中添加布局:

 

然后在代码中添加各种配置项

curveGraphView = findViewById(R.id.cgv);

curveGraphView.configure(
new CurveGraphConfig.Builder(this)
.setAxisColor(R.color.Blue) // Set X and Y axis line color stroke.
.setIntervalDisplayCount(7) // Set number of values to be displayed in X ax
.setGuidelineCount(2) // Set number of background guidelines to be shown.
.setGuidelineColor(R.color.GreenYellow) // Set color of the visible guidelines.
.setNoDataMsg(" No Data ") // Message when no data is provided to the view.
.setxAxisScaleTextColor(R.color.Black) // Set X axis scale text color.
.setyAxisScaleTextColor(R.color.Black) // Set Y axis scale text color
.build()
););

3、 提供数据集

PointMap pointMap = new PointMap();
pointMap.addPoint(0, 100);
pointMap.addPoint(1, 500);
pointMap.addPoint(5, 800);
pointMap.addPoint(4, 600);
10.2 效果图
效果1效果2

更多详细使用方式请看Github: https://github.com/swapnil1104/CurveGraphView

下载地址:CurveGraphView-master.zip

收起阅读 »

数据集的圆弧形控件:Donut

这个一个可以展示多个数据集的圆弧形控件,具有精细的颗粒控制、间隙功能、动画选项以及按比例缩放其值的功能。可以用于项目中的一些数据统计。9.1 如何使用?在build.gradle 中添加如下依赖:dependencies { implementatio...
继续阅读 »

这个一个可以展示多个数据集的圆弧形控件,具有精细的颗粒控制、间隙功能、动画选项以及按比例缩放其值的功能。可以用于项目中的一些数据统计。

9.1 如何使用?

build.gradle 中添加如下依赖:

dependencies {
implementation("app.futured.donut:library:$version")
}

然后在布局文件中添加View:


然后在代码中设置数据:

val dataset1 = DonutDataset(
name = "dataset_1",
color = Color.parseColor("#FB1D32"),
amount = 1f
)

val dataset2 = DonutDataset(
name = "dataset_2",
color = Color.parseColor("#FFB98E"),
amount = 1f
)

donut_view.cap = 5f
donut_view.submitData(listOf(dataset1, dataset2))
9.2 效果图

更多用法请看Github: https://github.com/futuredapp/donut

下载地址:donut-master.zip

收起阅读 »

View切换的过渡动画库:TransformationLayout

这是一个用于Activity或者Fragment 以及View切换的过渡动画库,效果非常炫,它使用Material Design的运动系统过渡模式来创建变形动画。该库提供了用于绑定目标视图,设置淡入淡出和路径运动方向以及许多其他自定义选项的属性。8.1 如何使...
继续阅读 »

这是一个用于Activity或者Fragment 以及View切换的过渡动画库,效果非常炫,它使用Material Design的运动系统过渡模式来创建变形动画。该库提供了用于绑定目标视图,设置淡入淡出和路径运动方向以及许多其他自定义选项的属性。

8.1 如何使用?

build.gradle 中添加如下依赖:

dependencies {
implementation "com.github.skydoves:transformationlayout:1.0.4"
}

然后,需要将我们需要添加过渡动画的View包裹到 TransformationLayout:






比如我们要将一个fab 过渡到一个card卡片,布局如下:







重点来了,绑定视图,将一个targetView绑定到TransformationLayout有2种方式:

  • 通过在xml中指定属性:
app:transformation_targetView="@+id/myCardView"
  • 在代码中绑定
transformationLayout.bindTargetView(myCardView)

当我们点击fab时,在监听器中调用startTransform()开始过渡动画,finishTransform()开始结束动画。

// start transformation when touching the fab.
fab.setOnClickListener {
transformationLayout.startTransform()
}

// finish transformation when touching the myCardView.
myCardView.setOnClickListener {
transformationLayout.finishTransform()
}

更多使用方式请看Github: https://github.com/skydoves/TransformationLayout

下载地址:TransformationLayout-main.zip

收起阅读 »

底部缩略库:RateBottomSheet

有时候,为了推广我们的应用,我们需要让用户跳转到应用商店为我们的APP打分,传统的对话框用户体验很不好,而本库则是用BottomSheet来进行提示,它位于底部缩略区域,用户体验很好。7.1 如何使用呢?在build.gradle 中添加如下依赖:depend...
继续阅读 »

有时候,为了推广我们的应用,我们需要让用户跳转到应用商店为我们的APP打分,传统的对话框用户体验很不好,而本库则是用BottomSheet来进行提示,它位于底部缩略区域,用户体验很好。

7.1 如何使用呢?

build.gradle 中添加如下依赖:

dependencies {
implementation 'com.mikhaellopez:ratebottomsheet:1.1.0'
}

然后修改默认的string资源文件来改变显示文案:


Like this App?
Do you like using this application?
Yes I do
Not really

Rate this app
Would you mind taking a moment to rate it? It won\'t take more than a minute. Thanks for your support!
Rate it now
Remind me later
No, thanks

代码中使用:

RateBottomSheetManager(this)
.setInstallDays(1) // 3 by default
.setLaunchTimes(2) // 5 by default
.setRemindInterval(1) // 2 by default
.setShowAskBottomSheet(false) // True by default
.setShowLaterButton(false) // True by default
.setShowCloseButtonIcon(false) // True by default
.monitor()

// Show bottom sheet if meets conditions
// With AppCompatActivity or Fragment
RateBottomSheet.showRateBottomSheetIfMeetsConditions(this)
7.2 效果图

更多详情请看Github:https://github.com/lopspower/RateBottomSheet

下载地址:RateBottomSheet-master.zip

收起阅读 »

带动画的底部导航栏库:AnimatedBottomBar

这是一个带动画的底部导航栏库。它使你可以以编程方式以及通过XML添加和删除选项卡。此外,我们可以轻松地从BottomBar拦截选项卡。限制访问应用程序导航中的高级区域时,“拦截”标签非常有用。流畅的动画提供了许多自定义选项,从动画插值器到设置波纹效果。6.1 ...
继续阅读 »

这是一个带动画的底部导航栏库。它使你可以以编程方式以及通过XML添加和删除选项卡。此外,我们可以轻松地从BottomBar拦截选项卡。限制访问应用程序导航中的高级区域时,“拦截”标签非常有用。流畅的动画提供了许多自定义选项,从动画插值器到设置波纹效果。

6.1 如何使用?

build.gradle 中添加如下依赖:

dependencies {
implementation 'nl.joery.animatedbottombar:library:1.0.8'
}

在xml文件中添加AnimatedBottomBar和自定义属性


res/menu目录下定义tabs.xml文件:







最后,代码中添加tab

// Creating a tab by passing values
val bottomBarTab1 = AnimatedBottomBar.createTab(drawable, "Tab 1")

// Creating a tab by passing resources
val bottomBarTab2 = AnimatedBottomBar.createTab(R.drawable.ic_home, R.string.tab_2, R.id.tab_home)
6.2 效果图
tab1tab2
tab1.giftab2.gif

详情信息请看Github: https://github.com/Droppers/AnimatedBottomBar

下载地址:AnimatedBottomBar-master.zip

收起阅读 »

Android 颜色库:ColorX

Android ColorX 以Kotlin 扩展函数的形式提供了一些重要的获取颜色的方法。通过提供不同颜色格式(RGB,HSV,CYMK等)的转换功能,它使开发变得更加轻松。该库的USP具有以下功能:颜色的不同阴影和色调。较深和较浅的阴影。颜色的补码5.1 ...
继续阅读 »

Android ColorX 以Kotlin 扩展函数的形式提供了一些重要的获取颜色的方法。
通过提供不同颜色格式(RGB,HSV,CYMK等)的转换功能,它使开发变得更加轻松。该库的USP具有以下功能:

  • 颜色的不同阴影和色调。
  • 较深和较浅的阴影。
  • 颜色的补码
5.1 如何使用?

build.gradle 中添加如下依赖:

dependencies {
implementation 'me.jorgecastillo:androidcolorx:0.2.0'
}

在代码中,一系列的转换方法:

val color = Color.parseColor("#e91e63")

val rgb = color.asRgb()
val argb = color.asArgb()
val hex = color.asHex()
val hsl = color.asHsl()
val hsla = color.asHsla()
val hsv = color.asHsv()
val cmyk = color.asCmyk()

val colorHsl = HSLColor(hue = 210f, saturation = 0.5f, lightness = 0.5f)

val colorInt = colorHsl.asColorInt()
val rgb = colorHsl.asRgb()
val argb = colorHsl.asArgb()
val hex = colorHsl.asHex()
val cmyk = colorHsl.asCmyk()
val hsla = colorHsl.asHsla()
val hsv = colorHsl.asHsv()
5.2 效果图

更多详细使用信息请看Github:https://github.com/JorgeCastilloPrz/AndroidColorX

下载地址:AndroidColorX-master.zip

收起阅读 »