注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

BaseUrlManager for Android 的设计初衷主要用于开发时,有多个环境需要打包APK的场景

BaseUrlManagerBaseUrlManager for Android 的设计初衷主要用于开发时,有多个环境需要打包APK的场景,通过BaseUrlManager提供的BaseUrl动态设置入口,只需打一 次包,即可轻松随意的切换不同的开发环境或测试...
继续阅读 »


BaseUrlManager

BaseUrlManager for Android 的设计初衷主要用于开发时,有多个环境需要打包APK的场景,通过BaseUrlManager提供的BaseUrl动态设置入口,只需打一 次包,即可轻松随意的切换不同的开发环境或测试环境。在打生产环境包时,关闭BaseUrl动态设置入口即可。

妈妈再也不用担心因环境不同需要打多个包的问题,从此告别环境不同要写一堆配置的烦恼,真香。

配合 RetrofitHelper 动态改变BaseUrl一起使用更香。

Gif 展示

Image

引入

Maven:

<dependency>
<groupId>com.king.base</groupId>
<artifactId>base-url-manager</artifactId>
<version>1.1.1</version>
<type>pom</type>
</dependency>

Gradle:


//AndroidX 版本
implementation 'com.king.base:base-url-manager:1.1.1'

//-----------------------v1.0.x以前的版本
//AndroidX 版本
implementation 'com.king.base:base-url-manager:1.0.1-androidx'

//Android Support 版本
implementation 'com.king.base:base-url-manager:1.0.1'

Lvy:

<dependency org='com.king.base' name='base-url-manager' rev='1.1.1'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现implementation失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来implementation)
allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

集成步骤代码示例 (示例出自于app中)

Step.1 在您项目中的AndroidManifest.xml中通过配置meta-data来自定义全局配置

    <!-- 在你项目中添加注册如下配置 -->
<activity android:name="com.king.base.baseurlmanager.BaseUrlManagerActivity"
android:screenOrientation="portrait"
android:theme="@style/BaseUrlManagerTheme"/>

Step.2 在您项目Application的onCreate方法中初始化BaseUrlManager

    //获取BaseUrlManager实例(适用于v1.1.x版本)
mBaseUrlManager = BaseUrlManager.getInstance();

//获取BaseUrlManager实例(适用于v1.0.x旧版本)
mBaseUrlManager = new BaseUrlManager(this);

//获取baseUrl
String baseUrl = mBaseUrlManager.getBaseUrl();

Step.3 提供动态配置BaseUrl的入口(通过Intent跳转到BaseUrlManagerActivity界面)

v.1.1.x 新版本写法

   BaseUrlManager.getInstance().startBaseUrlManager(this,SET_BASE_URL_REQUEST_CODE);

v1.0.x 以前版本写法

    Intent intent = new Intent(this, BaseUrlManagerActivity.class);
//BaseUrlManager界面的标题
//intent.putExtra(BaseUrlManagerActivity.KEY_TITLE,"BaseUrl配置");
//跳转到BaseUrlManagerActivity界面
startActivityForResult(intent,SET_BASE_URL_REQUEST_CODE);

Step.4 当配置改变了baseUrl时,在Activity或Fragment的onActivityResult方法中重新获取baseUrl即可


//方式1:通过BaseUrlManager获取baseUrl
String baseUrl = BaseUrlManager.getInstance().getBaseUrl();
//方式2:通过data直接获取baseUrl
UrlInfo urlInfo = BaseUrlManager.parseActivityResult(data);
String baseUrl = urlInfo.getBaseUrl();

更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档

BaseUrlManager.zip

收起阅读 »

KingPlayer 一个专注于 Android 视频播放器(IjkPlayer、ExoPlayer、VlcPlayer、SysPlayer)的基础库

KingPlayerKingPlayer 一个专注于 Android 视频播放器(IjkPlayer、ExoPlayer、VlcPlayer、SysPlayer)的基础库,无缝切换内核。功能说明 主要播放相关核心功能 播放器无缝切换&nbs...
继续阅读 »

KingPlayer

KingPlayer 一个专注于 Android 视频播放器(IjkPlayer、ExoPlayer、VlcPlayer、SysPlayer)的基础库,无缝切换内核。

功能说明

  •  主要播放相关核心功能
  •  播放器无缝切换
    •  MediaPlayer封装实现(SysPlayer)
    •  IjkPlayer封装实现
    •  ExoPlayer封装实现
    •  vlc-android封装实现
  •  控制图层相关
    •  待补充...

Gif 展示

Image

录制的gif效果有点不清晰,可以下载App查看详情。

引入

gradle:

使用 SysPlayer (Android自带的MediaPlayer)

//KingPlayer基础库,内置SysPlayer
implementation 'com.king.player:king-player:1.0.0-beta1'

使用 IjkPlayer

//KingPlayer基础库(必须)
implementation 'com.king.player:king-player:1.0.0-beta1'
//IjkPlayer
implementation 'com.king.player:ijk-player:1.0.0-beta1'

// 根据您的需求选择ijk模式的so
implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8'
// Other ABIs: optional
implementation 'tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8'

使用 ExoPlayer

//KingPlayer基础库(必须)
implementation 'com.king.player:king-player:1.0.0-beta1'
//ExoPlayer
implementation 'com.king.player:exo-player:1.0.0-beta1'

使用 VlcPlayer

//KingPlayer基础库(必须)
implementation 'com.king.player:king-player:1.0.0-beta1'
//VlcPlayer
implementation 'com.king.player:vlc-player:1.0.0-beta1'

示例

布局示例

    <com.king.player.kingplayer.view.VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

代码示例

        //初始化一个视频播放器(IjkPlayer、ExoPlayer、VlcPlayer、SysPlayer)
videoView.player = IjkPlayer(context)
//初始化数据源
val dataSource = DataSource(url)
videoView.setDataSource(dataSource)

videoView.setOnSurfaceListener(object : VideoView.OnSurfaceListener {
override fun onSurfaceCreated(surface: Surface, width: Int, height: Int) {
LogUtils.d("onSurfaceCreated: $width * $height")
videoView.start()
}

override fun onSurfaceSizeChanged(surface: Surface, width: Int, height: Int) {
LogUtils.d("onSurfaceSizeChanged: $width * $height")
}

override fun onSurfaceDestroyed(surface: Surface) {
LogUtils.d("onSurfaceDestroyed")
}

})

//缓冲更新监听
videoView.setOnBufferingUpdateListener {
LogUtils.d("buffering: $it")
}
//播放事件监听
videoView.setOnPlayerEventListener { event, bundle ->

}
//错误事件监听
videoView.setOnErrorListener { event, bundle ->

}


        
//------------ 控制相关
//开始
videoView.start()
//暂停
videoView.pause()
//进度调整到指定位置
videoView.seekTo(pos)
//停止
videoView.stop()
//释放
videoView.release()
//重置
videoView.reset()

更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档

其他

需使用JDK8+编译,在你项目中的build.gradle的android{}中添加配置:

compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}

代码下载:KingPlayer.zip

收起阅读 »

KingKeyboard for Android 是一个自定义键盘

KingKeyboardKingKeyboard for Android 是一个自定义键盘。内置了满足各种场景的键盘需求:包括但不限于混合、字母、数字、电话、车牌号等可输入场景。还支持自定义。集成简单,键盘可定制化。引入Maven:<dependency...
继续阅读 »


KingKeyboard

KingKeyboard for Android 是一个自定义键盘。内置了满足各种场景的键盘需求:包括但不限于混合、字母、数字、电话、车牌号等可输入场景。还支持自定义。集成简单,键盘可定制化。


引入

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中的源码使用示例

代码下载:KeyboardVisibilityEvent.zip

收起阅读 »

WordPOI是一个将Word接口文档转换成JavaBean的工具库

WordPOIWordPOI是一个将Word接口文档转换成JavaBean的工具库,主要目的是减少部分无脑的开发工作。核心功能:将文档中表格定义的实体转换成Java实体对象WordPOI特性说明支持解析doc格式和docx格式的Word文档支持批量解析Word...
继续阅读 »


WordPOI


WordPOI是一个将Word接口文档转换成JavaBean的工具库,主要目的是减少部分无脑的开发工作。

核心功能:将文档中表格定义的实体转换成Java实体对象

WordPOI特性说明

  1. 支持解析doc格式和docx格式的Word文档
  2. 支持批量解析Word文档并转换成实体
  3. 解析配置支持自定义,详情请查看{@link ParseConfig}相关配置
  4. 虽然解析可配置,但因文档内容的不可控,解析转换也具有一定的局限性

只要在文档上定义实体对象时,尽量满足示例文档的规则,就可以规避解析转换时的局限性。

ParseConfig属性说明

属性值类型默认值说明
startTableint0开始表格
startRowint1开始行
startColumnint0开始列
fieldNameColumnint0字段名称所在列
fieldTypeColumnint1字段类型所在列
fieldDescColumnint2字段注释说明所在列
charsetNameStringUTF-8字符集编码
genGetterAndSetterbooleantrue是否生成get和set方法
genToStringbooleantrue是否生成toString方法
useLombokbooleanfalse是否使用Lombok
parseEntityNamebooleanfalse是否解析实体名称
entityNameRowint0实体名称所在行
entityNameColumnint0实体名称所在列
serializablebooleanfalse是否实现Serializable序列化
showHeaderbooleantrue是否显示头注释
headerStringCreated by WordPOI头注释内容
transformationsMap<String,String>需要转型的集合(自定义转型配置)

引入

Maven:

<dependency>
<groupId>com.king.poi</groupId>
<artifactId>word-poi</artifactId>
<version>1.0.1</version>
<type>pom</type>
</dependency>

Gradle:

compile 'com.king.poi:word-poi:1.0.1'

Lvy:

<dependency org='com.king.poi' name='word-poi' rev='1.0.1'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

引入的库:

compile 'org.apache.poi:poi:4.1.0'
compile 'org.apache.poi:poi-ooxml:4.1.0'
compile 'org.apache.poi:poi-scratchpad:4.1.0'

如想直接引入jar包可直接点击左上角的Download下载最新的jar,然后引入到你的工程即可。

示例

代码示例 (直接在main方法中调用即可)

        try {

/**
* 解析文档中的表格实体,表格包含了实体名称,只需配置 {@link ParseConfig#parseEntityName} 为 true 和相关对应行,即可开启自动解析实体名称,自动解析实体名称
* {@link ParseConfig}中包含解析时需要的各种配置,方便灵活的支持文档中更多的表格样式
*/
ParseConfig config = new ParseConfig.Builder().startRow(2).parseEntityName(true).build();
WordPOI.wordToEntity(Test.class.getResourceAsStream("Api3.docx"),false,"C:/bean/","com.king.poi.bean",config);
//解析文档docx格式 需要传生成的对象实体名称
// WordPOI.wordToEntity(Test.class.getResourceAsStream("Api1.docx"),false,"C:/bean/","com.king.poi.bean","Result","PageInfo");
//解析文档docx格式 需要传生成的对象实体名称
// WordPOI.wordToEntity(Test.class.getResourceAsStream("Api2.doc"),true,"C:/bean/","com.king.poi.bean","TestBean");
} catch (Exception e) {
e.printStackTrace();
}
  • 文档实体示例一(默认格式,见文档 Api1.docx)

1.1. Result (响应结果实体)

字段字段类型说明
codeString0-代表成功,其它代表失败
descString操作失败时的说明信息
dataT返回对应的泛型实体对象

1.2. PageInfo (页码信息实体)

字段字段类型说明
curPageInteger当前页码
pageSizeInteger页码大小,每一页的记录条数
totalPageInteger总页数
hasNextBoolean是否有下一页
dataList<T>泛型T为对应的数据记录实体
  • 文档实体示例二(自动解析实体名称格式,见文档 Api3.docx)

1.1. 响应结果实体

Result
字段字段类型说明
codeString0-代表成功,其它代表失败
descString操作失败时的说明信息
dataT返回对应的泛型<T>实体对象

1.2. 页码信息实体

PageInfo
字段字段类型说明
curPageInteger当前页码
curPageInteger当前页码
pageSizeInteger页码大小,每一页的记录条数
totalPageInteger总页数
hasNextBoolean是否有下一页
dataList<T>泛型T为对应的数据记录实体

更多使用详情,请查看Test中的源码使用示例或直接查看API帮助文档

代码下载:WordPOI.zip

收起阅读 »

Gradle实战2:微信Tinker项目中的tinker-patch-gradle-plugin模块解析

引言上一篇,我们学习了《微信Tinker项目中的maven-publish封装》,了解到了在一个成熟项目中,maven相关gradle的通用封装,进而巩固前面学习的gradle相关理论知识接下来,我们将对Tinker项目中的tinker-patch-gradl...
继续阅读 »

引言

上一篇,我们学习了《微信Tinker项目中的maven-publish封装》,了解到了在一个成熟项目中,maven相关gradle的通用封装,进而巩固前面学习的gradle相关理论知识

接下来,我们将对Tinker项目中的tinker-patch-gradle-plugin模块进行解析,进一步感受Gradle在亿级应用中散发的魅力

PS1:本章主要是跟踪《tinker-patch-gradle-plugin模块》实现,来巩固gradle相关知识,具体热修相关安卓知识的话不会展开

PS2:由于tinker的官方工程比较大,对于巩固gradle知识干扰比较大,所以本章的代码工程是阉割了官方的代码展开,更加聚焦,同时实现博客和源码配套的模式

简介

tinker-patch-gradle-plugin模块,是开发者使用Thinker入口;

如果我们app集成thinker,其实就是对这个模块的使用,因为thinker的实现

都以插件的方式被封装到了这个模块,具体官方代码位置戳这里>>>

上面说到,我们会对官方工程进行裁剪,裁剪后对应的模块位置戳这里>>>

解析过程

应用模块

1.png 此图为我们的app模块引入thinker的步骤

1)模块所在位置,其实就是我们app模块的gradle文件

2)插件引入,通过classpath关键字引入封装好的thinker插件,其中插件的maven发布我们发布到了本地,所以用的时候我们maven指向了本地的 ‘../repo’

3)插件的使用,通过apply引入,然后tinkerPatch,buildConfig都是插件的自定义拓展,具体实现在下面步骤讲解

模块之工程定义

2.png

1)插件实现工程目录,可以看出这是一个gradle插件的标准目录,具体诠释见往期教程

2)插件实现工程gradle文件,这里除了有自定义插件的依赖外,还用到了上一章讲解的maven-publish封装

模块之自定义拓展

上面有提到tinkerPatch,buildConfig关键字为自定义拓展,这两个关键字只是thinker

中的拓展之一,我们来看下thinker的自定义拓展全貌:

 tinkerPatch {
/**
* necessary,default 'null'
* the old apk path, use to diff with the new apk to build
* add apk from the build/bakApk
*/

oldApk = getOldApkPath()
/**
* optional,default 'false'
* there are some cases we may get some warnings
* if ignoreWarning is true, we would just assert the patch process
* case 1: minSdkVersion is below 14, but you are using dexMode with raw.
* it must be crash when load.
* case 2: newly added Android Component in AndroidManifest.xml,
* it must be crash when load.
* case 3: loader classes in dex.loader{} are not keep in the main dex,
* it must be let tinker not work.
* case 4: loader classes in dex.loader{} changes,
* loader classes is ues to load patch dex. it is useless to change them.
* it won't crash, but these changes can't effect. you may ignore it
* case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
*/

ignoreWarning = false

/**
* optional,default 'true'
* whether sign the patch file
* if not, you must do yourself. otherwise it can't check success during the patch loading
* we will use the sign config with your build type
*/

useSign = true

/**
* optional,default 'true'
* whether use tinker to build
*/

tinkerEnable = buildWithTinker()

/**
* Warning, applyMapping will affect the normal android build!
*/

buildConfig {
/**
* optional,default 'null'
* if we use tinkerPatch to build the patch apk, you'd better to apply the old
* apk mapping file if minifyEnabled is enable!
* Warning:
* you must be careful that it will affect the normal assemble build!
*/

applyMapping = getApplyMappingPath()
/**
* optional,default 'null'
* It is nice to keep the resource id from R.txt file to reduce java changes
*/

applyResourceMapping = getApplyResourceMappingPath()

/**
* necessary,default 'null'
* because we don't want to check the base apk with md5 in the runtime(it is slow)
* tinkerId is use to identify the unique base apk when the patch is tried to apply.
* we can use git rev, svn rev or simply versionCode.
* we will gen the tinkerId in your manifest automatic
*/

tinkerId = getTinkerIdValue()

/**
* if keepDexApply is true, class in which dex refer to the old apk.
* open this can reduce the dex diff file size.
*/

keepDexApply = false

/**
* optional, default 'false'
* Whether tinker should treat the base apk as the one being protected by app
* protection tools.
* If this attribute is true, the generated patch package will contain a
* dex including all changed classes instead of any dexdiff patch-info files.
*/

isProtectedApp = false

/**
* optional, default 'false'
* Whether tinker should support component hotplug (add new component dynamically).
* If this attribute is true, the component added in new apk will be available after
* patch is successfully loaded. Otherwise an error would be announced when generating patch
* on compile-time.
*
* <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
*/

supportHotplugComponent = false
}

dex {
/**
* optional,default 'jar'
* only can be 'raw' or 'jar'. for raw, we would keep its original format
* for jar, we would repack dexes with zip format.
* if you want to support below 14, you must use jar
* or you want to save rom or check quicker, you can use raw mode also
*/

dexMode = "jar"

/**
* necessary,default '[]'
* what dexes in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
*/

pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
* necessary,default '[]'
* Warning, it is very very important, loader classes can't change with patch.
* thus, they will be removed from patch dexes.
* you must put the following class into main dex.
* Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
* own tinkerLoader, and the classes you use in them
*
*/

loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
"tinker.sample.android.app.BaseBuildInfo"
]
}

lib {
/**
* optional,default '[]'
* what library in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
* for library in assets, we would just recover them in the patch directory
* you can get them in TinkerLoadResult with Tinker
*/

pattern = ["lib/*/*.so"]
}

res {
/**
* optional,default '[]'
* what resource in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
* you must include all your resources in apk here,
* otherwise, they won't repack in the new apk resources.
*/

pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

/**
* optional,default '[]'
* the resource file exclude patterns, ignore add, delete or modify resource change
* it support * or ? pattern.
* Warning, we can only use for files no relative with resources.arsc
*/

ignoreChange = ["assets/sample_meta.txt"]

/**
* default 100kb
* for modify resource, if it is larger than 'largeModSize'
* we would like to use bsdiff algorithm to reduce patch file size
*/

largeModSize = 100
}

packageConfig {
/**
* optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
* package meta file gen. path is assets/package_meta.txt in patch file
* you can use securityCheck.getPackageProperties() in your ownPackageCheck method
* or TinkerLoadResult.getPackageConfigByName
* we will get the TINKER_ID from the old apk manifest for you automatic,
* other config files (such as patchMessage below)is not necessary
*/

configField("patchMessage", "tinker is sample to use")
/**
* just a sample case, you can use such as sdkVersion, brand, channel...
* you can parse it in the SamplePatchListener.
* Then you can use patch conditional!
*/

configField("platform", "all")
/**
* patch version via packageConfig
*/

configField("patchVersion", "1.0")
}
//or you can add config filed outside, or get meta value from old apk
//project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
//project.tinkerPatch.packageConfig.configField("test2", "sample")

/**
* if you don't use zipArtifact or path, we just use 7za to try
*/

sevenZip {
/**
* optional,default '7za'
* the 7zip artifact path, it will use the right 7za with your platform
*/

zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/**
* optional,default '7za'
* you can specify the 7za path yourself, it will overwrite the zipArtifact value
*/

// path = "/usr/local/bin/7za"
}
}
复制代码

精简后大致结构如下:

 tinkerPatch {
oldApk = getOldApkPath()
buildConfig {
supportHotplugComponent = false
}
dex {
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
}
lib {
pattern = ["lib/*/*.so"]
}
res {
largeModSize = 100
}
packageConfig {
configField("patchVersion", "1.0")
}
sevenZip {
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
}
}
复制代码

这里具体的配置是什么意思,先不用关注,因为配置的意思涉及到thinker本身业务和一些安卓热修相关知识点,我们主要关注下如果我们要实现这样的结构拓展,应该怎么做?下面我们来看看thinker是怎么做的:

3.png

1)插件入口,gradle插件的代码类入口

2)和 3),拓展调用和定义,在定义中可以看到通过‘project.extensions.create’ 来创建自定义拓展,层级嵌套拓展通过追加方式,如:先自定义tinkerPatch,然后在tinkerPatch中嵌套buildConfig

其中,细心的朋友会看到,为什么有些create是2个参数,有些是3个参数

4.png

根据官方定义可以看出,前面两个是定义拓展的key和value,然后后面的可变参数是 要传给value拓展的参数,举个例子:

project.tinkerPatch.extensions.create('buildConfig', TinkerBuildConfigExtension, project)
复制代码

key为‘buildConfig’,value为‘TinkerBuildConfigExtension’

然后project则是传递参数给到了‘TinkerBuildConfigExtension’,具体见TinkerBuildConfigExtension的定义如下:

6.png

其他拓展以此类推,这里不一一展开

模块之配置android属性

7.png

1)preDexLibraries 设置为false

默认情况下,preDexLibraries是为true的,作用主要是用来确认是否对Lib做preDexing操作,操作了的话带来的好处提高增量构建的速度;

这里设置为false,猜测是thinker涉及了多dex,为避免和库工程冲突

另外preDexLibraries是dexOptions属性之一,dexoptions是一个gradle对象,这个对象用来设置从java代码向.dex文件转化的过程中的一些配置选项;

更多dexOptions属性可以戳这里>>>

2)jumboMode 设置为true

jumboMode设置为true,意识是忽略方法数限制的检查

这样做的缺点是apk无法再低版本的设备上面安装,会出现错误:INSTALL_FAILED_DEXOPT

具体细节戳这里>>>

3)关闭 ENABLE_DEX_ARCHIVE

void disableArchiveDex(Project project) {
println 'disableArchiveDex -->'
try {
def booleanOptClazz = Class.forName('com.android.build.gradle.options.BooleanOption')
def enableDexArchiveField = booleanOptClazz.getDeclaredField('ENABLE_DEX_ARCHIVE')
enableDexArchiveField.setAccessible(true)
def enableDexArchiveEnumObj = enableDexArchiveField.get(null)
def defValField = enableDexArchiveEnumObj.getClass().getDeclaredField('defaultValue')
defValField.setAccessible(true)
defValField.set(enableDexArchiveEnumObj, false)
} catch (Throwable thr) {
// To some extends, class not found means we are in lower version of android gradle
// plugin, so just ignore that exception.
if (!(thr instanceof ClassNotFoundException)) {
project.logger.error("reflectDexArchiveFlag error: ${thr.getMessage()}.")
}
}
}
复制代码

ENABLE_DEX_ARCHIVE 这个功能主要是减少dex的大小

这里关闭主要避免破坏multidex的maindex规则,进而实现多dex的场景

4)keepRuntimeAnnotatedClasses 设置为 false

keepRuntimeAnnotatedClasses 主要作用是带有运行时注解的类,保留在主dex中

thinker关闭,主要降低主dex大小,兼容5.0以下的情况

模块之aapt2和资源固定相关

1.png 2.png

该部分功能实现:使用导出的符号表进行资源id的固定

为什么要进行资源ID的固定?具体戳这里>>,背景细节不在这展开

其中,这里实现逻辑为:

1)判断当前《Android Gradle Plugin》是否启动aapt2,如果没有启动跳过,如果启动了则进行aapt2的资源固定适配

2)aapt2的资源固定适配操作,通过指定稳定的资源id映射文件,同时结合“--stable-ids”命令进行固定

代码下载:DaviGradlePlu-main.zip

收起阅读 »

Android 第三方RoundedImageView设置各种圆形、方形头像

Android 自定义CoolImageView实现QQ首页背景图片动画效果一.第三方RoundedImageView1.在Android Studio中,可进入模块设置中添加库依赖。 进入Module结构设置添加库依赖 ,输入Rounde...
继续阅读 »

Android 自定义CoolImageView实现QQ首页背景图片动画效果




一.第三方RoundedImageView

1.在Android Studio中,可进入模块设置中添加库依赖。 进入Module结构设置添加库依赖 ,输入RoundedImageView然后搜索添加

2.在Moudle的build.gradle中添加如下代码,添加完之后在Build中进行下Make Module操作(编译下Module),使自己添加的依赖生效

repositories {

mavenCentral()

}

dependencies {

compile 'com.makeramen:roundedimageview:2.2.1'

}

3.添加相关属性:

控件属性: 

riv_border_width: 边框宽度

riv_border_color: 边框颜色

riv_oval: 是否圆形

riv_corner_radius: 圆角弧度

riv_corner_radius_top_left:左上角弧度

riv_corner_radius_top_right: 右上角弧度

riv_corner_radius_bottom_left:左下角弧度

riv_corner_radius_bottom_right:右下角弧度

4.示例布局:

 <com.makeramen.roundedimageview.RoundedImageView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:src="@mipmap/avatar"

app:riv_border_color="#333333"

app:riv_border_width="2dp"

app:riv_oval="true" />

<com.makeramen.roundedimageview.RoundedImageView

xmlns:app="http://schemas.android.com/apk/res-auto"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:scaleType="fitCenter"

android:src="@mipmap/avatar"

app:riv_border_color="#333333"

app:riv_border_width="2dp"

app:riv_corner_radius="10dp"

app:riv_mutate_background="true"

app:riv_oval="false"

app:riv_tile_mode="repeat" />

<com.makeramen.roundedimageview.RoundedImageView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:scaleType="fitCenter"

android:src="@mipmap/avatar"

app:riv_border_color="#333333"

app:riv_border_width="2dp"

app:riv_corner_radius_top_left="25dp"

app:riv_corner_radius_bottom_right="25dp"

app:riv_mutate_background="true"

app:riv_oval="false"

app:riv_tile_mode="repeat" />

<com.makeramen.roundedimageview.RoundedImageView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:scaleType="fitCenter"

android:src="@mipmap/avatar"

app:riv_border_color="#333333"

app:riv_border_width="2dp"

app:riv_corner_radius_top_right="25dp"

app:riv_corner_radius_bottom_left="25dp"

app:riv_mutate_background="true"

app:riv_oval="false"

app:riv_tile_mode="repeat" />

<com.makeramen.roundedimageview.RoundedImageView

android:layout_width="96dp"

android:layout_height="72dp"

android:scaleType="center"

android:src="@mipmap/avatar"

app:riv_border_color="#333333"

app:riv_border_width="2dp"

app:riv_corner_radius="25dp"

app:riv_mutate_background="true"

app:riv_oval="true"

app:riv_tile_mode="repeat" />

 <com.makeramen.roundedimageview.RoundedImageView

android:id="@+id/imCompanyHeadItem"

android:layout_width="50dp"

android:layout_marginTop="10dp"

android:layout_marginRight="6.5dp"

android:layout_marginLeft="6.5dp"

android:src="@drawable/head_home"

android:layout_gravity="center"

android:layout_height="50dp"

app:riv_border_color="@color/_c7ced8"

app:riv_border_width="1dp"

app:riv_corner_radius_top_left="5dp"

app:riv_corner_radius_bottom_right="5dp"

app:riv_corner_radius_bottom_left="5dp"

app:riv_corner_radius_top_right="5dp"

app:riv_mutate_background="true"

app:riv_oval="false"

app:riv_tile_mode="repeat"/>

二.自定义RoundImageView

1.布局:

 <com.iruiyou.pet.utils.RoundImageView

android:id="@+id/headIv"

android:layout_width="125dp"

android:layout_height="125dp"

android:layout_marginTop="92dp"

android:src="@drawable/head_home"

loonggg:border_incolor="#000fff"

loonggg:border_outcolor="#fff000"

loonggg:border_width="10dp"

app:layout_constraintLeft_toLeftOf="parent"

app:layout_constraintRight_toRightOf="parent"

app:layout_constraintTop_toTopOf="parent"/>

2.自定义类:

import android.content.Context;

import android.content.res.TypedArray;

import android.graphics.Bitmap;

import android.graphics.Canvas;

import android.graphics.Paint;

import android.graphics.PorterDuff;

import android.graphics.PorterDuffXfermode;

import android.graphics.Rect;

import android.graphics.drawable.BitmapDrawable;

import android.graphics.drawable.Drawable;

import android.graphics.drawable.NinePatchDrawable;

import android.util.AttributeSet;

import android.widget.ImageView;

import com.iruiyou.pet.R;



/**

*


* @author sgf


* 自定义圆形头像


*


*/


public class RoundImageView extends ImageView {

private int mBorderThickness = 0;

private Context mContext;

private int defaultColor = 0xFFFFFFFF;

// 外圆边框颜色

private int mBorderOutsideColor = 0;

// 内圆边框颜色

private int mBorderInsideColor = 0;

// RoundImageView控件默认的长、宽

private int defaultWidth = 0;

private int defaultHeight = 0;



public RoundImageView(Context context) {

super(context);

mContext = context;

}



public RoundImageView(Context context, AttributeSet attrs) {

super(context, attrs);

mContext = context;

// 设置RoundImageView的属性值,比如颜色,宽度等

setRoundImageViewAttributes(attrs);

}



public RoundImageView(Context context, AttributeSet attrs, int defStyle) {

super(context, attrs, defStyle);

mContext = context;

setRoundImageViewAttributes(attrs);

}



// 从attr.xml文件中获取属性值,并给RoundImageView设置

private void setRoundImageViewAttributes(AttributeSet attrs) {

TypedArray a = mContext.obtainStyledAttributes(attrs,

R.styleable.round_image_view);

mBorderThickness = a.getDimensionPixelSize(

R.styleable.round_image_view_border_width, 0);

mBorderOutsideColor = a.getColor(

R.styleable.round_image_view_border_outcolor, defaultColor);

mBorderInsideColor = a.getColor(

R.styleable.round_image_view_border_incolor, defaultColor);

a.recycle();

}



// 具体解释:比如我自定义一个控件,怎么实现呢,以RoundImageView为例,首先是继承ImageView,然后实现其构造函数,在构造函数中,获取attr中的属性值(再次解释:这里获取的具体的这个属性的值是怎么来的呢?比如颜色和宽度,这个在attr.xml中定义了相关的名字,而在使用RoundImageView的xml布局文件中,我们会设置其值,这里需要用的值,就是从那里设置的),并设置在本控件中,然后继承onDraw方法,画出自己想要的图形或者形状即可

/**

* 这个是继承的父类的onDraw方法


*


* onDraw和下面的方法不用管,基本和学习自定义没关系,就是实现怎么画圆的,你可以改变下面代码试着画三角形头像,哈哈


*/


@Override

protected void onDraw(Canvas canvas) {

Drawable drawable = getDrawable();

if (drawable == null) {

return;

}

if (getWidth() == 0 || getHeight() == 0) {

return;

}

this.measure(0, 0);

if (drawable.getClass() == NinePatchDrawable.class)

return;

Bitmap b = ((BitmapDrawable) drawable).getBitmap();

Bitmap bitmap = b.copy(Bitmap.Config.ARGB_8888, true);

if (defaultWidth == 0) {

defaultWidth = getWidth();

}

if (defaultHeight == 0) {

defaultHeight = getHeight();

}

int radius = 0;

// 这里的判断是如果内圆和外圆设置的颜色值不为空且不是默认颜色,就定义画两个圆框,分别为内圆和外圆边框

if (mBorderInsideColor != defaultColor

&& mBorderOutsideColor != defaultColor) {

radius = (defaultWidth < defaultHeight ? defaultWidth

: defaultHeight) / 2 - 2 * mBorderThickness;

// 画内圆

drawCircleBorder(canvas, radius + mBorderThickness / 2,

mBorderInsideColor);

// 画外圆

drawCircleBorder(canvas, radius + mBorderThickness

+ mBorderThickness / 2, mBorderOutsideColor);

} else if (mBorderInsideColor != defaultColor

&& mBorderOutsideColor == defaultColor) {// 这里的是如果内圆边框不为空且颜色值不是默认值,就画一个内圆的边框

radius = (defaultWidth < defaultHeight ? defaultWidth

: defaultHeight) / 2 - mBorderThickness;

drawCircleBorder(canvas, radius + mBorderThickness / 2,

mBorderInsideColor);

} else if (mBorderInsideColor == defaultColor

&& mBorderOutsideColor != defaultColor) {// 这里的是如果外圆边框不为空且颜色值不是默认值,就画一个外圆的边框

radius = (defaultWidth < defaultHeight ? defaultWidth

: defaultHeight) / 2 - mBorderThickness;

drawCircleBorder(canvas, radius + mBorderThickness / 2,

mBorderOutsideColor);

} else {// 这种情况是没有设置属性颜色的情况下,即没有边框的情况

radius = (defaultWidth < defaultHeight ? defaultWidth

: defaultHeight) / 2;

}

Bitmap roundBitmap = getCroppedRoundBitmap(bitmap, radius);

canvas.drawBitmap(roundBitmap, defaultWidth / 2 - radius, defaultHeight

/ 2 - radius, null);

}



/**

* 获取裁剪后的圆形图片


*


* @param bmp


* @param radius


* 半径


* @return


*/


public Bitmap getCroppedRoundBitmap(Bitmap bmp, int radius) {

Bitmap scaledSrcBmp;

int diameter = radius * 2;

// 为了防止宽高不相等,造成圆形图片变形,因此截取长方形中处于中间位置最大的正方形图片

int bmpWidth = bmp.getWidth();

int bmpHeight = bmp.getHeight();

int squareWidth = 0, squareHeight = 0;

int x = 0, y = 0;

Bitmap squareBitmap;

if (bmpHeight > bmpWidth) {// 高大于宽

squareWidth = squareHeight = bmpWidth;

x = 0;

y = (bmpHeight - bmpWidth) / 2;

// 截取正方形图片

squareBitmap = Bitmap.createBitmap(bmp, x, y, squareWidth,

squareHeight);

} else if (bmpHeight < bmpWidth) {// 宽大于高

squareWidth = squareHeight = bmpHeight;

x = (bmpWidth - bmpHeight) / 2;

y = 0;

squareBitmap = Bitmap.createBitmap(bmp, x, y, squareWidth,

squareHeight);

} else {

squareBitmap = bmp;

}

if (squareBitmap.getWidth() != diameter

|| squareBitmap.getHeight() != diameter) {

scaledSrcBmp = Bitmap.createScaledBitmap(squareBitmap, diameter,

diameter, true);

} else {

scaledSrcBmp = squareBitmap;

}

Bitmap output = Bitmap.createBitmap(scaledSrcBmp.getWidth(),

scaledSrcBmp.getHeight(), Bitmap.Config.ARGB_8888);

Canvas canvas = new Canvas(output);



Paint paint = new Paint();

Rect rect = new Rect(0, 0, scaledSrcBmp.getWidth(),

scaledSrcBmp.getHeight());



paint.setAntiAlias(true);

paint.setFilterBitmap(true);

paint.setDither(true);

canvas.drawARGB(0, 0, 0, 0);

canvas.drawCircle(scaledSrcBmp.getWidth() / 2,

scaledSrcBmp.getHeight() / 2, scaledSrcBmp.getWidth() / 2,

paint);

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

canvas.drawBitmap(scaledSrcBmp, rect, rect, paint);

bmp = null;

squareBitmap = null;

scaledSrcBmp = null;

return output;

}



/**

* 画边缘的圆,即内圆或者外圆


*/


private void drawCircleBorder(Canvas canvas, int radius, int color) {

Paint paint = new Paint();

/* 去锯齿 */

paint.setAntiAlias(true);

paint.setFilterBitmap(true);

paint.setDither(true);

paint.setColor(color);

/* 设置paint的 style 为STROKE:空心 */

paint.setStyle(Paint.Style.STROKE);

/* 设置paint的外框宽度 */

paint.setStrokeWidth(mBorderThickness);

canvas.drawCircle(defaultWidth / 2, defaultHeight / 2, radius, paint);

}

}

3.res--values--attrs.xml文件


<?xml version="1.0" encoding="utf-8"?>

<resources>



<declare-styleable name="round_image_view">

<attr name="border_width" format="dimension" />

<attr name="border_incolor" format="color" />

<attr name="border_outcolor" format="color"></attr>

</declare-styleable>



</resources>

三.第三方NiceImageView

1.效果图如下:



 2.特点:

基于AppCompatImageView扩展

支持圆角、圆形显示

可绘制边框,圆形时可绘制内外两层边框

支持边框不覆盖图片

可绘制遮罩

3.基本用法:

 1. 添加JitPack仓库 在项目根目录下的 build.gradle 中添加仓库:

allprojects {

repositories {

...

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

}

}

2. 添加项目依赖


dependencies {

implementation 'com.github.SheHuan:NiceImageView:1.0.5'

}

3. 在布局文件中添加CornerLabelView

<com.shehuan.niv.NiceImageView

android:layout_width="200dp"

android:layout_height="200dp"

android:layout_marginTop="10dp"

android:src="@drawable/cat"

app:border_color="#FF7F24"

app:border_width="4dp"

app:is_circle="true" />

4.支持的属性、方法

属性名含义默认值对应方法
is_circle是否显示为圆形(默认为矩形)falseisCircle()
corner_top_left_radius左上角圆角半径0dpsetCornerTopLeftRadius()
corner_top_right_radius右上角圆角半径0dpsetCornerTopRightRadius()
corner_bottom_left_radius左下角圆角半径0dpsetCornerBottomLeftRadius()
corner_bottom_right_radius右下角圆角半径0dpsetCornerBottomRightRadius()
corner_radius统一设置四个角的圆角半径0dpsetCornerRadius()
border_width边框宽度0dpsetBorderWidth()
border_color边框颜色#ffffffsetBorderColor()
inner_border_width相当于内层边框(is_circle为true时支持)0dpsetInnerBorderWidth()
inner_border_color内边框颜色#ffffffsetInnerBorderColor()
is_cover_srcborder、inner_border是否覆盖图片内容falseisCoverSrc()
mask_color图片上绘制的遮罩颜色不设置颜色则不绘制setMaskColor()

可参考:https://github.com/SheHuan/NiceImageView


 5.其它:


如果你需要实现类似钉钉的圆形组合头像,例如:



可以先生成对应的Bitmap,并用圆形的 NiceImageView 显示即可。如何生成组合Bitmap可以参考这里:CombineBitmap


四.如果你的项目中只有圆形的图片而不需要设置圆角图片的话,可以试试下面的第三方:


https://github.com/hdodenhof/CircleImageView

https://github.com/open-android/RoundedImageView


收起阅读 »

Android之CircleImageView使用

文章大纲一、什么是CircleImageView二、代码实战三、项目源码下载一、什么是CircleImageView  圆角 ImageView,在我们的 App 中这个想必是太常见了,也许我们可以有无数种展示圆角图片的方法,但是 CircleImageVie...
继续阅读 »

文章大纲

一、什么是CircleImageView
二、代码实战
三、项目源码下载

一、什么是CircleImageView

  圆角 ImageView,在我们的 App 中这个想必是太常见了,也许我们可以有无数种展示圆角图片的方法,但是 CircleImageView 绝对是我们在开发时需要优先考虑的,如果你还不知道 CircleImageView,那么你需要赶快去体验它在处理圆角图片时的强大了,相信你肯定会觉得和 CircleImageView 相见恨晚。

二、代码实战

1. 添加依赖

    //添加CircleImageView依赖
implementation 'de.hdodenhof:circleimageview:2.1.0'

2. 添加图片资源

 

3. 资源文件activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<de.hdodenhof.circleimageview.CircleImageView
xmlns:circleimageview="http://schemas.android.com/apk/res-auto"
android:id="@+id/imageview"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:src="@drawable/test"
circleimageview:civ_border_color="@android:color/holo_red_light"
circleimageview:civ_border_overlay="false"
circleimageview:civ_border_width="2dp"
circleimageview:civ_fill_color="@android:color/holo_blue_light"/>

</android.support.constraint.ConstraintLayout>

常用属性:
(1)civ_border_width: 设置边框的宽度,默认为0,即无边框。
(2)civ_border_color: 设置边框的颜色,默认为黑色。
(3)civ_border_overlay:设置边框是否覆盖在图片上,默认为false,即边框在图片外圈。
(4)civ_fill_color:设置图片的底色,默认透明。
(5)civ_border_width:设置边框大小
(6)civ_fill_color:设置图片的底色,默认透明

4. MainActivity.java

public class MainActivity extends AppCompatActivity {

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

5. 运行结果

 
demo下载地址:CircleImageViewTest.zip
收起阅读 »

移动端强大的富文本编辑器richeditor-android

通常我们使用富文本编辑器都是在H5端实现,但是如果你遇到在移动端发表文章的功能,那么richeditor-android这套框架可以轻松为你实现,不需要再使用大量的控件进行拼凑! 功能表如下图所示: 引入richeditor-android ...
继续阅读 »


通常我们使用富文本编辑器都是在H5端实现,但是如果你遇到在移动端发表文章的功能,那么richeditor-android这套框架可以轻松为你实现,不需要再使用大量的控件进行拼凑!



  • 功能表如下图所示:





  • 引入richeditor-android



richeditor-android需要的jar:

implementation 'jp.wasabeef:richeditor-android:1.2.2'


这是一个Dialog框架,demo中不想自己去写,所以就使用了第三方
implementation 'com.afollestad.material-dialogs:core:0.9.6.0'


  • 引入控件RichEditor


   <jp.wasabeef.richeditor.RichEditor
android:id="@+id/editor"
android:layout_width="match_parent"
android:layout_height="wrap_content" />


  • 使用到的权限


如果拍照需要相机权限,选择图片需要SD卡权限,插入网络图片需要网络权限


 <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />


  • 初始化RichEditor


       mEditor = (RichEditor) findViewById(R.id.editor);

//初始化编辑高度
mEditor.setEditorHeight(200);
//初始化字体大小
mEditor.setEditorFontSize(22);
//初始化字体颜色
mEditor.setEditorFontColor(Color.BLACK);
//mEditor.setEditorBackgroundColor(Color.BLUE);

//初始化内边距
mEditor.setPadding(10, 10, 10, 10);
//设置编辑框背景,可以是网络图片
// mEditor.setBackground("https://raw.githubusercontent.com/wasabeef/art/master/chip.jpg");
// mEditor.setBackgroundColor(Color.BLUE);
mEditor.setBackgroundResource(R.drawable.bg);
//设置默认显示语句
mEditor.setPlaceholder("Insert text here...");
//设置编辑器是否可用
mEditor.setInputEnabled(true);


  • 实时监听Editor输入内容


   mPreview = (TextView) findViewById(R.id.preview);
mEditor.setOnTextChangeListener(new RichEditor.OnTextChangeListener() {
@Override
public void onTextChange(String text) {
mPreview.setText(text);
}
});


  • 功能方法


        /**
* 撤销当前标签状态下所有内容
*/

findViewById(R.id.action_undo).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.undo();
}
});
/**
* 恢复撤销的内容
*/

findViewById(R.id.action_redo).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.redo();
}
});
/**
* 加粗
*/

findViewById(R.id.action_bold).setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setBold();
}
});
/**
* 斜体
*/

findViewById(R.id.action_italic).setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setItalic();
}
});
/**
* 下角表
*/

findViewById(R.id.action_subscript).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
if (mEditor.getHtml() == null) {
return;
}
mEditor.setSubscript();
}
});
/**
* 上角标
*/

findViewById(R.id.action_superscript).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
if (mEditor.getHtml() == null) {
return;
}
mEditor.setSuperscript();
}
});

/**
* 删除线
*/

findViewById(R.id.action_strikethrough).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setStrikeThrough();
}
});
/**
*下划线
*/

findViewById(R.id.action_underline).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setUnderline();
}
});
/**
* 设置标题(1到6)
*/

findViewById(R.id.action_heading1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setHeading(1);
}
});

findViewById(R.id.action_heading2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setHeading(2);
}
});

findViewById(R.id.action_heading3).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setHeading(3);
}
});

findViewById(R.id.action_heading4).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setHeading(4);
}
});

findViewById(R.id.action_heading5).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setHeading(5);
}
});

findViewById(R.id.action_heading6).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setHeading(6);
}
});
/**
* 设置字体颜色
*/

findViewById(R.id.action_txt_color).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
new MaterialDialog.Builder(MainActivity.this)
.title("选择字体颜色")
.items(R.array.color_items)
.itemsCallbackSingleChoice(-1, new MaterialDialog.ListCallbackSingleChoice() {
@Override
public boolean onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text) {

dialog.dismiss();
switch (which) {
case 0://红
mEditor.setTextColor(Color.RED);
break;
case 1://黄
mEditor.setTextColor(Color.YELLOW);
break;
case 2://蓝
mEditor.setTextColor(Color.GREEN);
break;
case 3://绿
mEditor.setTextColor(Color.BLUE);
break;
case 4://黑
mEditor.setTextColor(Color.BLACK);
break;
}
return false;
}
}).show();
}
});

findViewById(R.id.action_bg_color).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
new MaterialDialog.Builder(MainActivity.this)
.title("选择字体背景颜色")
.items(R.array.text_back_color_items)
.itemsCallbackSingleChoice(-1, new MaterialDialog.ListCallbackSingleChoice() {
@Override
public boolean onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text) {

dialog.dismiss();
switch (which) {
case 0://红
mEditor.setTextBackgroundColor(Color.RED);
break;
case 1://黄
mEditor.setTextBackgroundColor(Color.YELLOW);
break;
case 2://蓝
mEditor.setTextBackgroundColor(Color.GREEN);
break;
case 3://绿
mEditor.setTextBackgroundColor(Color.BLUE);
break;
case 4://黑
mEditor.setTextBackgroundColor(Color.BLACK);
break;
case 5://透明
mEditor.setTextBackgroundColor(R.color.transparent);
break;
}
return false;
}
}).show();

}
});
/**
* 向右缩进
*/

findViewById(R.id.action_indent).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setIndent();
}
});
/**
* 向左缩进
*/

findViewById(R.id.action_outdent).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setOutdent();
}
});
/**
*文章左对齐
*/

findViewById(R.id.action_align_left).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.setAlignLeft();
}
});
/**
* 文章居中对齐
*/

findViewById(R.id.action_align_center).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setAlignCenter();
}
});
/**
* 文章右对齐
*/

findViewById(R.id.action_align_right).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setAlignRight();
}
});
/**
* 无序排列
*/

findViewById(R.id.action_insert_bullets).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setBullets();
}
});
/**
* 有序排列
*/

findViewById(R.id.action_insert_numbers).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setNumbers();
}
});
/**
* 引用
*/

findViewById(R.id.action_blockquote).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.setBlockquote();
}
});

/**
* 插入图片
*/

findViewById(R.id.action_insert_image).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
ActivityCompat.requestPermissions(MainActivity.this, mPermissionList, 100);
}
});
/**
* 插入连接
*/

findViewById(R.id.action_insert_link).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new MaterialDialog.Builder(MainActivity.this)
.title("将输入连接地址")
.items("http://blog.csdn.net/huangxiaoguo1")
.itemsCallbackSingleChoice(-1, new MaterialDialog.ListCallbackSingleChoice() {
@Override
public boolean onSelection(MaterialDialog dialog, View itemView, int which, CharSequence text) {
dialog.dismiss();
mEditor.focusEditor();
mEditor.insertLink("http://blog.csdn.net/huangxiaoguo1",
"http://blog.csdn.net/huangxiaoguo1");
return false;
}
}).show();
}
});
/**
* 选择框
*/

findViewById(R.id.action_insert_checkbox).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mEditor.focusEditor();
mEditor.insertTodo();
}
});

/**
* 获取并显示Html
*/

findViewById(R.id.tv_showhtml).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(v.getContext(), WebViewActivity.class);
intent.putExtra("contextURL", mEditor.getHtml());
startActivity(intent);
}
});


  • 插入图片并使用屏幕宽度




权限,我这里只是选着图片,关于拍照的自己可以去实现

String[] mPermissionList = new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE};


@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case 100:
boolean writeExternalStorage = grantResults[0] == PackageManager.PERMISSION_GRANTED;
boolean readExternalStorage = grantResults[1] == PackageManager.PERMISSION_GRANTED;
if (grantResults.length > 0 && writeExternalStorage && readExternalStorage) {
getImage();
} else {
Toast.makeText(this, "请设置必要权限", Toast.LENGTH_SHORT).show();
}

break;
}
}

private void getImage() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("image/*"),
REQUEST_PICK_IMAGE);
} else {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_PICK_IMAGE);
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
case REQUEST_PICK_IMAGE:
if (data != null) {
String realPathFromUri = RealPathFromUriUtils.getRealPathFromUri(this, data.getData());
mEditor.insertImage("https://unsplash.it/2000/2000?random&58",
"huangxiaoguo\" style=\"max-width:100%");
mEditor.insertImage(realPathFromUri, realPathFromUri + "\" style=\"max-width:100%");
// mEditor.insertImage(realPathFromUri, realPathFromUri + "\" style=\"max-width:100%;max-height:100%");

} else {
Toast.makeText(this, "图片损坏,请重新选择", Toast.LENGTH_SHORT).show();
}

break;
}
}
}

注意这里 “\” style=\”max-width:100%”是让我们从手机选择的图片和网络加载的图片适配屏幕宽高,解决图片太大显示不全问题!


关于如何获得手机图片真正地址(realPathFromUri )请看http://blog.csdn.net/huangxiaoguo1/article/details/78983582


richeditor-android github地址:https://github.com/wasabeef/richeditor-android


demo地址:http://download.csdn.net/download/huangxiaoguo1/10205773

收起阅读 »

Android加载离线和网络git

本文介绍如何将android-gif-drawable集成到项目中,并且如何使用android-gif-drawable加载离线和网络Gif动图。 android-gif-drawable的集成 在线集成 Github上相关教程,也比较简单,将依赖添加到...
继续阅读 »


本文介绍如何将android-gif-drawable集成到项目中,并且如何使用android-gif-drawable加载离线和网络Gif动图。


android-gif-drawable的集成


在线集成


Github上相关教程,也比较简单,将依赖添加到项目的build.gradle文件即可:


dependencies {
compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.11'
}

离线集成


Android Studio 3.0中有效



  1. 进入Github上的realease页面-realease点我


  2. 下载其中的android-gif-drawable-1.2.11.aar


  3. android-gif-drawable-1.2.11.aar添加到项目的libs目录中


  4. 在项目的build.gradle中添加该arr文件



compile(name:'android-gif-drawable-1.2.11', ext:'aar')


  1. 集成完毕,可以进行测试。


android-gif-drawable的使用


android-gif-drawable有四种控件:GifImageViewGifImageButtonGifTextViewGifTextureView。这里以ImageView为例进行介绍。


加载本地图片



  1. 直接在布局中选定资源文件


<pl.droidsonroids.gif.GifImageView
android:id="@+id/fragment_gif_local"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/dog"/>


  1. 通过代码进行动态添加gif动图


//1. 构建GifDrawable
GifDrawable gifFromResDrawable = new GifDrawable( getResources(), R.drawable.dog );
//2. 设置给GifImageView控件
gifImageView.setImageDrawable(gifFromResDrawable);

GifDrawable


GifDrawable是用于该开源库的Drawable类。构造方法大致有9种:


//1. asset file
GifDrawable gifFromAssets = new GifDrawable( getAssets(), "anim.gif" );

//2. resource (drawable or raw)
GifDrawable gifFromResource = new GifDrawable( getResources(), R.drawable.anim );

//3. byte array
byte[] rawGifBytes = ...
GifDrawable gifFromBytes = new GifDrawable( rawGifBytes );

//4. FileDescriptor
FileDescriptor fd = new RandomAccessFile( "/path/anim.gif", "r" ).getFD();
GifDrawable gifFromFd = new GifDrawable( fd );

//5. file path
GifDrawable gifFromPath = new GifDrawable( "/path/anim.gif" );

//6. file
File gifFile = new File(getFilesDir(),"anim.gif");
GifDrawable gifFromFile = new GifDrawable(gifFile);

//7. AssetFileDescriptor
AssetFileDescriptor afd = getAssets().openFd( "anim.gif" );
GifDrawable gifFromAfd = new GifDrawable( afd );

//8. InputStream (it must support marking)
InputStream sourceIs = ...
BufferedInputStream bis = new BufferedInputStream( sourceIs, GIF_LENGTH );
GifDrawable gifFromStream = new GifDrawable( bis );

//9. direct ByteBuffer
ByteBuffer rawGifBytes = ...
GifDrawable gifFromBytes = new GifDrawable( rawGifBytes );

加载网络Gif


我们解决的办法是将Gif图片下载到缓存目录中,然后从磁盘缓存中获取该Gif动图进行显示。


1、下载工具DownloadUtils.java


public class DownloadUtils {
private final int DOWN_START = 1; // Handler消息类型(开始下载)
private final int DOWN_POSITION = 2; // Handler消息类型(下载位置)
private final int DOWN_COMPLETE = 3; // Handler消息类型(下载完成)
private final int DOWN_ERROR = 4; // Handler消息类型(下载失败)
private OnDownloadListener onDownloadListener;

public void setOnDownloadListener(OnDownloadListener onDownloadListener) {
this.onDownloadListener = onDownloadListener;
}

/**
* 下载文件
*
* @param url 文件路径
* @param filepath 保存地址
*/

public void download(String url, String filepath) {
MyRunnable mr = new MyRunnable();
mr.url = url;
mr.filepath = filepath;
new Thread(mr).start();
}

@SuppressWarnings("unused")
private void sendMsg(int what) {
sendMsg(what, null);
}

private void sendMsg(int what, Object mess) {
Message m = myHandler.obtainMessage();
m.what = what;
m.obj = mess;
m.sendToTarget();
}

Handler myHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DOWN_START: // 开始下载
int filesize = (Integer) msg.obj;
onDownloadListener.onDownloadConnect(filesize);
break;
case DOWN_POSITION: // 下载位置
int pos = (Integer) msg.obj;
onDownloadListener.onDownloadUpdate(pos);
break;
case DOWN_COMPLETE: // 下载完成
String url = (String) msg.obj;
onDownloadListener.onDownloadComplete(url);
break;
case DOWN_ERROR: // 下载失败
Exception e = (Exception) msg.obj;
e.printStackTrace();
onDownloadListener.onDownloadError(e);
break;
}
super.handleMessage(msg);
}
};

class MyRunnable implements Runnable {
private String url = "";
private String filepath = "";

@Override
public void run() {
try {
doDownloadTheFile(url, filepath);
} catch (Exception e) {
sendMsg(DOWN_ERROR, e);
}
}
}

/**
* 下载文件
*
* @param url 下载路劲
* @param filepath 保存路径
* @throws Exception
*/

private void doDownloadTheFile(String url, String filepath) throws Exception {
if (!URLUtil.isNetworkUrl(url)) {
sendMsg(DOWN_ERROR, new Exception("不是有效的下载地址:" + url));
return;
}
URL myUrl = new URL(url);
URLConnection conn = myUrl.openConnection();
conn.connect();
InputStream is = null;
int filesize = 0;
try {
is = conn.getInputStream();
filesize = conn.getContentLength();// 根据响应获取文件大小
sendMsg(DOWN_START, filesize);
} catch (Exception e) {
sendMsg(DOWN_ERROR, new Exception(new Exception("无法获取文件")));
return;
}
FileOutputStream fos = new FileOutputStream(filepath); // 创建写入文件内存流,
// 通过此流向目标写文件
byte buf[] = new byte[1024];
int numread = 0;
int temp = 0;
while ((numread = is.read(buf)) != -1) {
fos.write(buf, 0, numread);
fos.flush();
temp += numread;
sendMsg(DOWN_POSITION, temp);
}
is.close();
fos.close();
sendMsg(DOWN_COMPLETE, filepath);
}

interface OnDownloadListener{
public void onDownloadUpdate(int percent);

public void onDownloadError(Exception e);

public void onDownloadConnect(int filesize);

public void onDownloadComplete(Object result);
}
}

2、调用DonwloadUtils进行下载,下载完成后加载本地图片


//1. 下载gif图片(文件名自定义可以通过Hash值作为key)
DownloadUtils downloadUtils = new DownloadUtils();
downloadUtils.download(gifUrlArray[0],
getDiskCacheDir(getContext())+"/0.gif");
//2. 下载完毕后通过“GifDrawable”进行显示
downloadUtils.setOnDownloadListener(new DownloadUtils.OnDownloadListener() {
@Override
public void onDownloadUpdate(int percent) {
}
@Override
public void onDownloadError(Exception e) {
}
@Override
public void onDownloadConnect(int filesize) {
}
//下载完毕后进行显示
@Override
public void onDownloadComplete(Object result) {
try {
GifDrawable gifDrawable = new GifDrawable(getDiskCacheDir(getContext())+"/0.gif");
mGifOnlineImageView.setImageDrawable(gifDrawable);
} catch (IOException e) {
e.printStackTrace();
}
}
});

//获取缓存的路径
public String getDiskCacheDir(Context context) {
String cachePath = null;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
// 路径:/storage/emulated/0/Android/data/<application package>/cache
cachePath = context.getExternalCacheDir().getPath();
} else {
// 路径:/data/data/<application package>/cache
cachePath = context.getCacheDir().getPath();
}
return cachePath;
}

github地址:https://github.com/koral--/android-gif-drawable

转载请注明链接:http://blog.csdn.net/feather_wch/article/details/79558240

收起阅读 »

Android 安卓超级强劲的轻量级数据库ObjectBox,快的飞起

文章目录 ObjectBox 引入ObjectBox 简单的代码栗子 生成和创建数据库 ObjectBox初始化 基本操作 - 增 基本操作 - ...
继续阅读 »


在这里插入图片描述







ObjectBox


ObjectBox是一个超快的面向对象数据库,相比于Sqlite,效率高了10倍左右






引入ObjectBox


在跟项目中的build.gradle中引入:


buildscript {
...
ext.objectboxVersion = '2.9.1'

dependencies {
...
classpath "io.objectbox:objectbox-gradle-plugin:$objectboxVersion"
...
}
}

在app下的build.gradle头部引入


(有以下两种情况,看你项目中用的什么):


plugins {
...
id 'io.objectbox'
}

apply plugin: 'io.objectbox'





简单的代码栗子


接下来将会讲解ObjectBox基本使用






生成和创建数据库


1、新建一个模型类,并使用 @Entity 将类注解,@Id 为自增主键(进阶的代码栗子会详细一点讲注解),@Id 注解也是必不可少的。


package com.mt.objectboxproject

import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id

@Entity
data class Person(
@Id
var id: Long = 0,
var age: Int = 0,
var name: String? = null
)



2、AndroidStudio操作:Build -> MakeProject,或者点击运行按钮旁边的小锤子锤一下,这一步是为了生成ObjectBox所需要的文件,之后便会看到生成了 app\objectbox-models\default.json 文件






ObjectBox初始化


1、创建ObjectBox的小助手,需要在 Application 中进行调用 init 初始化


package com.mt.objectboxproject

import android.content.Context
import io.objectbox.BoxStore

/**
* ObjectBox的小助手,需要在Application中进行调用init初始化
*/

object ObjectBox {
lateinit var store: BoxStore
private set

fun init(context: Context) {
store = MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
}
}



2、在 Application 中初始化


package com.mt.objectboxproject

import android.app.Application

class MainApplication : Application() {
override fun onCreate() {
super.onCreate()

//初始化ObjectBox
ObjectBox.init(this)
}
}



基本操作 - 增


package com.mt.objectboxproject

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

//插入一条数据
val userBox = ObjectBox.store.boxFor(Person::class.java)
val person = Person()
person.age = 21
person.name = "第三女神程忆难"
userBox.put(person)

//==========================================================================================

//插入多条数据
val persons = mutableListOf<Person>()

//模拟多条数据
val person1 = Person()
person1.age = 24
person1.name = "1bit"

val person2 = Person()
person2.age = 25
person2.name = "梦想橡皮擦"

val person3 = Person()
person3.age = 26
person3.name = "沉默王二"

persons.add(person1)
persons.add(person2)
persons.add(person3)

//插入数据库
userBox.put(persons)


}
}



基本操作 - 查


package com.mt.objectboxproject

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val userBox = ObjectBox.store.boxFor(Person::class.java)

//==========================================================================================
//根据主键id查询
val person = userBox[1]

//==========================================================================================
//获取person有所数据
val allPersons = userBox.all

//==========================================================================================
//条件查询
val build = userBox.query()
.equal(Person_.name, "沉默王二")
.order(Person_.name)
.build()

//查找数据
val find = build.find()

//记得close
build.close()

}
}



基本操作 - 删


package com.mt.objectboxproject

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val userBox = ObjectBox.store.boxFor(Person::class.java)

//==========================================================================================
//根据主键Id去删除
val isRemoved = userBox.remove(1)

//==========================================================================================
//根据主键id集合去删除
val ids = mutableListOf<Long>(1,2,3,4)
userBox.removeByIds(ids)

//==========================================================================================
//根据模型类去删除
val person = userBox[1]
person.name = "第三女神程忆难"
userBox.remove(person)

//==========================================================================================
//删除所有数据
userBox.removeAll()

}
}



基本操作 - 改


package com.mt.objectboxproject

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val userBox = ObjectBox.store.boxFor(Person::class.java)

//==========================================================================================
//先查询获取到person,set值后重新put
val person = userBox[1]
person.name = "小傅哥"
userBox.put(person)
}
}





进阶的代码栗子


注解讲解






注解



  • @Id:主键,默认为自增主键,交由ObjectBox管理

  • @Index:注释一个属性,为相应的数据库列创建一个数据库索引。这可以提高查询该属性时的性能。

  • @Transient:标记不应保留的属性。在 Java 中,静态或瞬态属性也不会被持久化。

  • @NameInDb:对数据库中字段名进行自定义。

  • @Backlink:反向关联。

  • @ToOne:一对一关联注解。

  • @ToMany:一对多关联注解。






ObjectBox地址:https://docs.objectbox.io

收起阅读 »

简阅-一个以Kotlin实现的第三方聚合阅读App开源啦

简阅(SimpleRead)以Kotlin实现的简单纯净的阅读软件,主要使用到MVP+RxJava+Retrofit+RxLifecycle+Glide+GreenDao等技术软件开发背景简阅是我学习安卓开发的第一个项目,最初是使用传统的MVC模式,然后一步步...
继续阅读 »

简阅(SimpleRead)

以Kotlin实现的简单纯净的阅读软件,主要使用到MVP+RxJava+Retrofit+RxLifecycle+Glide+GreenDao等技术

软件开发背景

简阅是我学习安卓开发的第一个项目,最初是使用传统的MVC模式,然后一步步迭代,由MVP再到Kotlin.如今项目功能已经基本稳定,我将项目规范了下, 然后开源供大家交流学习,毕竟当时学习也看了很多前辈的项目,学到了很多,所以现在是时候回报开源社区啦。

软件地址

酷安下载地址

软件截图

  

实现的功能

知乎日报
  • 获取知乎日报最新新闻
  • 上拉加载前一天知乎新闻
  • 可选择阅读具体某天的知乎新闻
  • 可随机阅读一篇知乎新闻
  • 已读新闻灰显
  • 收藏/取消收藏某一篇新闻
  • 分享新闻
煎蛋新鲜事
  • 获取最新煎蛋新鲜事
  • 上拉加载前一天新鲜事
  • 已读新闻灰显
  • 收藏/取消收藏某一篇新闻
  • 分享新闻
每日一文
  • 查看当天的文章
  • 随机一篇文章
  • 三种阅读风格切换

其余

  • 遵循Material Design设计
  • 多种主题选择
  • Frament懒加载
  • 网络缓存
  • 离线缓存

技术慨要

  • MVP

    MVP是目前安卓开发中最流行的架构之一,Model负责数据和业务逻辑,View层负责view相关的展示以及context层的调用,Presenter层负责使M层和V层交互

  • RxJava

    RxJava是一个基于事件流的异步响应框架

    给 Android 开发者的 RxJava 详解 -- 扔物线

  • Retrofit

    RESTful的HTTP网络请求框架,优势在于可以结合RxJava实现链式网络请求以及轻松实现线程调度,同时它是以注解的方式标注请求,优雅简洁

  • RxLifecycle

    RxLifecycle是知乎团队出的一个方便取消RxJava订阅的库,使用它结合RxJava无需再到onDestory()中取消订阅

  • GreenDao

    GreenDao是一个老牌ORM数据库框架,目前3.2.2版本可以说是最值得使用的ORM框架

  • Glide

    一个API简洁但是功能极为强大的图片加载框架

  • jsoup

    jsoup是一个强大的解析html网页源码的库

  • BaseRecyclerViewAdapterHelper

    一个快速实现RecyclerviewAdapter的库,和普通写法比起来能减少70%代码量

  • 其余还有一些相关技术就不一一罗列出来了,大家可以自行查看源码.

收起阅读 »

Android资源冲突检测插件

背景 之前我们写了一篇定义关于如何定义Gradle插件,有兴趣的朋友可以看一下,今天我们就来简单讲一个自定义Gradle插件的实战Android项目Module间资源冲突检测的Gradle插件。对应的使用方法和源码已经在GitHub给出Android资源冲突检...
继续阅读 »

背景


之前我们写了一篇定义关于如何定义Gradle插件,有兴趣的朋友可以看一下,今天我们就来简单讲一个自定义Gradle插件的实战Android项目Module间资源冲突检测的Gradle插件。对应的使用方法和源码已经在GitHub给出Android资源冲突检测插件


解决问题


具体我们的插件的作用是干什么的呢?这里简单解释一下,就是当我们的项目越来越大的时候我们会将项目拆分为多个Module,这个时候,每个Module里面都有自己的资源文件,包括图片,文字,颜色,字体大小等。如果我们在多个Module里面定义了相同名字的资源,但是对应的资源内容不一样,这个时候项目并不会出错,但是当我们最终打包的时候多个Module中的资源只会留下一个,这样我们想要的效果就会出错。这个插件就是用来跑整个项目所有Module将有冲突的资源提取出来,目前只支持String,Color,Dimen,其他的会在后续补充。


实现方式


首先,我们先接着自定义Gradle插件的思路往下讲,关于自定义Gradle的一些基本知识:

大家也可以查看:Gradle官方文档

或者查看我的上一篇:如何定义Gradle插件

1、先定义一个我们自己的Task,继承DefaultTask,用来接收一些参数


public class GeekTask extends DefaultTask {
private boolean strings;
private boolean colors;
private boolean dimens;
public boolean getStringFlag() {
return strings;
}
@Input
public void checkString(boolean flag) {
this.strings = flag;
}

public boolean getColorFlag() {
return colors;
}
@Input
public void checkColors(boolean flag) {
this.colors = flag;
}

@Input
public void checkDimens(boolean flag){
this.dimens = flag;
}

public boolean getDimensFlag(){
return dimens;
}
@TaskAction
void sayGreeting() {
System.out.printf("%s, %s!\n", getStringFlag(), getColorFlag());
}

}

2、我们怎么去调用我们定义的这个Task呢?


 @Override
void apply(Project project) {
GeekTask task = project.getTasks().create("checkResources", GeekTask.class)
}

其中checkResources是我们定义的Task的名称,后面我们可以调用它。


checkResources{
checkString true
checkColors true
checkDimens true
}

这个是定义在我们需要使用我们自己写的插件的Module对应的Gradle文件里面的checkResources表示Task的名字,下面的是对应的方法和参数。当然,在这个Gradle里面需要引用我们的插件apply plugin: '插件名字'

3、使用传递进来的参数。


GeekTask task = project.getTasks().create("checkResources", GeekTask.class)
task.doLast {
System.out.println(task.getStringFlag())
if (task.getStringFlag()) {
// do check string
}
if (task.getColorFlag()) {
// do check color
}
if (task.getDimensFlag()) {
// do check dimen
}
}

上面我们通过我们定义的task就可获取到,我们传递进来的参数,task.doLast这一步表示我们里面的代码执行在Task的之后保证可以获取到参数,这里稍微讲一下插件代码的运行时机:

如果我们直接写在apply()方法中的代码是执行的编译期,也就是一开始就执行了,是执行在任何之前的。

task.doFirst {}虽然也是在Task之前执行,但是它是在要执行Task的时候先执行doFirst里面的代码。

task.doLast{}这个是执行在Task执行之后的。

4、怎么实现资源检测。

这个代码比较简单主要是获取所有module下对应资源的文件,然后进行解析和比较,具体的代码这里就不写了,有兴趣的朋友可以下载完整代码Android资源冲突检测插件


如何使用


首先我们要在项目最外层的build.gradle里面引用我上传的项目


apply plugin: 'geekplugin'

其次加载其代码


classpath 'com.geek.check:AndroidResourceCheck:1.0.0'

这里注意是calsspath具体和compile的区别大家可以Google一下

然后设置参数,用来配置我们需要检测的资源


checkResources{
checkString true
checkColors true
checkDimens true
}

最后就是运行这个插件

我们可以在项目的根目录运行这个Task


gradle checkResources

如果我们有资源冲突文件,最后会在项目的跟目录生成ResourcesError目录,对应的冲突文件在里面,大家可以查看。


总结


好了,这个插件大概就这么多东西,相信大家通过这个也会对自定义Gradle插件有更深的一些认识,当然,这还只是一些皮毛,更深层次的使用还需要大家去研究,谁有更好的资料和建议也可以评论提出,我们一起进步。



作者:Only凹凸曼
链接:https://www.jianshu.com/p/9d2a047f2c22
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

一文全面了解Android单元测试

前言 众所周知,一个好的项目需要不断地打造,而一些有效的测试则是加速这一过程的利器。本篇博文将带你了解并逐步深入Android单元测试。 什么是单元测试? 单元测试就是针对类中的某一个方法进行验证是否正确的过程,单元就是指独立的粒子,在Android...
继续阅读 »

前言


众所周知,一个好的项目需要不断地打造,而一些有效的测试则是加速这一过程的利器。本篇博文将带你了解并逐步深入Android单元测试。


什么是单元测试?




单元测试就是针对类中的某一个方法进行验证是否正确的过程,单元就是指独立的粒子,在Android和Java中大都是指方法。


为什么要进行单元测试?




使用单元测试可以提高开发效率,当项目随着迭代越来越大时,每一次编译、运行、打包、调试需要耗费的时间会随之上升,因此,使用单元测试可以不需这一步骤就可以对单个方法进行功能或逻辑测试。 同时,为了能测试每一个细分功能模块,需要将其相关代码抽成相应的方法封装起来,这也在一定程度上改善了代码的设计。因为是单个方法的测试,所以能更快地定位到bug。


单元测试case需要对这段业务逻辑进行验证。在验证的过程中,开发人员可以深度了解业务流程,同时新人来了看一下项目单元测试就知道哪个逻辑跑了多少函数,需要注意哪些边界——是的,单元测试做的好和文档一样具备业务指导能力。


Android测试的分类




Android测试主要分为三个方面:



  • 1)、单元测试(Junit4、Mockito、PowerMockito、Robolectric)

  • 2)、UI测试(Espresso、UI Automator)

  • 3)、压力测试(Monkey)


一、单元测试之基础Junit4




什么是Junit4?




Junit4是事实上的Java标准测试库,并且它是JUnit框架有史以来的最大改进,其主要目标便是利用Java5的Annotation特性简化测试用例的编写。


开始使用Junit4进行单元测试




1.Android Studio已经自动集成了Junit4测试框架,如下


    dependencies {
...
testImplementation 'junit:junit:4.12'
}

2.Junit4框架使用时涉及到的重要注解如下


    @Test 指明这是一个测试方法 (@Test注解可以接受2个参数,一个是预期错误
expected,一个是超时时间timeout,
格式如 @Test(expected = IndexOutOfBoundsException.class),
@Test(timeout = 1000)
@Before 在所有测试方法之前执行
@After 在所有测试方法之后执行
@BeforeClass 在该类的所有测试方法和@Before方法之前执
行 (修饰的方法必须是静态的)@AfterClass 在该类的所有测试方法和@After
方法之后执行(修饰的方法必须是静态的)
@Ignore 忽略此单元测试

此外,很多时候,因为某些原因(比如正式代码还没有实现等),我们可能想让JUnit忽略某些方法,让它在跑所有测试方法的时候不要跑这个测试方法。要达到这个目的也很简单,只需要在要被忽略的测试方法前面加上@Ignore就可以了


3.主要的测试方法——断言


    assertEquals(expected, actual) 判断2个值是否相等,相等则测试通过。
assertEquals(expected, actual, tolerance) tolerance 偏差值

注意:上面的每一个方法,都有一个重载的方法,可以加一个String类型的参数,表示如果验证失败的话,将用这个字符串作为失败的结果报告


4.自定义Junit Rule——实现TestRule接口并重写apply方法


    public class JsonChaoRule implements TestRule {

@Override
public Statement apply(final Statement base, final Description description) {
Statement repeatStatement = new Statement() {
@Override
public void evaluate() throws Throwable {
//测试前的初始化工作
//执行测试方法
base.evaluate();
//测试后的释放资源等工作
}
};
return repeatStatement;
}
}

然后在想要的测试类中使用@Rule注解声明使用JsonChaoRule即可(注意被@Rule注解的变量必须是final的):


    @Rule
public final JsonChaoRule repeatRule = new JsonChaoRule();

5.开始上手,使用Junit4进行单元测试



  • 1.编写测试类。

  • 2.鼠标右键点击测试类,选择选择Go To->Test (或者使用快捷键Ctrl+Shift+T,此快捷键可 以在方法和测试方法之间来回切换)在Test/java/项目 测试文件夹/下自动生成测试模板。

  • 3.使用断言(assertEqual、assertEqualArrayEquals等等)进行单元测试。

  • 4.右键点击测试类,Run编写好的测试类。


6.使用Android Studio自带的Gradle脚本自动化单元测试


点击Android Studio中的Gradle projects下的app/Tasks/verification/test即可同时测试module下所有的测试类(案例),并在module下的build/reports/tests/下生成对应的index.html测试报告


7.对Junit4的总结:



  • 优点:速度快,支持代码覆盖率等代码质量的检测工具,

  • 缺点:无法单独对Android UI,一些类进行操作,与原生JAVA有一些差异。


可能涉及到的额外的概念:


打桩方法:使方法简单快速地返回一个有效的结果。


测试驱动开发:编写测试,实现功能使测试通过,然后不断地使用这种方式实现功能的快速迭代开发。


二、单元测试之基础Mockito


什么是Mockito?


Mockito 是美味的 Java 单元测试 Mock 框架,mock可以模拟各种各样的对象,从而代替真正的对象做出希望的响应。


开始使用Mockito进行单元测试


1.在build.gradle里面添加Mcokito的依赖


    testImplementation 'org.mockito:mockito-core:2.7.1'

2.使用mock()方法模拟对象


    Person mPerson = mock(Person.class); 

能量补充站(-vov-)


在JUnit框架下,case(即每一个测试点,带@Test注解的那个函数)也是个函数,直接调用这个函数就不是case,和case是无关的,两者并不会相互影响,可以直接调用以减少重复代码。单元测试不应该对某一个条件过度耦合,因此,需要用mock解除耦合,直接mock出网络请求得到的数据,单独验证页面对数据的响应。


3.验证方法的调用,指定方法的返回值,或者执行特定的动作


    when(iMathUtils.sum(1, 1)).thenReturn(2); 
doReturn(3).when(iMathUtils).sum(1,1);
//给方法设置桩可以设置多次,只会返回最后一次设置的值
doReturn(2).when(iMathUtils).sum(1,1);

//验证方法调用次数
//方法调用1次
Mockito.when(mockValidator.verifyPassword("xiaochuang_is_handsome")).thenReturn(true);
//方法调用3次
Mockito.when(mockValidator.verifyPassword("xiaochuang_is_handsome")
, Mockito.times(3).thenReturn(true);

//verify方法用于验证“模仿对象”的互动或验证发生的某些行为
verify(mPerson, atLeast(2)).getAge();

//参数匹配器,用于匹配特定的参数
any()
contains()
argThat()
when(mPerson.eat(any(String.class))).thenReturn("米饭");

//除了mock()外,spy()也可以模拟对象,spy与mock的
//唯一区别就是默认行为不一样:spy对象的方法默认调用
//真实的逻辑,mock对象的方法默认什么都不做,或直接
//返回默认值
//如果要保留原来对象的功能,而仅仅修改一个或几个
//方法的返回值,可以采用spy方法,无参构造的类初始
//化也使用spy方法
Person mPerson = spy(Person.class);

//检查入参的mocks是否有任何未经验证的交互
verifyNoMoreInteractions(iMathUtils);

4.使用Mockito后的思考


简单的测试会使整体的代码更简单,更可读、更可维护。如果你不能把测试写的很简单,那么请在测试时重构你的代码



  • 优点:丰富强大的方式验证“模仿对象”的互动或验证发生的某些行为

  • 缺点:Mockito框架不支持mock匿名类、final类、static方法、private方法。


虽然,static方法可以使用wrapper静态类的方式实现mockito的单元测试,但是,毕竟过于繁琐,因此,PowerMockito由此而来。


三、拯救Mockito于水深火热的PowerMockito


什么是PowerMockito?


PowerMockito是一个扩展了Mockito的具有更强大功能的单元测试框架,它支持mock匿名类、final类、static方法、private方法


开始PowerMockito之旅


1.在build.gradle里面添加Mcokito的依赖


    testImplementation 'org.powermock:powermock-module-junit4:1.6.5'
testImplementation 'org.powermock:powermock-api-mockito:1.6.5'

2.用PowerMockito来模拟对象


    //使用PowerMock须加注解@PrepareForTest和@RunWith(PowerMockRunner.class)(@PrepareForTest()里写的
// 是对应方法所在的类 ,mockito支持的方法使用PowerMock的形式实现时,可以不加这两个注解)
@PrepareForTest(T.class)
@RunWith(PowerMockRunner.class)

//mock含静态方法或字段的类
PowerMockito.mockStatic(Banana.class);

//Powermock提供了一个Whitebox的class,可以方便的绕开权限限制,可以get/set private属性,实现注入。
//也可以调用private方法。也可以处理static的属性/方法,根据不同需求选择不同参数的方法即可。
修改类里面静态字段的值
Whitebox.setInternalState(Banana.class, "COLOR", "蓝色");

//调用类中的真实方法
PowerMockito.when(banana.getBananaInfo()).thenCallRealMethod();

//验证私有方法是否被调用
PowerMockito.verifyPrivate(banana, times(1)).invoke("flavor");

//忽略调用私有方法
PowerMockito.suppress(PowerMockito.method(Banana.class, "flavor"));

//修改私有变量
MemberModifier.field(Banana.class, "fruit").set(banana, "西瓜");

//使用PowerMockito mock出来的对象可以直接调用final方法
Banana banana = PowerMockito.mock(Banana.class);

//whenNew 方法的意思是之后 new 这个对象时,返回某个被 Mock 的对象而不是让真的 new
//新的对象。如果构造方法有参数,可以在withNoArguments方法中传入。
PowerMockito.whenNew(Banana.class).withNoArguments().thenReturn(banana);

3.使用PowerMockRule来代替@RunWith(PowerMockRunner.class)的方式,需要多添加以下依赖:


    testImplementation "org.powermock:powermock-module-junit4-rule:1.7.4"
testImplementation "org.powermock:powermock-classloading-xstream:1.7.4"

使用示例如下:


    @Rule
public PowerMockRule mPowerMockRule = new PowerMockRule();

4.使用Parameterized来进行参数化测试:


通过注解@Parameterized.parameters提供一系列数据给构造器中的构造参数或给被注解@Parameterized.parameter注解的public全局变量


    RunWith(Parameterized.class)
public class ParameterizedTest {

private int num;
private boolean truth;

public ParameterizedTest(int num, boolean truth) {
this.num = num;
this.truth = truth;
}

//被此注解注解的方法将把返回的列表数据中的元素对应注入到测试类
//的构造函数ParameterizedTest(int num, boolean truth)中
@Parameterized.Parameters
public static Collection providerTruth()
{
return Arrays.asList(new Object[][]{
{0, true},
{1, false},
{2, true},
{3, false},
{4, true},
{5, false}
});
}

// //也可不使用构造函数注入的方式,使用注解注入public变量的方式
// @Parameterized.Parameter
// public int num;
// //value = 1指定括号里的第二个Boolean值
// @Parameterized.Parameter(value = 1)
// public boolean truth;

@Test
public void printTest() {
Assert.assertEquals(truth, print(num));
System.out.println(num);
}

private boolean print(int num) {
return num % 2 == 0;
}

}

四、能在Java单元测试里面执行Android代码的Robolectric


什么是Robolectric?


Robolectric通过一套能运行在JVM上的Android代码,解决了在Java单元测试中很难进行Android单元测试的痛点。


进入Roboletric的领地




1.在build.gradle里面添加Robolectric的依赖


        //Robolectric核心
testImplementation "org.robolectric:robolectric:3.8"
//支持support-v4
testImplementation 'org.robolectric:shadows-support-v4:3.4-rc2'
//支持Multidex功能
testImplementation "org.robolectric:shadows-multidex:3.+"

2.Robolectric常用用法


首先给指定的测试类上面进行配置


    @RunWith(RobolectricTestRunner.class)
//目前Robolectric最高支持sdk版本为23。
@Config(constants = BuildConfig.class, sdk = 23)

下面是一些常用用法:


    //当Robolectric.setupActivity()方法返回的时候,
//默认会调用Activity的onCreate()、onStart()、onResume()
mTestActivity = Robolectric.setupActivity(TestActivity.class);

//获取TestActivity对应的影子类,从而能获取其相应的动作或行为
ShadowActivity shadowActivity = Shadows.shadowOf(mTestActivity);
Intent intent = shadowActivity.getNextStartedActivity();

//使用ShadowToast类获取展示toast时相应的动作或行为
Toast latestToast = ShadowToast.getLatestToast();
Assert.assertNull(latestToast);
//直接通过ShadowToast简单工厂类获取Toast中的文本
Assert.assertEquals("hahaha", ShadowToast.getTextOfLatestToast());

//使用ShadowAlertDialog类获取展示AlertDialog时相应的
//动作或行为(暂时只支持app包下的,不支持v7。。。)
latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
AlertDialog latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
Assert.assertNull(latestAlertDialog);

//使用RuntimeEnvironment.application可以获取到
//Application,方便我们使用。比如访问资源文件。
Application application = RuntimeEnvironment.application;
String appName = application.getString(R.string.app_name);
Assert.assertEquals("WanAndroid", appName);

//也可以直接通过ShadowApplication获取application
ShadowApplication application = ShadowApplication.getInstance();
Assert.assertNotNull(application.hasReceiverForIntent(intent));

自定义Shadow类:


    @Implements(Person.class)
public class ShadowPerson {

@Implementation
public String getName() {
return "AndroidUT";
}

}

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class,
sdk = 23,
shadows = {ShadowPerson.class})


Person person = new Person();
//实际上调用的是ShadowPerson的方法,输出JsonChao
Log.d("test", person.getName());

ShadowPerson shadowPerson = Shadow.extract(person);
//测试通过
Assert.assertEquals("JsonChao", shadowPerson.getName());

}

注意:异步测试出现一些问题(比如改变一些编码习惯,比如回调函数不能写成匿名内部类对象,需要定义一个全局变量,并破坏其封装性,即提供一个get方法,供UT调用),解决方案使用Mockito来结合进行测试,将异步转为同步


3.Robolectric的优缺点



  • 优点:支持大部分Android平台依赖类底层的引用与模拟。

  • 缺点:异步测试有些问题,需要结合一些框架来配合完成更多功能。


五、单元测试覆盖率报告生成之jacoco


什么是Jacoco


Jacoco的全称为Java Code Coverage(Java代码覆盖率),可以生成java的单元测试代码覆盖率报告


加入Jacoco到你的单元测试大家族


在应用Module下加入jacoco.gradle自定义脚本,app.gradle apply from它,同步,即可看到在app的Task下生成了Report目录,Report目录 下生成了JacocoTestReport任务。


    apply plugin: 'jacoco'

jacoco {
toolVersion = "0.7.7.201606060606" //指定jacoco的版本
reportsDir = file("$buildDir/JacocoReport") //指定jacoco生成报告的文件夹
}

//依赖于testDebugUnitTest任务
task jacocoTestReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') {
group = "reporting" //指定task的分组
reports {
xml.enabled = true //开启xml报告
html.enabled = true //开启html报告
}

def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug",
includes: ["**/*Presenter.*"],
excludes: ["*.*"])//指定类文件夹、包含类的规则及排除类的规则,
//这里我们生成所有Presenter类的测试报告
def mainSrc = "${project.projectDir}/src/main/java" //指定源码目录

sourceDirectories = files([mainSrc])
classDirectories = files([debugTree])
executionData = files("${buildDir}/jacoco/testDebugUnitTest.exec")//指定报告数据的路径
}

在Gradle构建板块Gradle.projects下的app/Task/verification下,其中testDebugUnitTest构建任务会生成单元测试结果报告,包含xml及html格式,分别对应test-results和reports文件夹;jacocoTestReport任务会生成单元测试覆盖率报告,结果存放在jacoco和JacocoReport文件夹。


image


生成的JacocoReport文件夹下的index.html即对应的单元测试覆盖率报告,用浏览器打开后,可以看到覆盖情况被不同的颜色标识出来,其中绿色表示代码被单元测试覆盖到,黄色表示部分覆盖,红色则表示完全没有覆盖到


六、单元测试的流程


要验证程序正确性,必然要给出所有可能的条件(极限编程),并验证其行为或结果,才算是100%覆盖条件。实际项目中,验证一般条件边界条件就OK了。


在实际项目中,单元测试对象与页面是一对一的,并不建议跨页面,这样的单元测试耦合太大,维护困难。 需要写完后,看覆盖率,找出单元测试中没有覆盖到的函数分支条件等,然后继续补充单元测试case列表,并在单元测试工程代码中补上case。 直到规划的页面中所有逻辑的重要分支、边界条件都被覆盖,该项目的单元测试结束。


建议(-ovo-)~


可以从公司项目小规模使用,形成自己的单元测试风格后,就可更大范围地推广了。




资源git地址:==》完整项目单元测试学习案例


收起阅读 »

全新 LeakCanary 2 ! 完全基于 Kotlin 重构升级 !

大概一年以前,写过一篇 LeakCanary 源码解析 ,当时是基于 1.5.4 版本进行分析的 。Square 公司在今年四月份发布了全新的 2.0 版本,完全使用 Kotlin 进行重构,核心原理并没有太大变化,但是做了一定的性能优化。在本文中,就让我们通...
继续阅读 »

大概一年以前,写过一篇 LeakCanary 源码解析 ,当时是基于 1.5.4 版本进行分析的 。Square 公司在今年四月份发布了全新的 2.0 版本,完全使用 Kotlin 进行重构,核心原理并没有太大变化,但是做了一定的性能优化。在本文中,就让我们通过源码来看看 2.0 版本发生了哪些变化。本文不会过多的分析源码细节,详细细节可以阅读我之前基于 1.5.4 版本写的文章,两个版本在原理方面并没有太大变化。



含注释 fork 版本 LeakCanary 源码



使用


首先来对比一下两个版本的使用方式。


1.5.4 版本


在老版本中,我们需要添加如下依赖:


dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
}

leakcanary-android-no-op 库在 release 版本中使用,其中是没有任何逻辑代码的。


然后需要在自己的 Application 中进行初始化。


public class ExampleApplication extends Application {

@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}

LeakCanary.install() 执行后,就会构建 RefWatcher 对象,开始监听 Activity.onDestroy() 回调, 通过 RefWatcher.watch() 监测 Activity 引用的泄露情况。发现内存泄露之后进行 heap dump ,利用 Square 公司的另一个库 haha(已废弃)来分析 heap dump 文件,找到引用链之后通知用户。这一套原理在新版本中还是没变的。


2.0 版本


新版本的使用更加方便了,你只需要在 build.gradle 文件中添加如下依赖:


debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-2'

是的,你没看过,这样就可以了。你肯定会有一个疑问,那它是如何初始化的呢?我刚看到这个使用文档的时候,同样也有这个疑问。当你看看源码之后就一目了然了。我先不解释,看一下源码中的 LeakSentryInstaller 这个类:


/**
* Content providers are loaded before the application class is created. [LeakSentryInstaller] is
* used to install [leaksentry.LeakSentry] on application start.
*
* Content Provider 在 Application 创建之前被自动加载,因此无需用户手动在 onCrate() 中进行初始化
*/

internal class LeakSentryInstaller : ContentProvider() {

override fun onCreate(): Boolean {
CanaryLog.logger = DefaultCanaryLog()
val application = context!!.applicationContext as Application
InternalLeakSentry.install(application) // 进行初始化工作,核心
return true
}

override fun query(
uri: Uri,
strings: Array<String>?,
s: String?,
strings1: Array<String>?,
s1: String?
)
: Cursor? {
return null
}

override fun getType(uri: Uri): String? {
return null
}

override fun insert(
uri: Uri,
contentValues: ContentValues?
)
: Uri? {
return null
}

override fun delete(
uri: Uri,
s: String?,
strings: Array<String>?
)
: Int {
return 0
}

override fun update(
uri: Uri,
contentValues: ContentValues?,
s: String?,
strings: Array<String>?
)
: Int {
return 0
}
}

看到这个类你应该也明白了。LeakCanary 利用 ContentProvier 进行了初始化。ContentProvier 一般会在 Application 被创建之前被加载,LeakCanary 在其 onCreate() 方法中调用了 InternalLeakSentry.install(application) 进行初始化。这应该是我第一次看到第三方库这么进行初始化。这的确是方便了开发者,但是仔细想想弊端还是很大的,如果所有第三方库都这么干,开发者就没法控制应用启动时间了。很多开发者为了加快应用启动速度,都下了很大心血,包括按需延迟初始化第三方库。但在 LeakCanary 中,这个问题并不存在,因为它本身就是一个只在 debug 版本中使用的库,并不会对 release 版本有任何影响。


源码解析


前面提到了 InternalLeakSentry.install() 就是核心的初始化工作,其地位就和 1.5.4 版本中的 LeakCanary.install() 一样。下面就从 install() 方法开始,走进 LeakCanary 2.0 一探究竟。


1. LeakCanary.install()


fun install(application: Application) {
CanaryLog.d("Installing LeakSentry")
checkMainThread() // 只能在主线程调用,否则会抛出异常
if (this::application.isInitialized) {
return
}
InternalLeakSentry.application = application

val configProvider = { LeakSentry.config }
ActivityDestroyWatcher.install( // 监听 Activity.onDestroy(),见 1.1
application, refWatcher, configProvider
)
FragmentDestroyWatcher.install( // 监听 Fragment.onDestroy(),见 1.2
application, refWatcher, configProvider
)
listener.onLeakSentryInstalled(application) // 见 1.3
}

install() 方法主要做了三件事:



  • 注册 Activity.onDestroy() 监听

  • 注册 Fragment.onDestroy() 监听

  • 监听完成后进行一些初始化工作


依次看一看。


1.1 ActivityDestroyWatcher.install()


ActivityDestroyWatcher 类的源码很简单:


internal class ActivityDestroyWatcher private constructor(
private val refWatcher: RefWatcher,
private val configProvider: () -> Config
) {

private val lifecycleCallbacks = object : ActivityLifecycleCallbacksAdapter() {
override fun onActivityDestroyed(activity: Activity) {
if (configProvider().watchActivities) {
refWatcher.watch(activity) // 监听到 onDestroy() 之后,通过 refWatcher 监测 Activity
}
}
}

companion object {
fun install(
application: Application,
refWatcher: RefWatcher,
configProvider: ()
-> Config
) {
val activityDestroyWatcher =
ActivityDestroyWatcher(refWatcher, configProvider)
// 注册 Activity 生命周期监听
application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
}
}
}

install() 方法中注册了 Activity 生命周期监听,在监听到 onDestroy() 时,调用 RefWatcher.watch() 方法开始监测 Activity。


1.2 FragmentDestroyWatcher.install()


FragmentDestroyWatcher 是一个接口,它有两个实现类 AndroidOFragmentDestroyWatcherSupportFragmentDestroyWatcher


internal interface FragmentDestroyWatcher {

fun watchFragments(activity: Activity)

companion object {

private const val SUPPORT_FRAGMENT_CLASS_NAME = "androidx.fragment.app.Fragment"

fun install(
application: Application,
refWatcher: RefWatcher,
configProvider: ()
-> LeakSentry.Config
) {
val fragmentDestroyWatchers = mutableListOf<FragmentDestroyWatcher>()

if (SDK_INT >= O) { // >= 26,使用 AndroidOFragmentDestroyWatcher
fragmentDestroyWatchers.add(
AndroidOFragmentDestroyWatcher(refWatcher, configProvider)
)
}

if (classAvailable(
SUPPORT_FRAGMENT_CLASS_NAME
)
) {
fragmentDestroyWatchers.add( // androidx 使用 SupportFragmentDestroyWatcher
SupportFragmentDestroyWatcher(refWatcher, configProvider)
)
}

if (fragmentDestroyWatchers.size == 0) {
return
}

application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacksAdapter() {
override fun onActivityCreated(
activity: Activity,
savedInstanceState: Bundle?
)
{
for (watcher in fragmentDestroyWatchers) {
watcher.watchFragments(activity)
}
}
})
}

private fun classAvailable(className: String): Boolean {
return try {
Class.forName(className)
true
} catch (e: ClassNotFoundException) {
false
}
}
}
}

如果我没记错的话,1.5.4 是不监测 Fragment 的泄露的。而 2.0 版本提供了对 Android O 以及 androidx 版本中的 Fragment 的内存泄露检测。 AndroidOFragmentDestroyWatcherSupportFragmentDestroyWatcher 的实现代码其实是一致的,Android O 及以后,androidx 都具备对 Fragment 生命周期的监听功能。以 AndroidOFragmentDestroyWatcher 为例,简单看一下它的实现。


@RequiresApi(Build.VERSION_CODES.O) //
internal class AndroidOFragmentDestroyWatcher(
private val refWatcher: RefWatcher,
private val configProvider: () -> Config
) : FragmentDestroyWatcher {

private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {

override fun onFragmentViewDestroyed(
fm: FragmentManager,
fragment: Fragment
)
{
val view = fragment.view
if (view != null && configProvider().watchFragmentViews) {
refWatcher.watch(view)
}
}

override fun onFragmentDestroyed(
fm: FragmentManager,
fragment: Fragment
)
{
if (configProvider().watchFragments) {
refWatcher.watch(fragment)
}
}
}

override fun watchFragments(activity: Activity) {
val fragmentManager = activity.fragmentManager
fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
}
}

同样,还是使用 RefWatcher.watch() 方法来进行监测。


1.3 listener.onLeakSentryInstalled()


onLeakSentryInstalled() 回调中会初始化一些检测内存泄露过程中需要的对象,如下所示:


override fun onLeakSentryInstalled(application: Application) {
this.application = application

val heapDumper = AndroidHeapDumper(application, leakDirectoryProvider) // 用于 heap dump

val gcTrigger = GcTrigger.Default // 用于手动调用 GC

val configProvider = { LeakCanary.config } // 配置项

val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
handlerThread.start()
val backgroundHandler = Handler(handlerThread.looper) // 发起内存泄漏检测的线程

heapDumpTrigger = HeapDumpTrigger(
application, backgroundHandler, LeakSentry.refWatcher, gcTrigger, heapDumper, configProvider
)
application.registerVisibilityListener { applicationVisible ->
this.applicationVisible = applicationVisible
heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
}
addDynamicShortcut(application)
}

对老版本代码熟悉的同学,看到这些对象应该很熟悉。



  • heapDumper 用于确认内存泄漏之后进行 heap dump 工作。

  • gcTrigger 用于发现可能的内存泄漏之后手动调用 GC 确认是否真的为内存泄露。


这两个对象是 LeakCanary 检测内存泄漏的核心。后面会进行详细分析。


到这里,整个 LeakCanary 的初始化工作就完成了。与 1.5.4 版本不同的是,新版本增加了对 Fragment 以及 androidx 的支持。当发生 Activity.onDestroy()Fragment.onFragmentViewDestroyed() , Fragment.onFragmentDestroyed() 三者之一时,RefWatcher 就开始工作了,调用其 watch() 方法开始检测引用是否泄露。


2. RefWatcher.watch()


在看源码之前,我们先来看几个后面会使用到的队列。


  /**
* References passed to [watch] that haven't made it to [retainedReferences] yet.
* watch() 方法传进来的引用,尚未判定为泄露
*/

private val watchedReferences = mutableMapOf<String, KeyedWeakReference>()
/**
* References passed to [watch] that we have determined to be retained longer than they should
* have been.
* watch() 方法传进来的引用,已经被判定为泄露
*/

private val retainedReferences = mutableMapOf<String, KeyedWeakReference>()
private val queue = ReferenceQueue<Any>() // 引用队列,配合弱引用使用

通过 watch() 方法传入的引用都会保存在 watchedReferences 中,被判定泄露之后保存在 retainedReferences 中。注意,这里的判定过程不止会发生一次,已经进入队列 retainedReferences 的引用仍有可能被移除。queue 是一个 ReferenceQueue 引用队列,配合弱引用使用,这里记住一句话:



弱引用一旦变得弱可达,就会立即入队。这将在 finalization 或者 GC 之前发生。



也就是说,会被 GC 回收的对象引用,会保存在队列 queue 中。


回头再来看看 watch() 方法的源码。


  @Synchronized fun watch(
watchedReference: Any,
referenceName: String
)
{
if (!isEnabled()) {
return
}
removeWeaklyReachableReferences() // 移除队列中将要被 GC 的引用,见 2.1
val key = UUID.randomUUID()
.toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference = // 构建当前引用的弱引用对象,并关联引用队列 queue
KeyedWeakReference(watchedReference, key, referenceName, watchUptimeMillis, queue)
if (referenceName != "") {
CanaryLog.d(
"Watching instance of %s named %s with key %s", reference.className,
referenceName, key
)
} else {
CanaryLog.d(
"Watching instance of %s with key %s", reference.className, key
)
}

watchedReferences[key] = reference // 将引用存入 watchedReferences
checkRetainedExecutor.execute {
moveToRetained(key) // 如果当前引用未被移除,仍在 watchedReferences 队列中,
// 说明仍未被 GC,移入 retainedReferences 队列中,暂时标记为泄露
// 见 2.2
}
}

逻辑还是比较清晰的,首先会调用 removeWeaklyReachableReferences() 方法,这个方法在整个过程中会多次调用。其作用是移除 watchedReferences 中将被 GC 的引用。


2.1 removeWeaklyReachableReferences()


  private fun removeWeaklyReachableReferences() {
// WeakReferences are enqueued as soon as the object to which they point to becomes weakly
// reachable. This is before finalization or garbage collection has actually happened.
// 弱引用一旦变得弱可达,就会立即入队。这将在 finalization 或者 GC 之前发生。
var ref: KeyedWeakReference?
do {
ref = queue.poll() as KeyedWeakReference? // 队列 queue 中的对象都是会被 GC 的
if (ref != null) {
val removedRef = watchedReferences.remove(ref.key)
if (removedRef == null) {
retainedReferences.remove(ref.key)
}
// 移除 watchedReferences 队列中的会被 GC 的 ref 对象,剩下的就是可能泄露的对象
}
} while (ref != null)
}

整个过程中会多次调用,以确保将已经入队 queue 的将被 GC 的对象引用移除掉,避免无谓的 heap dump 操作。而仍在 watchedReferences 队列中的引用,则可能已经泄露,移到队列 retainedReferences 中,这就是 moveToRetained() 方法的逻辑。代码如下:


2.2 moveToRetained()


  @Synchronized private fun moveToRetained(key: String) {
removeWeaklyReachableReferences() // 再次调用,防止遗漏
val retainedRef = watchedReferences.remove(key)
if (retainedRef != null) {
retainedReferences[key] = retainedRef
onReferenceRetained()
}
}

这里的 onReferenceRetained() 最后会回调到 InternalLeakCanary.kt 中。


  override fun onReferenceRetained() {
if (this::heapDumpTrigger.isInitialized) {
heapDumpTrigger.onReferenceRetained()
}
}

调用了 HeapDumpTriggeronReferenceRetained() 方法。


  fun onReferenceRetained() {
scheduleRetainedInstanceCheck("found new instance retained")
}

private fun scheduleRetainedInstanceCheck(reason: String) {
if (checkScheduled) {
return
}
checkScheduled = true
backgroundHandler.post {
checkScheduled = false
checkRetainedInstances(reason) // 检测泄露实例
}
}

checkRetainedInstances() 方法是确定泄露的最后一个方法了。这里会确认引用是否真的泄露,如果真的泄露,则发起 heap dump,分析 dump 文件,找到引用链,最后通知用户。整体流程和老版本是一致的,但在一些细节处理,以及 dump 文件的分析上有所区别。下面还是通过源码来看看这些区别。


  private fun checkRetainedInstances(reason: String) {
CanaryLog.d("Checking retained instances because %s", reason)
val config = configProvider()
// A tick will be rescheduled when this is turned back on.
if (!config.dumpHeap) {
return
}

var retainedKeys = refWatcher.retainedKeys

// 当前泄露实例个数小于 5 个,不进行 heap dump
if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return

if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
showRetainedCountWithDebuggerAttached(retainedKeys.size)
scheduleRetainedInstanceCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)
CanaryLog.d(
"Not checking for leaks while the debugger is attached, will retry in %d ms",
WAIT_FOR_DEBUG_MILLIS
)
return
}

// 可能存在被观察的引用将要变得弱可达,但是还未入队引用队列。
// 这时候应该主动调用一次 GC,可能可以避免一次 heap dump
gcTrigger.runGc()

retainedKeys = refWatcher.retainedKeys

if (checkRetainedCount(retainedKeys, config.retainedVisibleThreshold)) return

HeapDumpMemoryStore.setRetainedKeysForHeapDump(retainedKeys)

CanaryLog.d("Found %d retained references, dumping the heap", retainedKeys.size)
HeapDumpMemoryStore.heapDumpUptimeMillis = SystemClock.uptimeMillis()
dismissNotification()
val heapDumpFile = heapDumper.dumpHeap() // AndroidHeapDumper
if (heapDumpFile == null) {
CanaryLog.d("Failed to dump heap, will retry in %d ms", WAIT_AFTER_DUMP_FAILED_MILLIS)
scheduleRetainedInstanceCheck("failed to dump heap", WAIT_AFTER_DUMP_FAILED_MILLIS)
showRetainedCountWithHeapDumpFailed(retainedKeys.size)
return
}

refWatcher.removeRetainedKeys(retainedKeys) // 移除已经 heap dump 的 retainedKeys

HeapAnalyzerService.runAnalysis(application, heapDumpFile) // 分析 heap dump 文件
}

首先调用 checkRetainedCount() 函数判断当前泄露实例个数如果小于 5 个,仅仅只是给用户一个通知,不会进行 heap dump 操作,并在 5s 后再次发起检测。这是和老版本一个不同的地方。


  private fun checkRetainedCount(
retainedKeys: Set<String>,
retainedVisibleThreshold: Int // 默认为 5 个
)
: Boolean {
if (retainedKeys.isEmpty()) {
CanaryLog.d("No retained instances")
dismissNotification()
return true
}

if (retainedKeys.size < retainedVisibleThreshold) {
if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
CanaryLog.d(
"Found %d retained instances, which is less than the visible threshold of %d",
retainedKeys.size,
retainedVisibleThreshold
)
// 通知用户 "App visible, waiting until 5 retained instances"
showRetainedCountBelowThresholdNotification(retainedKeys.size, retainedVisibleThreshold)
scheduleRetainedInstanceCheck( // 5s 后再次发起检测
"Showing retained instance notification", WAIT_FOR_INSTANCE_THRESHOLD_MILLIS
)
return true
}
}
return false
}

当集齐 5 个泄露实例之后,也并不会立马进行 heap dump。而是先手动调用一次 GC。当然不是使用 System.gc(),如下所示:


  object Default : GcTrigger {
override fun runGc() {
// Code taken from AOSP FinalizationTest:
// https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
// java/lang/ref/FinalizationTester.java
// System.gc() does not garbage collect every time. Runtime.gc() is
// more likely to perform a gc.
Runtime.getRuntime()
.gc()
enqueueReferences()
System.runFinalization()
}

那么,为什么要进行这次 GC 呢?可能存在被观察的引用将要变得弱可达,但是还未入队引用队列的情况。这时候应该主动调用一次 GC,可能可以避免一次额外的 heap dump 。GC 之后再次调用 checkRetainedCount() 判断泄露实例个数。如果此时仍然满足条件,就要发起 heap dump 操作了。具体逻辑在 AndroidHeapDumper.dumpHeap() 方法中,核心方法就是下面这句代码:


Debug.dumpHprofData(heapDumpFile.absolutePath)

生成 heap dump 文件之后,要删除已经处理过的引用,


refWatcher.removeRetainedKeys(retainedKeys)

最后启动一个前台服务 HeapAnalyzerService 来分析 heap dump 文件。老版本中是使用 Square 自己的 haha 库来解析的,这个库已经废弃了,Square 完全重写了解析库,主要逻辑都在 moudle leakcanary-analyzer 中。这部分我还没有阅读,就不在这里分析了。对于新的解析器,官网是这样介绍的:



Uses 90% less memory and 6 times faster than the prior heap parser.



减少了 90% 的内存占用,而且比原来快了 6 倍。后面有时间单独来分析一下这个解析库。


后面的过程就不再赘述了,通过解析库找到最短 GC Roots 引用路径,然后展示给用户。


总结


通读完源码,LeakCanary 2 还是带来了很多的优化。与老版本相比,主要有以下不同:



  • 百分之百使用 Kotlin 重写

  • 自动初始化,无需用户手动再添加初始化代码

  • 支持 fragment,支持 androidx

  • 当泄露引用到达 5 个时才会发起 heap dump

  • 全新的 heap parser,减少 90% 内存占用,提升 6 倍速度



作者:秉心说TM
链接:https://juejin.cn/post/6844903876043210759
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


收起阅读 »

利用tess-two和cv4j实现简单的ocr功能

ocr
ocr 光学字符识别(英语:Optical Character Recognition, OCR)是指对文本资料的图像文件进行分析识别处理,获取文字及版面信息的过程。 Tesseract Tesseract是Ray Smith于1985到1995年间在惠普...
继续阅读 »

ocr



光学字符识别(英语:Optical Character Recognition, OCR)是指对文本资料的图像文件进行分析识别处理,获取文字及版面信息的过程。



Tesseract


Tesseract是Ray Smith于1985到1995年间在惠普布里斯托实验室开发的一个OCR引擎,曾经在1995 UNLV精确度测试中名列前茅。但1996年后基本停止了开发。2006年,Google邀请Smith加盟,重启该项目。目前项目的许可证是Apache 2.0。该项目目前支持Windows、Linux和Mac OS等主流平台。但作为一个引擎,它只提供命令行工具。 现阶段的Tesseract由Google负责维护,是最好的开源OCR Engine之一,并且支持中文。


tess-two是Tesseract在Android平台上的移植。


下载tess-two:


compile 'com.rmtheis:tess-two:8.0.0'

然后将训练好的eng.traineddata放入android项目的assets文件夹中,就可以识别英文了。


1. 简单地识别英文


初始化tess-two,加载训练好的tessdata


    private void prepareTesseract() {
try {
prepareDirectory(DATA_PATH + TESSDATA);
} catch (Exception e) {
e.printStackTrace();
}

copyTessDataFiles(TESSDATA);
}

/**
* Prepare directory on external storage
*
* @param path
* @throws Exception
*/
private void prepareDirectory(String path) {

File dir = new File(path);
if (!dir.exists()) {
if (!dir.mkdirs()) {
Log.e(TAG, "ERROR: Creation of directory " + path + " failed, check does Android Manifest have permission to write to external storage.");
}
} else {
Log.i(TAG, "Created directory " + path);
}
}

/**
* Copy tessdata files (located on assets/tessdata) to destination directory
*
* @param path - name of directory with .traineddata files
*/
private void copyTessDataFiles(String path) {
try {
String fileList[] = getAssets().list(path);

for (String fileName : fileList) {

// open file within the assets folder
// if it is not already there copy it to the sdcard
String pathToDataFile = DATA_PATH + path + "/" + fileName;
if (!(new File(pathToDataFile)).exists()) {

InputStream in = getAssets().open(path + "/" + fileName);

OutputStream out = new FileOutputStream(pathToDataFile);

// Transfer bytes from in to out
byte[] buf = new byte[1024];
int len;

while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
out.close();

Log.d(TAG, "Copied " + fileName + "to tessdata");
}
}
} catch (IOException e) {
Log.e(TAG, "Unable to copy files to tessdata " + e.toString());
}
}



拍完照后,调用startOCR方法。

    private void startOCR(Uri imgUri) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4; // 1 - means max size. 4 - means maxsize/4 size. Don't use value <4, because you need more memory in the heap to store your data.
Bitmap bitmap = BitmapFactory.decodeFile(imgUri.getPath(), options);

String result = extractText(bitmap);
resultView.setText(result);

} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
}

extractText()会调用tess-two的api来实现ocr文字识别。


    private String extractText(Bitmap bitmap) {
try {
tessBaseApi = new TessBaseAPI();
} catch (Exception e) {
Log.e(TAG, e.getMessage());
if (tessBaseApi == null) {
Log.e(TAG, "TessBaseAPI is null. TessFactory not returning tess object.");
}
}

tessBaseApi.init(DATA_PATH, lang);

tessBaseApi.setImage(bitmap);
String extractedText = "empty result";
try {
extractedText = tessBaseApi.getUTF8Text();
} catch (Exception e) {
Log.e(TAG, "Error in recognizing text.");
}
tessBaseApi.end();
return extractedText;
}

最后,显示识别的效果,此时的效果还算可以。






2. 识别代码

接下来,尝试用上面的程序识别一段代码。





此时,效果一塌糊涂。我们重构一下startOCR(),增加局部的二值化处理。

    private void startOCR(Uri imgUri) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4; // 1 - means max size. 4 - means maxsize/4 size. Don't use value <4, because you need more memory in the heap to store your data.
Bitmap bitmap = BitmapFactory.decodeFile(imgUri.getPath(), options);

CV4JImage cv4JImage = new CV4JImage(bitmap);
Threshold threshold = new Threshold();
threshold.adaptiveThresh((ByteProcessor)(cv4JImage.convert2Gray().getProcessor()), Threshold.ADAPTIVE_C_MEANS_THRESH, 12, 30, Threshold.METHOD_THRESH_BINARY);
Bitmap newBitmap = cv4JImage.getProcessor().getImage().toBitmap(Bitmap.Config.ARGB_8888);

ivImage2.setImageBitmap(newBitmap);

String result = extractText(newBitmap);
resultView.setText(result);

} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
}

在这里,使用cv4j来实现图像的二值化处理。


            CV4JImage cv4JImage = new CV4JImage(bitmap);
Threshold threshold = new Threshold();
threshold.adaptiveThresh((ByteProcessor)(cv4JImage.convert2Gray().getProcessor()), Threshold.ADAPTIVE_C_MEANS_THRESH, 12, 30, Threshold.METHOD_THRESH_BINARY);
Bitmap newBitmap = cv4JImage.getProcessor().getImage().toBitmap(Bitmap.Config.ARGB_8888);


图像二值化就是将图像上的像素点的灰度值设置为0或255,也就是将整个图像呈现出明显的黑白效果。图像的二值化有利于图像的进一步处理,使图像变得简单,而且数据量减小,能凸显出感兴趣的目标的轮廓。



cv4j的github地址:https://github.com/imageprocessor/cv4j



cv4jgloomyfish和我一起开发的图像处理库,纯java实现。



再来试试效果,图片中间部分是二值化后的效果,此时基本能识别出代码的内容。





3. 识别中文

如果要识别中文字体,需要使用中文的数据包。可以去下面的网站上下载。


https://github.com/tesseract-ocr/tessdata


跟中文相关的数据包有chi_sim.traineddata、chi_tra.traineddata,它们分别表示是简体中文和繁体中文。


tessBaseApi.init(DATA_PATH, lang);

前面的例子都是识别英文的,所以原先的lang值为"eng",现在要识别简体中文的话需要将其值改为"chi_sim"。



最后

本项目只是demo级别的演示,离生产环境的使用还差的很远。

本项目的github地址:https://github.com/fengzhizi715/Tess-TwoDemo


为何说只是demo级别呢?



  • 数据包很大,特别是中文的大概有50多M,放在移动端的肯定不合适。一般正确的做法,都是放在云端。

  • 识别文字很慢,特别是中文,工程上还有很多优化的空间。

  • 做ocr之前需要做很多预处理的工作,在本例子中只用了二值化,其实还有很多预处理的步骤比如倾斜校正、字符切割等等。

  • 为了提高tess-two的识别率,可以自己训练数据集。




作者:fengzhizi715
链接:https://www.jianshu.com/p/5314f63c75d8
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

功能强大的升级库

CheckVersionLib V2版震撼来袭,功能强大,链式编程,调用简单,集成轻松,扩展性强大老规矩先看V2效果,这个版本最大的特点就是使用非常简单,相对于1.+版本 效果 特点 任何地方都可以调用 简单简单简单简单(重要的话我说四遍) 扩...
继续阅读 »

CheckVersionLib


V2版震撼来袭,功能强大,链式编程,调用简单,集成轻松,扩展性强大

老规矩先看V2效果,这个版本最大的特点就是使用非常简单,相对于1.+版本


效果

V2.gif



特点



  • 任何地方都可以调用




  • 简单简单简单简单(重要的话我说四遍)




  • 扩展性强大




  • 所有具有升级功能的app均可使用,耶稣说的




  • 更强大的自定义界面支持




  • 支持强制更新(一行代码)




  • 支持静默下载 (一行代码)




  • 适配到Android Q




导入

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


appcompat

  implementation 'com.github.AlexLiuSheng:CheckVersionLib:2.4.1_appcompat'


jitpack && android x

dependencies {
implementation 'com.github.AlexLiuSheng:CheckVersionLib:2.4.1_androidx'
}


使用

和1.+版本一样,两种模式


只使用下载模式


先来个最简单的调用

        AllenVersionChecker
.getInstance()
.downloadOnly(
UIData.create().setDownloadUrl(downloadUrl)
)
.executeMission(context);

UIData:UIData是一个Bundle,用于存放用于UI展示的一些数据,后面自定义界面时候可以拿来用


请求服务器版本+下载


该模式最简单的使用

   AllenVersionChecker
.getInstance()
.requestVersion()
.setRequestUrl(requestUrl)
.request(new RequestVersionListener() {
@Nullable
@Override
public UIData onRequestVersionSuccess(String result) {
//拿到服务器返回的数据,解析,拿到downloadUrl和一些其他的UI数据
...
//如果是最新版本直接return null
return UIData.create().setDownloadUrl(downloadUrl);
}

@Override
public void onRequestVersionFailure(String message) {

}
})
.executeMission(context);

请求版本一些其他的http参数可以设置,如下

 AllenVersionChecker
.getInstance()
.requestVersion()
.setHttpHeaders(httpHeader)
.setRequestMethod(HttpRequestMethod.POSTJSON)
.setRequestParams(httpParam)
.setRequestUrl(requestUrl)
.request(new RequestVersionListener() {
@Nullable
@Override
public UIData onRequestVersionSuccess(String result) {
//拿到服务器返回的数据,解析,拿到downloadUrl和一些其他的UI数据
...
UIData uiData = UIData
.create()
.setDownloadUrl(downloadUrl)
.setTitle(updateTitle)
.setContent(updateContent);
//放一些其他的UI参数,拿到后面自定义界面使用
uiData.getVersionBundle().putString("key", "your value");
return uiData;

}

@Override
public void onRequestVersionFailure(String message) {

}
})
.executeMission(context);


合适的地方关闭任务

为了避免不必要的内存泄漏,需要在合适的地方取消任务

AllenVersionChecker.getInstance().cancelAllMission();

以上就是最基本的使用(库默认会有一套界面),如果还不满足项目需求,下面就可以用这个库来飙车了


一些其他的function设置

解释下,下面的builder叫DownloadBuilder

 DownloadBuilder builder=AllenVersionChecker
.getInstance()
.downloadOnly();


or



DownloadBuilder builder=AllenVersionChecker
.getInstance()
.requestVersion()
.request()

取消任务


 AllenVersionChecker.getInstance().cancelAllMission(this);

静默下载


 builder.setSilentDownload(true); 默认false

设置当前服务器最新的版本号,供库判断是否使用缓存



  • 缓存策略:如果本地有安装包,首先判断与当前运行的程序的versionCode是否不一致,然后判断是否有传入最新的
    versionCode,如果传入的versionCode大于本地的,重新从服务器下载,否则使用缓存

 builder.setNewestVersionCode(int); 默认null

强制更新


设置此listener即代表需要强制更新,会在用户想要取消下载的时候回调
需要你自己关闭所有界面

builder.setForceUpdateListener(() -> {
forceUpdate();
});

update in v2.2.1
动态设置是否强制更新,如果使用本库来请求服务器,可以在回调时动态设置一些参数或者回调

   public UIData onRequestVersionSuccess(DownloadBuilder downloadBuilder,String result) {
downloadBuilder.setForceUpdateListener(() -> {
forceUpdate();
});
Toast.makeText(V2Activity.this, "request successful", Toast.LENGTH_SHORT).show();
return crateUIData();
}

下载忽略本地缓存


如果本地有安装包缓存也会重新下载apk

 builder.setForceRedownload(true); 默认false

是否显示下载对话框


builder.setShowDownloadingDialog(false); 默认true

是否显示通知栏


builder.setShowNotification(false);  默认true

以前台service运行(update in 2.2.2)
推荐以前台服务运行更新,防止在后台时,服务被杀死


builder.setRunOnForegroundService(true); 默认true

自定义通知栏


      builder.setNotificationBuilder(
NotificationBuilder.create()
.setRingtone(true)
.setIcon(R.mipmap.dialog4)
.setTicker("custom_ticker")
.setContentTitle("custom title")
.setContentText(getString(R.string.custom_content_text))
);

是否显示失败对话框


  builder.setShowDownloadFailDialog(false); 默认true

自定义下载路径


  builder.setDownloadAPKPath(address); 默认:/storage/emulated/0/AllenVersionPath/

自定义下载文件名


  builder.setApkName(apkName); 默认:getPackageName()

可以设置下载监听


   builder.setApkDownloadListener(new APKDownloadListener() {
@Override
public void onDownloading(int progress) {

}

@Override
public void onDownloadSuccess(File file) {

}

@Override
public void onDownloadFail() {

}
});

设置取消监听
此回调会监听所有cancel事件


 
builder.setOnCancelListener(() -> {
Toast.makeText(V2Activity.this,"Cancel Hanlde",Toast.LENGTH_SHORT).show();
});

如果想单独监听几种状态下的cancel,可像如下这样设置


  • builder.setDownloadingCancelListener();

  • builder.setDownloadFailedCancelListener();

  • builder.setReadyDownloadCancelListener();


设置确定监听(added after 2.2.2)



  • builder.setReadyDownloadCommitClickListener();

  • builder.setDownloadFailedCommitClickListener();


静默下载+直接安装(不会弹出升级对话框)


    builder.setDirectDownload(true);
builder.setShowNotification(false);
builder.setShowDownloadingDialog(false);
builder.setShowDownloadFailDialog(false);

自定义安装回调


    setCustomDownloadInstallListener(CustomInstallListener customDownloadInstallListener)


自定义界面

自定义界面使用回调方式,开发者需要返回自己定义的Dialog(父类android.app)



  • 所有自定义的界面必须使用listener里面的context实例化




  • 界面展示的数据通过UIData拿




自定义显示更新界面


设置CustomVersionDialogListener



  • 定义此界面必须有一个确定下载的按钮,按钮id必须为@id/versionchecklib_version_dialog_commit




  • 如果有取消按钮(没有忽略本条要求),则按钮id必须为@id/versionchecklib_version_dialog_cancel



eg.

  builder.setCustomVersionDialogListener((context, versionBundle) -> {
BaseDialog baseDialog = new BaseDialog(context, R.style.BaseDialog, R.layout.custom_dialog_one_layout);
//versionBundle 就是UIData,之前开发者传入的,在这里可以拿出UI数据并展示
TextView textView = baseDialog.findViewById(R.id.tv_msg);
textView.setText(versionBundle.getContent());
return baseDialog;
});

自定义下载中对话框界面


设置CustomDownloadingDialogListener


  • 如果此界面要设计取消操作(没有忽略),请务必将id设置为@id/versionchecklib_loading_dialog_cancel

    builder.setCustomDownloadingDialogListener(new CustomDownloadingDialogListener() {
@Override
public Dialog getCustomDownloadingDialog(Context context, int progress, UIData versionBundle) {
BaseDialog baseDialog = new BaseDialog(context, R.style.BaseDialog, R.layout.custom_download_layout);
return baseDialog;
}
//下载中会不断回调updateUI方法
@Override
public void updateUI(Dialog dialog, int progress, UIData versionBundle) {
TextView tvProgress = dialog.findViewById(R.id.tv_progress);
ProgressBar progressBar = dialog.findViewById(R.id.pb);
progressBar.setProgress(progress);
tvProgress.setText(getString(R.string.versionchecklib_progress, progress));
}
});

自定义下载失败对话框


设置CustomDownloadFailedListener



  • 如果有重试按钮请将id设置为@id/versionchecklib_failed_dialog_retry




  • 如果有 确认/取消按钮请将id设置为@id/versionchecklib_failed_dialog_cancel



   builder.setCustomDownloadFailedListener((context, versionBundle) -> {
BaseDialog baseDialog = new BaseDialog(context, R.style.BaseDialog, R.layout.custom_download_failed_dialog);
return baseDialog;
});


update Log


  • 2.2.1

    • 修复内存泄漏问题

    • 使用binder传递参数

    • 一些已知的bug




混淆配置

 -keepattributes *Annotation*
-keepclassmembers class * {
@org.greenrobot.eventbus.Subscribe ;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }

# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
(java.lang.Throwable);
}

git地址:https://github.com/AlexLiuSheng/CheckVersionLib

下载地址:CheckVersionLib-master.zip

收起阅读 »

优秀优秀,Android图片涂鸦库

DoodleImage doodle for Android. You can undo, zoom, move, add text, textures, etc. Also, a powerful, customizable and extensible d...
继续阅读 »

Doodle


Image doodle for Android. You can undo, zoom, move, add text, textures, etc. Also, a powerful, customizable and extensible doodle framework & multi-function drawing board.

Android图片涂鸦,具有撤消、缩放、移动、添加文字,贴图等功能。还是一个功能强大,可自定义和可扩展的涂鸦框架、多功能画板。

01.gif

01
02
03


Feature 特性



  • Brush and shape 画笔及形状


    The brush can choose hand-painted, mosaic, imitation, eraser, text, texture, and the imitation function is similar to that in PS, copying somewhere in the picture. Shapes can be selected from hand-drawn, arrows, lines, circles, rectangles, and so on. The background color of the brush can be selected as a color, or an image.


    画笔可以选择手绘、马赛克、仿制、橡皮擦、文字、贴图,其中仿制功能跟PS中的类似,复制图片中的某处地方。形状可以选择手绘、箭头、直线、圆、矩形等。画笔的底色可以选择颜色,或者一张图片。




  • Undo/Redo 撤销/重做


    Each step of the doodle operation can be undone or redone.


    每一步的涂鸦操作都可以撤销。




  • Zoom, move, and rotate 放缩、移动及旋转


    In the process of doodle, you can freely zoom, move and rotate the picture with gestures. Also, you can move,rotate and scale the doodle item.


    在涂鸦的过程中,可以自由地通过手势缩放、移动、旋转图片。可对涂鸦移动、旋转、缩放等。




  • Zoomer 放大器


    In order to doodle more finely, an zoomer can be set up during the doodle process.


    为了更细微地涂鸦,涂鸦过程中可以设置出现放大器。




Usage 用法


Gradle

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

dependencies {
compile 'com.github.1993hzw:Doodle:5.5.3'
}

There are two ways to use the Doodle library:

这里有两种方式使用Doodle涂鸦库


A. Launch DoodleActivity directly (the layout is like demo images above). If you need to customize more interactions, please use another method (Way B).

使用写好的涂鸦界面,直接启动.启动的页面可参看上面的演示图片。如果需要自定义更多的交互方式,则请使用另一种方式(即B方式)。

DoodleParams params = new DoodleParams(); // 涂鸦参数
params.mImagePath = imagePath; // the file path of image
DoodleActivity.startActivityForResult(MainActivity.this, params, REQ_CODE_DOODLE);

See DoodleParams for more details.

查看DoodleParams获取更多涂鸦参数信息。


B. Recommend, use DoodleView and customize your layout.

推荐的方法:使用DoodleView,便于拓展,灵活性高,自定义自己的交互界面.

/*
Whether or not to optimize drawing, it is suggested to open, which can optimize the drawing speed and performance.
Note: When item is selected for editing after opening, it will be drawn at the top level, and not at the corresponding level until editing is completed.
是否优化绘制,建议开启,可优化绘制速度和性能.
注意:开启后item被选中编辑时时会绘制在最上面一层,直到结束编辑后才绘制在相应层级
*/
boolean optimizeDrawing = true;
DoodleView mDoodleView = new DoodleView(this, bitmap, optimizeDrawing, new IDoodleListener() {
/*
called when save the doodled iamge.
保存涂鸦图像时调用
*/
@Override
public void onSaved(IDoodle doodle, Bitmap bitmap, Runnable callback) {
//do something
}

/*
called when it is ready to doodle because the view has been measured. Now, you can set size, color, pen, shape, etc.
此时view已经测量完成,涂鸦前的准备工作已经完成,在这里可以设置大小、颜色、画笔、形状等。
*/
@Override
public void onReady(IDoodle doodle) {
//do something
}
});

mTouchGestureListener = new DoodleOnTouchGestureListener(mDoodleView, new DoodleOnTouchGestureListener.ISelectionListener() {
/*
called when the item(such as text, texture) is selected/unselected.
item(如文字,贴图)被选中或取消选中时回调
*/
@Override
public void onSelectedItem(IDoodle doodle, IDoodleSelectableItem selectableItem, boolean selected) {
//do something
}

/*
called when you click the view to create a item(such as text, texture).
点击View中的某个点创建可选择的item(如文字,贴图)时回调
*/
@Override
public void onCreateSelectableItem(IDoodle doodle, float x, float y) {
//do something
/*
if (mDoodleView.getPen() == DoodlePen.TEXT) {
IDoodleSelectableItem item = new DoodleText(mDoodleView, "hello", 20 * mDoodleView.getUnitSize(), new DoodleColor(Color.RED), x, y);
mDoodleView.addItem(item);
} else if (mDoodleView.getPen() == DoodlePen.BITMAP) {
IDoodleSelectableItem item = new DoodleBitmap(mDoodleView, bitmap, 80 * mDoodle.getUnitSize(), x, y);
mDoodleView.addItem(item);
}
*/
}
});

// create touch detector, which dectects the gesture of scoll, scale, single tap, etc.
// 创建手势识别器,识别滚动,缩放,点击等手势
IDoodleTouchDetector detector = new DoodleTouchDetector(getApplicationContext(), mTouchGestureListener);
mDoodleView.setDefaultTouchDetector(detector);

// Setting parameters.设置参数
mDoodleView.setPen(DoodlePen.TEXT);
mDoodleView.setShape(DoodleShape.HAND_WRITE);
mDoodleView.setColor(new DoodleColor(Color.RED));

When turning off optimized drawing, you only need to call addItem(IDoodleItem) when you create it. When you start optimizing drawing, the created or selected item needs to call markItemToOptimizeDrawing(IDoodleItem), and you should call notifyItemFinishedDrawing(IDoodleItem) when you finish drawing. So this is generally used in code:

当关闭优化绘制时,只需要在创建时调用addItem(IDoodleItem);而当开启优化绘制时,创建或选中的item需要调用markItemToOptimizeDrawing(IDoodleItem),结束绘制时应调用notifyItemFinishedDrawing(IDoodleItem)。因此在代码中一般这样使用:

// when you are creating a item or selecting a item to edit
if (mDoodle.isOptimizeDrawing()) {
mDoodle.markItemToOptimizeDrawing(item);
} else {
mDoodle.addItem(item);
}

...

// finish creating or editting
if (mDoodle.isOptimizeDrawing()) {
mDoodle.notifyItemFinishedDrawing(item);
}

Then, add the DoodleView to your layout. Now you can start doodling freely.

把DoodleView添加到布局中,然后开始涂鸦。


Demo 实例

Here are other simple examples to teach you how to use the doodle framework.



  1. Mosaic effect
    马赛克效果




  2. Change text's size by scale gesture
    手势缩放文本大小



More...

Now I think you should know that DoodleActivity has used DoodleView. You also can customize your layout like DoodleActivity. See DoodleActivity for more details.

现在你应该知道DoodleActivity就是使用了DoodleView实现涂鸦,你可以参照DoodleActivity是怎么实现涂鸦界面的交互来实现自己的自定义页面。

DoodleView has implemented IDoodle.

DoodleView实现了IDoodle接口。

public interface IDoodle {
...
public float getUnitSize();
public void setDoodleRotation(int degree);
public void setDoodleScale(float scale, float pivotX, float pivotY);
public void setPen(IDoodlePen pen);
public void setShape(IDoodleShape shape);
public void setDoodleTranslation(float transX, float transY);
public void setSize(float paintSize);
public void setColor(IDoodleColor color);
public void addItem(IDoodleItem doodleItem);
public void removeItem(IDoodleItem doodleItem);
public void save();
public void topItem(IDoodleItem item);
public void bottomItem(IDoodleItem item);
public boolean undo(int step);
...
}


Framework diagram 框架图

structure


Doodle Coordinate 涂鸦坐标

coordinate


Extend 拓展

You can create a customized item like DoodlePath, DoodleText, DoodleBitmap which extend DoodleItemBase or implement IDoodleItem.

实现IDoodleItem接口或基础DoodleItemBase,用于创建自定义涂鸦条目item,比如DoodlePath, DoodleText, DoodleBitmap

You can create a customized pen like DoodlePen which implements IDoodlePen.

实现IDoodlePen接口用于创建自定义画笔pen,比如DoodlePen

You can create a customized shape like DoodleShape which implements IDoodleShape.

实现IDoodleShape接口用于创建自定义形状shape,比如DoodleShape

You can create a customized color like DoodleColor which implements IDoodleColor.

实现IDoodleColor接口用于创建自定义颜色color,比如DoodleColor

You can create a customized touch gesture detector like DoodleTouchDetector(GestureListener) which implements IDoodleTouchDetector.

实现IDoodleTouchDetector接口用于创建自定义手势识别器,比如DoodleTouchDetector


git地址:https://github.com/1993hzw/doodle

下载地址:doodle-master.zip

收起阅读 »

Android tess_two Android图片文字识别

ocr
先看效果图 我主要是识别截图,所以图片比较规范,识别率应该很高。 简介什么都不说了,直接看简单的用法吧 首先肯定是引入依赖了 dependencies { compile 'com.rmtheis:tess-two:6.2.0' } 简单的用法...
继续阅读 »


先看效果图


我主要是识别截图,所以图片比较规范,识别率应该很高。


简介什么都不说了,直接看简单的用法吧


首先肯定是引入依赖了


dependencies {
compile 'com.rmtheis:tess-two:6.2.0'
}

简单的用法其实就几行代码:


TessBaseAPI tessBaseAPI = new TessBaseAPI();
tessBaseAPI.init(DATAPATH, DEFAULT_LANGUAGE);//参数后面有说明。
tessBaseAPI.setImage(bitmap);
String text = tessBaseAPI.getUTF8Text();

就这样简单的把一个bitmap设置进去,就能识别到里面的文字并输出了。
但是真正用的时候还是遇到了点麻烦,虽然只是简单的识别。
主要是tessBaseAPI.init(DATAPATH, DEFAULT_LANGUAGE)这个方法容易出错。
先看一下这个方法的源码吧:


public boolean init(String datapath, String language) {
return init(datapath, language, OEM_DEFAULT);
}
/**
* Initializes the Tesseract engine with the specified language model(s). Returns
* true on success.
*
* @see #init(String, String)
*
* @param datapath the parent directory of tessdata ending in a forward
* slash
* @param language an ISO 639-3 string representing the language(s)
* @param ocrEngineMode the OCR engine mode to be set
* @return true on success
*/

public boolean init(String datapath, String language, int ocrEngineMode) {
if (datapath == null)
throw new IllegalArgumentException("Data path must not be null!");
if (!datapath.endsWith(File.separator))
datapath += File.separator;

File datapathFile = new File(datapath);
if (!datapathFile.exists())
throw new IllegalArgumentException("Data path does not exist!");

File tessdata = new File(datapath + "tessdata");
if (!tessdata.exists() || !tessdata.isDirectory())
throw new IllegalArgumentException("Data path must contain subfolder tessdata!");

//noinspection deprecation
if (ocrEngineMode != OEM_CUBE_ONLY) {
for (String languageCode : language.split("\\+")) {
if (!languageCode.startsWith("~")) {
File datafile = new File(tessdata + File.separator +
languageCode + ".traineddata");
if (!datafile.exists())
throw new IllegalArgumentException("Data file not found at " + datafile);
}
}
}

boolean success = nativeInitOem(mNativeData, datapath, language, ocrEngineMode);

if (success) {
mRecycled = false;
}

return success;
}

注意


从下面的方法中抛出的几个异常可以看出来,初始化的时候,第一个参数是个文件夹,而且这个文件夹中必须有一个tessdata的文件夹;而且这个文件夹中要有个文件叫做 第二个参数.traineddata 。具体的可以看下面代码里的注释。这些文件夹和文件没有的一定要创建好,不然会报错。


第二个参数.traineddata 是个什么文件呢?
这个是识别用到的语言库还是文字库什么的,按那个初始化方法的意思是哟啊放到SD卡中的。可以在下面的地址下载。我的demo里把这个文件放在了assets中,启动的时候复制到内存卡里。
https://github.com/tesseract-ocr/tessdata


chi_sim.traineddata应该是健体中文吧,我用的是这个。中英文都能识别。


代码


下面是主要代码:


import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.googlecode.tesseract.android.TessBaseAPI;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";
private Button btn;
private TextView tv;

/**
* TessBaseAPI初始化用到的第一个参数,是个目录。
*/

private static final String DATAPATH = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator;
/**
* 在DATAPATH中新建这个目录,TessBaseAPI初始化要求必须有这个目录。
*/

private static final String tessdata = DATAPATH + File.separator + "tessdata";
/**
* TessBaseAPI初始化测第二个参数,就是识别库的名字不要后缀名。
*/

private static final String DEFAULT_LANGUAGE = "chi_sim";
/**
* assets中的文件名
*/

private static final String DEFAULT_LANGUAGE_NAME = DEFAULT_LANGUAGE + ".traineddata";
/**
* 保存到SD卡中的完整文件名
*/

private static final String LANGUAGE_PATH = tessdata + File.separator + DEFAULT_LANGUAGE_NAME;

/**
* 权限请求值
*/

private static final int PERMISSION_REQUEST_CODE=0;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn = (Button) findViewById(R.id.btn);
tv = (TextView) findViewById(R.id.tv);

if (Build.VERSION.SDK_INT >= 23) {
if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
}
}

//Android6.0之前安装时就能复制,6.0之后要先请求权限,所以6.0以上的这个方法无用。
copyToSD(LANGUAGE_PATH, DEFAULT_LANGUAGE_NAME);

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
Log.i(TAG, "run: kaishi " + System.currentTimeMillis());

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.quanbu);
Log.i(TAG, "run: bitmap " + System.currentTimeMillis());

TessBaseAPI tessBaseAPI = new TessBaseAPI();

tessBaseAPI.init(DATAPATH, DEFAULT_LANGUAGE);

tessBaseAPI.setImage(bitmap);
final String text = tessBaseAPI.getUTF8Text();
Log.i(TAG, "run: text " + System.currentTimeMillis() + text);
runOnUiThread(new Runnable() {
@Override
public void run() {
tv.setText(text);
}
});

tessBaseAPI.end();
}
}).start();
}
});

}

/**
* 将assets中的识别库复制到SD卡中
* @param path 要存放在SD卡中的 完整的文件名。这里是"/storage/emulated/0//tessdata/chi_sim.traineddata"
* @param name assets中的文件名 这里是 "chi_sim.traineddata"
*/

public void copyToSD(String path, String name) {
Log.i(TAG, "copyToSD: "+path);
Log.i(TAG, "copyToSD: "+name);

//如果存在就删掉
File f = new File(path);
if (f.exists()){
f.delete();
}
if (!f.exists()){
File p = new File(f.getParent());
if (!p.exists()){
p.mkdirs();
}
try {
f.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}

InputStream is=null;
OutputStream os=null;
try {
is = this.getAssets().open(name);
File file = new File(path);
os = new FileOutputStream(file);
byte[] bytes = new byte[2048];
int len = 0;
while ((len = is.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
os.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null)
is.close();
if (os != null)
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}

/**
* 请求到权限后在这里复制识别库
* @param requestCode
* @param permissions
* @param grantResults
*/

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Log.i(TAG, "onRequestPermissionsResult: "+grantResults[0]);
switch (requestCode){
case PERMISSION_REQUEST_CODE:
if (grantResults.length>0&&grantResults[0]==PackageManager.PERMISSION_GRANTED){
Log.i(TAG, "onRequestPermissionsResult: copy");
copyToSD(LANGUAGE_PATH, DEFAULT_LANGUAGE_NAME);
}
break;
default:
break;
}
}
}



GitHub:https://github.com/rmtheis/tess-two

Demo的GitHub地址:https://github.com/wangyisll/TessTwoDemo

下载地址:tess-two

收起阅读 »

View系列:事件分发(二)

滑动冲突常见场景:内外层滑动方向不一致(如:ViewPager中嵌套竖向滑动的RecyclerView)内外层滑动方向一致(如:RecyclerView嵌套)一般从2个角度出发:父View自己主动拦截,或子View申请父View进行拦截父View事件发送方,父...
继续阅读 »

滑动冲突

常见场景:

  1. 内外层滑动方向不一致(如:ViewPager中嵌套竖向滑动的RecyclerView)
  2. 内外层滑动方向一致(如:RecyclerView嵌套)

image-20210602150942026

一般从2个角度出发:父View自己主动拦截,或子View申请父View进行拦截

父View

事件发送方,父View拦截。

父View根据自己的需求,选择在何时给onInterceptTouchEvent返回true,使事件直接分发给自己处理(前提:子View未设置requestDisallowInteceptTouchEvent(true),否则根本就不会经过onInterceptTouchEvent方法)。

  • DOWN不要拦截,否则根据事件分发逻辑,事件直接给父View自己处理了
  • UP不要拦截,否则子View无法出发click事件,无法移除longClick消息
  • 在MOVE中根据逻辑需求判断是否拦截
    public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (满足父容器的拦截要求) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
}
return intercepted;
}

子View

事件接收方,内部拦截

事件已经传递到子View,子View只有选择是否消费该事件,或者向父View申请拦截事件。

注意:申请拦截事件,不代表就以后就收不到事件了。request只是会清除FLAG_DISALLOW_INTERCEPT标记,导致父View检查onInterceptTouchEvent方法,仅此而已(恢复到默认状态)。主要看父View.onInterceptTouchEvent中的返回值。

    public boolean dispatchTouchEvent(MotionEvent event) {//或 onTouchEvent
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);//不许拦截
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);//申请拦截
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
}
return super.dispatchTouchEvent(event);
}

:cry:多点触控

安卓自定义View进阶-多点触控详解

自由地对图片进行缩放和移动

多点触控相关的事件:

事件简介
ACTION_DOWN第一个 手指 初次接触到屏幕 时触发。
ACTION_MOVE手指 在屏幕上滑动 时触发,会多次触发(单个或多个手指)。
ACTION_UP最后一个 手指 离开屏幕时触发。
ACTION_POINTER_DOWN有非主要的手指按下(即按下之前已经有手指在屏幕上)。
ACTION_POINTER_UP有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)。
以下事件类型不推荐使用---以下事件在2.0开始,在 2.2 版本以上被废弃---
ACTION_POINTER_1_DOWN第 2 个手指按下,已废弃,不推荐使用。
ACTION_POINTER_2_DOWN第 3 个手指按下,已废弃,不推荐使用。
ACTION_POINTER_3_DOWN第 4 个手指按下,已废弃,不推荐使用。
ACTION_POINTER_1_UP第 2 个手指抬起,已废弃,不推荐使用。
ACTION_POINTER_2_UP第 3 个手指抬起,已废弃,不推荐使用。
ACTION_POINTER_3_UP第 4 个手指抬起,已废弃,不推荐使用。

多点触控相关的方法:

方法简介
getActionMasked()与 getAction() 类似,多点触控需要使用这个方法获取事件类型
getActionIndex()获取该事件是哪个指针(手指)产生的。
getPointerCount()获取在屏幕上手指的个数。
getPointerId(int pointerIndex)获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。
findPointerIndex(int pointerId)通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容。
getX(int pointerIndex)获取某一个指针(手指)的X坐标
getY(int pointerIndex)获取某一个指针(手指)的Y坐标

index和pointId

在 2.2 版本以上,我们可以通过getActionIndex() 轻松获取到事件的索引(Index),Index 变化有以下几个特点:

1、从 0 开始,自动增长。 2、之前落下的手指抬起,后面手指的 Index 会随之减小。 (0、1、2 --> 第2个手指抬起 --> 第三个手指变为1 --> 0、1) 3、Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。 4、对 move 事件无效。 **getActionIndex()**获取到的始终是数值 0

相同点不同点
1. 从 0 开始,自动增长。
2. 落下手指时优先填补空缺(填补之前抬起手指的编号)。
Index 会变化,pointId 始终不变。

pointerIndex 与 pointerId

pointerIndex 和 actionIndex 区别并不大,两者的数值是相同的,可以认为 pointerIndex 是特地为 move 事件准备的 actionIndex。

类型简介
pointerIndex用于获取具体事件,可能会随着其他手指的抬起和落下而变化
pointerId用于识别手指,手指按下时产生,手指抬起时回收,期间始终不变

这两个数值使用以下两个方法相互转换:

方法简介
getPointerId(int pointerIndex)获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。
findPointerIndex(int pointerId)通过 pointerId 获取到当前状态下 pointIndex,之后通过 pointIndex 获取其他内容。

自定义View示例

img
/**
* Created by Varmin
* on 2017/7/5 16:16.
* 文件描述:left,content,right三个tag,在布局中给每个部分设置该tag。用于该ViewGroup内部给子View排序。
* 功能:默认全部关闭左右滑动。分别设置打开
*/
public class SlideView extends ViewGroup implements View.OnClickListener, View.OnLongClickListener {
private static final String TAG = "SlideView";
public final String LEFT = "left";
public final String CONTENT = "content";
public final String RIGHT = "right";
private Scroller mScroller;
/**
* scroller滑动时间。默认250ms
*/
public static final int DEFAULT_TIMEOUT = 250;
public static final int SLOW_TIMEOUT = 500;
/**
* 左右View的宽度
*/
private int leftWidth;
private int rightWidth;
private GestureDetector mGesture;
private ViewConfiguration mViewConfig;

public SlideView(Context context) {
super(context);
init(context);
}

public SlideView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}

public SlideView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

private void init(Context context) {
mScroller = new Scroller(context);
//都是自己处理的,这里没有用到该手势方法
//缺点:误差有些大。这种精确滑动的,最好自己判断
mGesture = new GestureDetector(context, new SlideGestureDetector());
mViewConfig = ViewConfiguration.get(context);
//默认false
setClickable(true);
}

/**
* 所有的子View都映射完xml,该方法最早能获取到childCount
* 在onMeasuer/onLayout中获取,注册监听的话,会多次被调用
* 在构造方法中,不能获取到childCount。
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
initListener();
}

private void initListener() {
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
childView.setClickable(true);
childView.setOnClickListener(this);
if (CONTENT.equals(childView.getTag())) {
childView.setOnLongClickListener(this);
}
}

}
@Override
public void onClick(View v) {
String tag = (String) v.getTag();
switch (tag) {
case LEFT:
Toast.makeText(getContext(), "Left", Toast.LENGTH_SHORT).show();
break;
case CONTENT:
Toast.makeText(getContext(), "Content", Toast.LENGTH_SHORT).show();
closeAll(SLOW_TIMEOUT);
break;
case RIGHT:
Toast.makeText(getContext(), "Right", Toast.LENGTH_SHORT).show();
break;
}
}

@Override
public boolean onLongClick(View v) {
Toast.makeText(getContext(), "Content_LongClick", Toast.LENGTH_SHORT).show();
return true;
}

/**
* 每个View的大小都是由父容器给自己传递mode来确定。
* 每个View的位置都是由父容器给自己设定好自己在容器中的左上右下来确定位置。
* 所以,继承至ViewGroup的容器,要在自己内部实现对子View大小和位置的确定。
*/

/**
* 子View不会自己测量自己的,所以在这里测量各个子View大小
* 另外,处理自己是wrap的情况,给自己一个确定的值。
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//测量子View
measureChildren(widthMeasureSpec, heightMeasureSpec);
//测量自己
//默认是给该ViewGroup设置固定宽高,假设不纯在wrap情况,onlayout中也不考虑此情况
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
String tag = (String) childView.getTag();
switch (tag) {
case LEFT:
leftWidth = childWidth;
childView.layout(-childWidth, 0, 0, childHeight);
break;
case CONTENT:
childView.layout(0, 0, childWidth, childHeight);
break;
case RIGHT:
rightWidth = childWidth;
childView.layout(getMeasuredWidth(), 0,
getMeasuredWidth() + childWidth, childHeight);
break;
}
}

}


@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean handled = super.onInterceptTouchEvent(ev);
if (handled) {
return true;
}
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInitX = (int) ev.getX();
mInitY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int offsetX = (int) (ev.getX() - mInitX);
int offsetY = (int) (ev.getY() - mInitY);
/**
* 判断可以横向滑动了
* 1,拦截自己的子View接收事件
* 2,申请父ViewGroup不要看拦截事件。
*/
if ((Math.abs(offsetX) - Math.abs(offsetY)) > mViewConfig.getScaledTouchSlop()) {
requestDisallowInterceptTouchEvent(true);
return true;
}
break;
case MotionEvent.ACTION_UP:
//重置回ViewGroup默认的拦截状态
requestDisallowInterceptTouchEvent(false);
break;
}
return handled;
}

private int mInitX;
private int mOffsetX;
private int mInitY;
private int mOffsetY;
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean handled = false;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
mOffsetX = (int) (event.getX() - mInitX);
mOffsetY = (int) (event.getY() - mInitY);
if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > 0) {//横向触发条件
//预估,偏移offsetX后的大小
int mScrollX = getScrollX() + (-mOffsetX);
if (mScrollX <= 0) {//向右滑动,显示leftView:110
//上面的是预估,如果预估大于目标:你不能return放弃了,要调整mOffsetX的值使其刚好等于目标
if (Math.abs(mScrollX) > leftWidth) {
mOffsetX = leftWidth - Math.abs(getScrollX());
//return true;
}
}else {//向左滑动,显示rightView:135
if (mScrollX > rightWidth) {
mOffsetX = getScrollX() - rightWidth;
//return true;
}
}
this.scrollBy(-mOffsetX,0);
mInitX = (int) event.getX();
mInitY = (int) event.getY();
return true;
}

break;
case MotionEvent.ACTION_UP:
int upScrollX = getScrollX();
if (upScrollX > 0) {//向左滑动,显示rightView
if (upScrollX >= (rightWidth/2)) {
mOffsetX = upScrollX - rightWidth;
}else {
mOffsetX = upScrollX;
}
}else {//向右,显示leftView
if (Math.abs(upScrollX) >= (leftWidth/2)) {
mOffsetX = leftWidth - Math.abs(upScrollX);
}else {
mOffsetX = upScrollX;
}
}
// this.scrollBy(-mOffsetX,0);//太快
// startScroll(-mOffsetX, 0, 1000);//直接放进去,不行?
/**
* 注意startX。dx表示的是距离,不是目标位置
*/
mScroller.startScroll(getScrollX(), getScrollY(), -mOffsetX, 0,SLOW_TIMEOUT);
invalidate();

break;
}

if (!handled) {
handled = super.onTouchEvent(event);
}
return handled;
}


@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}


/**
* 虽然传入的dx、dy并不是scrollTo实际要到的点,dx,dy只是一小段距离。
* 但是computeScroll()我们scrollTo的是:现在位置+dx的距离 = 目标位置
*
* @param dx //TODO *距离!距离!并不是说要到达的目标。*
* @param dy
* @param duration 默认的滑动时间是250,复位的时候如果感觉太快可以自己设置事件.
*
*/
private void startScroll(int dx, int dy, int duration) {
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration);
//mScroller.extendDuration(duration); 在250ms基础上增加。构造函数传入的话,就是duration的时间。
invalidate();
}


/**
* 是否打开,ListView中复用关闭
* @return
*/
public boolean isOpened(){
return getScrollX() != 0;
}
public void closeAll(int duration){
mScroller.startScroll(getScrollX(), getScrollY(), (-getScrollX()), 0, duration);
invalidate();
}
}

Tips

scrollTo/By

通过三种方式可以实现View的滑动:

  1. 通过View本身提供的scrollTo/scrollBy方法;

  2. 通过动画使Veiw平移。

  3. 通过改变View的LayoutParams属性值。

**setScrollX/Y、scrollTo: **移动到x,y的位置

**scrollBy: **移动x,y像素的距离

    public void setScrollX(int value) {
scrollTo(value, mScrollY);
}

public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}

public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
}
}

**注意:**假如scrollTo(30,10),按照View右下正,左上负的概念,因该是向右滑动30,向下滑动10。


作者:Varmin_
链接:https://juejin.cn/post/6972431645429202980
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

MVVMFrame for Android 是一个基于Google官方推出的JetPack(Lifecycle,LiveData,ViewModel,Room)构建的快速开发框架,从此构建一个MVVM模式的项目变得快捷简单。

MVVMFrame for Android 是一个基于Google官方推出的Architecture Components dependencies(现在叫JetPack){ Lifecycle,LiveData,ViewModel,Room } 构建的快速开...
继续阅读 »

MVVMFrame for Android 是一个基于Google官方推出的Architecture Components dependencies(现在叫JetPack){ Lifecycle,LiveData,ViewModel,Room } 构建的快速开发框架。有了 MVVMFrame 的加持,从此构建一个 MVVM 模式的项目变得快捷简单。

架构

Image

Android version

引入

由于2021年2月3日 JFrog宣布将关闭Bintray和JCenter,计划在2022年2月完全关闭。 所以后续版本不再发布至 JCenter

  1. 在Project的 build.gradle 里面添加远程仓库
allprojects {
repositories {
//...
mavenCentral()
}
}
  1. 在Module的 build.gradle 里面添加引入依赖项

v2.x(使用 Hilt 简化 Dagger2 依赖注入用法)

//AndroidX 版本
implementation 'com.github.jenly1314:mvvmframe:2.1.0'

以前发布至JCenter的版本

v2.0.0(使用 Hilt 简化 Dagger2 依赖注入用法)

//AndroidX 版本
implementation 'com.king.frame:mvvmframe:2.0.0'

v1.x 以前版本(使用 Dagger2)

//AndroidX 版本
implementation 'com.king.frame:mvvmframe:1.1.4'

//Android Support版本
implementation 'com.king.frame:mvvmframe:1.0.2'

Dagger和 Room 的相关注解处理器

你需要引入下面的列出的编译时的注解处理器,用于自动生成相关代码。其它对应版本具体详情可查看 Versions

v2.x 版本($versions 相关可查看Versions

你需要在项目根目录的 build.gradle 文件中配置 Hilt 的插件路径:

buildscript {
...
dependencies {
...
classpath "com.google.dagger:hilt-android-gradle-plugin:$versions.daggerHint"
}
}

接下来,在 app/build.gradle 文件中,引入 Hilt 的插件和相关依赖:

...
apply plugin: 'dagger.hilt.android.plugin'

dependencies{
...

//AndroidX ------------------ MVVMFrame v2.x.x
//lifecycle
annotationProcessor "androidx.lifecycle:lifecycle-compiler:$versions.lifecycle"
//room
annotationProcessor "androidx.room:room-compiler:$versions.room"
//hilt
implementation "com.google.dagger:hilt-android:$versions.daggerHint"
annotationProcessor "com.google.dagger:hilt-android-compiler:$versions.daggerHint"

//从2.1.0以后已移除
// implementation "androidx.hilt:hilt-lifecycle-viewmodel:$versions.hilt"
// annotationProcessor "androidx.hilt:hilt-compiler:$versions.hilt"
}

v1.x 以前版本,建议 查看分支版本

在 app/build.gradle 文件中引入 Dagger 和 Room 相关依赖:


dependencies{
...

//AndroidX ------------------ MVVMFrame v1.1.4
//dagger
annotationProcessor 'com.google.dagger:dagger-android-processor:2.30.1'
annotationProcessor 'com.google.dagger:dagger-compiler:2.30.1'
//room
annotationProcessor 'androidx.room:room-compiler:2.2.5'
}

dependencies{
...

// Android Support ------------------ MVVMFrame v1.0.2
//dagger
annotationProcessor 'com.google.dagger:dagger-android-processor:2.19'
annotationProcessor 'com.google.dagger:dagger-compiler:2.19'
//room
annotationProcessor 'android.arch.persistence.room:compiler:1.1.1'
}

如果你的项目使用的是 Kotlin,记得加上 kotlin-kapt 插件,并需使用 kapt 替代 annotationProcessor

MVVMFrame引入的库(具体对应版本请查看 Versions

    //appcompat
compileOnly deps.appcompat

//retrofit
api deps.retrofit.retrofit
api deps.retrofit.gson
api deps.retrofit.converter_gson

//retrofit-helper
api deps.jenly.retrofit_helper

//lifecycle
api deps.lifecycle.runtime
api deps.lifecycle.extensions
annotationProcessor deps.lifecycle.compiler

//room
api deps.room.runtime
annotationProcessor deps.room.compiler

//hilt
compileOnly deps.dagger.hilt_android
annotationProcessor deps.dagger.hilt_android_compiler

compileOnly deps.hilt.hilt_viewmodel
annotationProcessor deps.hilt.hilt_compiler

//log
api deps.timber

示例

集成步骤代码示例 (示例出自于app中)

Step.1 启用DataBinding,在你项目中的build.gradle的android{}中添加配置:

Android Studio 4.x 以后版本

buildFeatures{
dataBinding = true
}

Android Studio 4.x 以前版本

dataBinding {
enabled true
}

Step.2 使用JDK8编译(v1.1.2新增),在你项目中的build.gradle的android{}中添加配置:

compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}

Step.3 自定义全局配置(继承MVVMFrame中的FrameConfigModule)(提示:如果你没有自定义配置的需求,可以直接忽略此步骤)

/**
* 自定义全局配置
* @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
public class AppConfigModule extends FrameConfigModule {
@Override
public void applyOptions(Context context, ConfigModule.Builder builder) {
builder.baseUrl(Constants.BASE_URL)//TODO 配置Retrofit中的baseUrl
.retrofitOptions(new RetrofitOptions() {
@Override
public void applyOptions(Retrofit.Builder builder) {
//TODO 配置Retrofit
//如想使用RxJava
//builder.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
}
})
.okHttpClientOptions(new OkHttpClientOptions() {
@Override
public void applyOptions(OkHttpClient.Builder builder) {
//TODO 配置OkHttpClient
}
})
.gsonOptions(new GsonOptions() {
@Override
public void applyOptions(GsonBuilder builder) {
//TODO 配置Gson
}
})
.roomDatabaseOptions(new RoomDatabaseOptions<RoomDatabase>() {
@Override
public void applyOptions(RoomDatabase.Builder<RoomDatabase> builder) {
//TODO 配置RoomDatabase
}
});
}
}

Step.4 在你项目中的AndroidManifest.xml中通过配置meta-data来自定义全局配置(提示:如果你没有自定义配置的需求,可以直接忽略此步骤)

<!-- MVVMFrame 全局配置 -->
<meta-data android:name="com.king.mvvmframe.config.AppConfigModule"
android:value="FrameConfigModule"/>

Step.5 关于Application

2.x版本 因为从2.x开始使用到了Hilt,所以你自定义的Application需加上 @HiltAndroidApp 注解,这是使用Hilt的一个必备前提。示例如下:

   @HiltAndroidApp
public class YourApplication extends Application {

}

1.x版本 将你项目的 Application 继承MVVMFrame中的 BaseApplication

/**
* MVVMFrame 框架基于Google官方的Architecture Components dependencies 构建,在使用MVVMFrame时,需遵循一些规范:
* 1.你的项目中的Application中需初始化MVVMFrame框架相关信息,有两种方式处理:
* a.直接继承本类{@link BaseApplication}即可;
* b.如你的项目中的Application本身继承了其它第三方的Application,因为Java是单继承原因,导致没法继承本类,可参照{@link BaseApplication}类,
* 将{@link BaseApplication}中相关代码复制到你项目的Application中,在相应的生命周期中调用即可。
*
* @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
public class App extends BaseApplication {

@Override
public void onCreate() {
//TODO 如果默认配置已经能满足你的需求,你不需要自定义配置,可以通过下面注释掉的方式设置 BaseUrl,从而可以省略掉 step3 , setp4 两个步骤。
// RetrofitHelper.getInstance().setBaseUrl(baseUrl);
super.onCreate();
//开始构建项目时,DaggerApplicationComponent类可能不存在,你需要执行Make Project才能生成,Make Project快捷键 Ctrl + F9
ApplicationComponent appComponent = DaggerApplicationComponent.builder()
.appComponent(getAppComponent())
.build();
//注入
appComponent.inject(this);

}


}

其他

关于v2.x

因为v2.x版本 使用了 Hilt 的缘故,简化了之前 Dagger2 的用法,建议在新项目中使用。如果是从 v1.x 升级到 v2.x,集成步骤稍有变更,详情请查看 Step.5,并且可能还需要删除以前 @Component@Module等注解桥接层相关的逻辑代码,因为从v2.x开始,这些桥接逻辑无需自己编写,全部交由 Hilt 处理。

关于使用 Hilt

Hilt 是JetPack中新增的一个依赖注入库,其基于Dagger2研发(后面统称为Dagger),但它不同于Dagger。对于Android开发者来说,Hilt可以说专门为Android 打造。

之前使用的Dagger for Android虽然也是针对于Android打造,也能通过 @ContributesAndroidInjector 来通过生成简化一部分样板代码,但是感觉还不够彻底。因为 Component 层相关的桥接还是要自己写。Hilt的诞生改善了这些问题。

Hilt 大幅简化了Dagger 的用法,使得我们不用通过 @Component 注解去编写桥接层的逻辑,但是也因此限定了注入功能只能从几个 Android 固定的入口点开始,

Hilt 一共支持 6 个入口点,分别是:

Application

Activity

Fragment

View

Service

BroadcastReceiver

其中,只有 Application 这个入口点是使用 @HiltAndroidApp 注解来声明,示例如下

Application 示例

   @HiltAndroidApp
public class YourApplication extends Application {

}

其他的所有入口点,都是用 @AndroidEntryPoint 注解来声明,示例如下

Activity 示例

   @AndroidEntryPoint
public class YourActivity extends BaseActivity {

}

Fragment 示例

   @AndroidEntryPoint
public class YourFragment extends BaseFragment {

}

Service 示例

   @AndroidEntryPoint
public class YourService extends BaseService {

}

BroadcastReceiver 示例

   @AndroidEntryPoint
public class YourBroadcastReceiver extends BaseBroadcastReceiver {

}

其它示例

BaseViewModel 示例 (如果您继承使用了BaseViewModel或其子类,你需要参照如下方式在类上添加 @HiltViewModel 并在构造函数上添加 @Inject 注解)

   @HiltViewModel
public class YourViewModel extends BaseViewModel<YourModel> {
@Inject
public DataViewModel(@NonNull Application application, YourModel model) {
super(application, model);
}
}

BaseModel 示例 (如果您继承使用了BaseModel或其子类,你需要参照如下方式在构造函数上添加 @Inject 注解)

   public class YourModel extends BaseModel {
@Inject
public BaseModel(IDataRepository dataRepository){
super(dataRepository);
}
}

如果使用的是 v2.0.0 版本 (使用 androidx.hilt:hilt-lifecycle-viewmodel 的方式)

BaseViewModel 示例 (如果您继承使用了BaseViewModel或其子类,你需要参照如下方式在构造函数上添加 @ViewModelInject 注解)

   public class YourViewModel extends BaseViewModel<YourModel> {
@ViewModelInject
public DataViewModel(@NonNull Application application, YourModel model) {
super(application, model);
}
}

关于使用 Dagger

之所以特意说 Dagger 是因为Dagger的学习曲线相对陡峭一点,没那么容易理解。

  1. 如果你对 Dagger 很了解,那么你将会更加轻松的去使用一些注入相关的骚操作。

因为 MVVMFrame 中使用到了很多 Dagger 注入相关的一些操作。所以会涉及Dagger相关技术知识。

但是并不意味着你一定要会使用 Dagger,才能使用MVVMFrameComponent

如果你对 Dagger 并不熟悉,其实也是可以用的,因为使用 Dagger 全局注入主要都已经封装好了。你只需参照Demo 中的示例,照葫芦画瓢。 主要关注一些继承了BaseActivityBaseFragmentBaseViewModel等相关类即可。

这里列一些主要的通用注入参照示例:(下面Dagger相关的示例仅适用于v1.x版本,因为v2.x已基于Hilt编写,简化了Dagger依赖注入桥接层相关逻辑)

直接或间接继承了 BaseActivity 的配置示例:

/**
* Activity模块统一管理:通过{@link ContributesAndroidInjector}方式注入,自动生成模块组件关联代码,减少手动编码
* @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
@Module(subcomponents = BaseActivitySubcomponent.class)
public abstract class ActivityModule {

@ContributesAndroidInjector
abstract MainActivity contributeMainActivity();

}

直接或间接继承了 BaseFragment 的配置示例:

/**
* Fragment模块统一管理:通过{@link ContributesAndroidInjector}方式注入,自动生成模块组件关联代码,减少手动编码
* @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
@Module(subcomponents = BaseFragmentSubcomponent.class)
public abstract class FragmentModule {

@ContributesAndroidInjector
abstract MainFragment contributeMainFragment();

}

直接或间接继承了 BaseViewModel 的配置示例:

/**
* ViewModel模块统一管理:通过{@link Binds}和{@link ViewModelKey}绑定关联对应的ViewModel
* ViewModelModule 例子
* @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
@Module
public abstract class ViewModelModule {

@Binds
@IntoMap
@ViewModelKey(MainViewModel.class)
abstract ViewModel bindMainViewModel(MainViewModel viewModel);
}

ApplicationModule 的配置示例

/**
* Application模块:为{@link ApplicationComponent}提供注入的各个模块
* @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
@Module(includes = {ViewModelFactoryModule.class,ViewModelModule.class,ActivityModule.class,FragmentModule.class})
public class ApplicationModule {

}

ApplicationComponent 的配置示例

/**
* @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
@ApplicationScope
@Component(dependencies = AppComponent.class,modules = {ApplicationModule.class})
public interface ApplicationComponent {
//指定你的 Application 继承类
void inject(App app);
}

通过上面的通用配置注入你所需要的相关类之后,如果配置没什么问题,你只需 执行Make Project 一下,或通过 Make Project 快捷键 Ctrl + F9 ,就可以自动生产相关代码。 比如通过 ApplicationComponent 生成的 DaggerApplicationComponent 类。

然后在你的 Application 集成类 App 中通过 DaggerApplicationComponent 构建 ApplicationComponent,然后注入即可。

    //开始构建项目时,DaggerApplicationComponent类可能不存在,你需要执行Make Project才能生成,Make Project快捷键 Ctrl + F9
ApplicationComponent appComponent = DaggerApplicationComponent.builder()
.appComponent(getAppComponent())
.build();
//注入
appComponent.inject(this);

你也可以直接查看app中的源码示例

关于设置 BaseUrl

目前通过设置 BaseUrl 的入口主要有两种:

1.一种是通过在 Manifest 中配置 meta-data 的来自定义 FrameConfigModule,在里面 通过 {@link ConfigModule.Builder#baseUrl(String)}来配置 BaseUrl。(一次设置,全局配置)

2.一种就是通过RetrofitHelper {@link RetrofitHelper#setBaseUrl(String)} 或 {@link RetrofitHelper#setBaseUrl(HttpUrl)} 来配置 BaseUrl。(可多次设置,动态全局配置,有前提条件)

以上两种配置 BaseUrl 的方式都可以达到目的。但是你可以根据不同的场景选择不同的配置方式。

主要场景与选择如下:

一般场景:对于只使用单个不变的 BaseUrl的

场景1:如果本库的默认已满足你的需求,无需额外自定义配置的。
     选择:建议你直接使用 {@link RetrofitHelper#setBaseUrl(String)} 或 {@link RetrofitHelper#setBaseUrl(HttpUrl)} 来初始化 BaseUrl,切记在框架配置初始化 BaseUrl之前,建议在你自定义的 {@link Application#onCreate()}中初始化。
场景2:如果本库的默认配置不满足你的需求,你需要自定义一些配置的。(比如需要使用 RxJava相关)
     选择:建议你在自定义配置中通过 {@link ConfigModule.Builder#baseUrl(String)} 来初始化 BaseUrl。

二般场景:对于只使用单个 BaseUrl 但是,BaseUrl中途会变动的。

场景3:和一般场景一样,也能分两种,所以选择也和一般场景也可以是一样的。
     选择:两种选择都行,但当 BaseUrl需要中途变动时,还需将 {@link RetrofitHelper#setDynamicDomain(boolean)} 设置为 {@code true} 才能支持动态改变 BaseUrl。

特殊场景:对于支持多个 BaseUrl 且支持动态可变的。

   选择:这个场景的选择,主要涉及到另外的方法,请查看 {@link RetrofitHelper#putDomain(String, String)} 和 {@link RetrofitHelper#putDomain(String, HttpUrl)}相关详情

更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档


代码下载:MVVMFrame.zip

收起阅读 »

RetrofitHelper是一个支持配置多个BaseUrl,支持动态改变BaseUrl,动态配置超时时长的Retrofit帮助类

RetrofitHelper for Android 是一个为 Retrofit 提供便捷配置多个BaseUrl相关的扩展帮助类。 支持配置多个BaseUrl 支持动态改变BaseUrl 支持动态配置超时时长 支持添加公...
继续阅读 »


RetrofitHelper

RetrofitHelper for Android 是一个为 Retrofit 提供便捷配置多个BaseUrl相关的扩展帮助类。

主要功能介绍

  •  支持配置多个BaseUrl
  •  支持动态改变BaseUrl
  •  支持动态配置超时时长
  •  支持添加公共请求头

Gif 展示

Image

引入

由于2021年2月3日 JFrog宣布将关闭Bintray和JCenter,计划在2022年2月完全关闭。 所以后续版本不再发布至 JCenter

  1. 在Project的 build.gradle 里面添加远程仓库
allprojects {
repositories {
//...
mavenCentral()
}
}
  1. 在Module的 build.gradle 里面添加引入依赖项
//AndroidX 版本
implementation 'com.github.jenly1314:retrofit-helper:1.0.1'

RetrofitHelper引入的库(具体对应版本请查看 Versions

    compileOnly "androidx.appcompat:appcompat:$versions.appcompat"
compileOnly "com.squareup.retrofit2:retrofit:$versions.retrofit"

因为 RetrofitHelper 的依赖只在编译时有效,并未打入包中,所以您的项目中必须依赖上面列出相关库

示例

主要集成步骤代码示例

Step.1 需使用JDK8编译,在你项目中的build.gradle的android{}中添加配置:

compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}

Step.2 通过RetrofitUrlManager初始化OkHttpClient,进行初始化配置

//通过RetrofitHelper创建一个支持多个BaseUrl的 OkHttpClient
//方式一
val okHttpClient = RetrofitHelper.getInstance()
.createClientBuilder()
//...你自己的其他配置
.build()
//方式二
val okHttpClient = RetrofitHelper.getInstance()
.with(builder)
//...你自己的其他配置
.build()
//完整示例
val okHttpClient = RetrofitHelper.getInstance()
.createClientBuilder()
.addInterceptor(LogInterceptor())
.build()
val retrofit = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(Gson()))
.build()

Step.3 定义接口时,通过注解标记对应接口,支持动态改变 BaseUrl相关功能

 interface ApiService {

/**
* 接口示例,没添加任何标识,和常规使用一致
* @return
*/
@GET("api/user")
fun getUser(): Call<User>


/**
* Retrofit默认返回接口定义示例,添加 DomainName 标示 和 Timeout 标示
* @return
*/
@DomainName(domainName) // domainName 域名别名标识,用于支持切换对应的 BaseUrl
@Timeout(connectTimeout = 15,readTimeout = 15,writeTimeout = 15,timeUnit = TimeUnit.SECONDS) //超时标识,用于自定义超时时长
@GET("api/user")
fun getUser(): Call<User>

/**
* 动态改变 BaseUrl
* @return
*/
@BaseUrl(baseUrl) //baseUrl 标识,用于支持指定 BaseUrl
@GET("api/user")
fun getUser(): Call<User>


//--------------------------------------

/**
* 使用RxJava返回接口定义示例,添加 DomainName 标示 和 Timeout 标示
* @return
*/
@DomainName(domainName) // domainName 域名别名标识,用于支持切换对应的 BaseUrl
@Timeout(connectTimeout = 20,readTimeout = 10) //超时标识,用于自定义超时时长
@GET("api/user")
fun getUser(): Observable<User>

}

Step.4 添加多个 BaseUrl 支持

        //添加多个 BaseUrl 支持 ,domainName为域名别名标识,domainUrl为域名对应的 BaseUrl,与上面的接口定义表示一致即可生效
RetrofitHelper.getInstance().putDomain(domainName,domainUrl)
        //添加多个 BaseUrl 支持 示例
RetrofitHelper.getInstance().apply {
//GitHub baseUrl
putDomain(Constants.DOMAIN_GITHUB,Constants.GITHUB_BASE_URL)
//Google baseUrl
putDomain(Constants.DOMAIN_GOOGLE,Constants.GOOGLE_BASE_URL)
}

RetrofitHelper

/**
* Retrofit帮助类
*


* 主要功能介绍:
* 1.支持管理多个 BaseUrl,且支持运行时动态改变
* 2.支持接口自定义超时时长,满足每个接口动态定义超时时长
* 3.支持添加公共请求头
*


*
* RetrofitHelper中的核心方法
*
* {@link #createClientBuilder()} 创建 {@link OkHttpClient.Builder}初始化一些配置参数,用于支持多个 BaseUrl
*
* {@link #with(OkHttpClient.Builder)} 传入 {@link OkHttpClient.Builder} 配置一些参数,用于支持多个 BaseUrl
*
* {@link #setBaseUrl(String)} 和 {@link #setBaseUrl(HttpUrl)} 主要用于设置默认的 BaseUrl。
*
* {@link #putDomain(String, String)} 和 {@link #putDomain(String, HttpUrl)} 主要用于支持多个 BaseUrl,且支持 BaseUrl 动态改变。
*
* {@link #setDynamicDomain(boolean)} 设置是否支持 配置多个BaseUrl,且支持动态改变,一般会通过其他途径自动开启,此方法一般不会主动用到,只有在特殊场景下可能会有此需求,所以提供此方法主要用于提供更多种可能。
*
* {@link #setHttpUrlParser(HttpUrlParser)} 设置 HttpUrl解析器 , 当前默认采用的 {@link DomainParser} 实现类,你也可以自定义实现 {@link HttpUrlParser}
*
* {@link #setAddHeader(boolean)} 设置是否添加头,一般会通过{@link #addHeader(String, String)}相关方法自动开启,此方法一般不会主动用到,只有特殊场景下会有此需求,主要用于提供统一控制。
*
* {@link #addHeader(String, String)} 设置头,主要用于添加公共头消息。
*
* {@link #addHeaders(Map)} 设置头,主要用于设置公共头消息。
*
* 这里只是列出一些对外使用的核心方法,和相关的简单说明。如果想了解更多,可以查看对应的方法和详情。
*
*


*
* @author Jenly
*/
public final class RetrofitHelper{
//...
}

特别说明

        //通过setBaseUrl可以动态改变全局的 BaseUrl,优先级比putDomain(domainName,domainUrl)低,谨慎使用
RetrofitHelper.getInstance().setBaseUrl(dynamicUrl)

更多使用详情,请查看Demo中的源码使用示例或直接查看API帮助文档


代码下载:RetrofitHelper.zip

收起阅读 »

Android原生绘图进度条+简单自定义属性代码生成器

先一下效果:一、简单自定义属性生成器1.玩安卓的应该都写过自定义控件的自定义属性:如下我写着写着感觉好枯燥,基本上流程相似,也没有什么技术难度,想:这种事不就应该交给机器吗?2.通过attrs.xml自动生成相应代码秉承着能用代码解决的问题,绝对不动手。能够靠...
继续阅读 »

先一下效果:

圆形进度条.gif

横向进度条.gif

一、简单自定义属性生成器

1.玩安卓的应该都写过自定义控件的自定义属性:如下

自定义控件.png

我写着写着感觉好枯燥,基本上流程相似,也没有什么技术难度,想:这种事不就应该交给机器吗?

2.通过attrs.xml自动生成相应代码

秉承着能用代码解决的问题,绝对不动手。能够靠智商解决的问题,绝对不靠体力的大无畏精神:

写了一个小工具,将代码里的内容自动生成一下:基本上就是字符串的切割和拼装,工具附在文尾

使用方法与注意点:

1.拷贝到AndroidStudio的test里,将attrs.xml的文件路径设置一下,运行
2.自定义必须符合命名规则,如z_pb_on_height,专属前缀如z_,单词间下划线连接即可
3.它并不是什么高大上的东西,只是简单的字符串切割拼组,只适用简单的自定义属性[dimension|color|boolean|string](不过一般的自定义属性也够用了)

自动生成.png

在开篇之前:先看一下Android系统内自定义控件的书写风格,毕竟跟原生看齐没有什么坏处

看一下LinearLayout的源码:

1.构造方法使用最多参数的那个,其他用this(XXX)调用

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

public LinearLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public LinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}

public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
...
}

2.自定义属性的书写

1).先将自定义属性的成员变量定义好

2).如果自定义属性不是很多,一个一个a.getXXX,默认值直接写在后面就行了
3).看了一下TextView的源码,自定义属性很多,它是先定义默认值的变量,再使用,而且用switch来对a.getXXX进行赋值

final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);

int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1);
if (index >= 0) {
setOrientation(index);
}

index = a.getInt(com.android.internal.R.styleable.LinearLayout_gravity, -1);
if (index >= 0) {
setGravity(index);
}

boolean baselineAligned = a.getBoolean(R.styleable.LinearLayout_baselineAligned, true);
if (!baselineAligned) {
setBaselineAligned(baselineAligned);
}
......
a.recycle();

一、水平的进度条

条形进度条分析.png

1.自定义控件属性:values/attrs.xml

    






















2.初始代码:将进行一些常规处理

public class TolyProgressBar extends ProgressBar {

private Paint mPaint;
private int mPBWidth;
private RectF mRectF;
private Path mPath;
private float[] mFloat8Left;//左边圆角数组
private float[] mFloat8Right;//右边圆角数组

private float mProgressX;//进度理论值
private float mEndX;//进度条尾部
private int mTextWidth;//文字宽度
private boolean mLostRight;//是否不画右边
private String mText;//文字

private int mPbBgColor = 0xffC9C9C9;
private int mPbOnColor = 0xff54F340;
private int mPbOnHeight = dp(6);
private int mPbBgHeight = dp(6);
private int mPbTxtColor = 0xff525252;
private int mPbTxtSize = sp(10);
private int mPbTxtOffset = sp(10);
private boolean mPbTxtGone= false;

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

public TolyProgressBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public TolyProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TolyProgressBar);
mPbOnHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_on_height, mPbOnHeight);
mPbTxtOffset = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_txt_offset, mPbTxtOffset);
mPbOnColor = a.getColor(R.styleable.TolyProgressBar_z_pb_on_color, mPbOnColor);
mPbTxtSize = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_txt_size, mPbTxtSize);
mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
mPbBgHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_bg_height, mPbBgHeight);
mPbBgColor = a.getColor(R.styleable.TolyProgressBar_z_pb_bg_color, mPbBgColor);
mPbTxtGone = a.getBoolean(R.styleable.TolyProgressBar_z_pb_txt_gone, mPbTxtGone);
a.recycle();

init();
}

private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextSize(mPbTxtSize);
mPaint.setColor(mPbOnColor);
mPaint.setStrokeWidth(mPbOnHeight);

mRectF = new RectF();
mPath = new Path();


mFloat8Left = new float[]{//仅左边两个圆角--为背景
mPbOnHeight / 2, mPbOnHeight / 2,//左上圆角x,y
0, 0,//右上圆角x,y
0, 0,//右下圆角x,y
mPbOnHeight / 2, mPbOnHeight / 2//左下圆角x,y
};

mFloat8Right = new float[]{
0, 0,//左上圆角x,y
mPbBgHeight / 2, mPbBgHeight / 2,//右上圆角x,y
mPbBgHeight / 2, mPbBgHeight / 2,//右下圆角x,y
0, 0//左下圆角x,y
};
}

}

private int sp(int sp) {
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}

private int dp(int dp) {
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}


2.测量:

    @Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = measureHeight(heightMeasureSpec);
setMeasuredDimension(width, height);
mPBWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();//进度条实际宽度
}

    /**
* 测量高度
*
* @param heightMeasureSpec
* @return
*/

private int measureHeight(int heightMeasureSpec) {
int result = 0;
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);

if (mode == MeasureSpec.EXACTLY) {
//控件尺寸已经确定:如:
// android:layout_height="40dp"或"match_parent"
result = size;
} else {
int textHeight = (int) (mPaint.descent() - mPaint.ascent());
result = getPaddingTop() + getPaddingBottom() + Math.max(
Math.max(mPbBgHeight, mPbOnHeight), Math.abs(textHeight));

if (mode == MeasureSpec.AT_MOST) {//最多不超过
result = Math.min(result, size);
}
}
return result;
}
复制代码

3.绘制:

    @Override
protected synchronized void onDraw(Canvas canvas) {
super.onDraw(canvas);

canvas.save();
canvas.translate(getPaddingLeft(), getHeight() / 2);

parseBeforeDraw();//1.绘制前对数值进行计算以及控制的flag设置

if (getProgress() == 100) {//进度达到100后文字消失
whenOver();//2.
}
if (mEndX > 0) {//当进度条尾部>0绘制
drawProgress(canvas);//3.
}
if (!mPbTxtGone) {//绘制文字
mPaint.setColor(mPbTxtColor);
int y = (int) (-(mPaint.descent() + mPaint.ascent()) / 2);
canvas.drawText(mText, mProgressX, y, mPaint);
} else {
mTextWidth = 0 - mPbTxtOffset;
}
if (!mLostRight) {//绘制右侧
drawRight(canvas);/4.
}

canvas.restore();
}

1).praseBeforeDraw()

/**
* 对数值进行计算以及控制的flag设置
*/

private void parseBeforeDraw() {
mLostRight = false;//lostRight控制是否绘制右侧
float radio = getProgress() * 1.f / getMax();//当前百分比率
mProgressX = radio * mPBWidth;//进度条当前长度
mEndX = mProgressX - mPbTxtOffset / 2; //进度条当前长度-文字间隔的左半
mText = getProgress() + "%";
if (mProgressX + mTextWidth > mPBWidth) {
mProgressX = mPBWidth - mTextWidth;
mLostRight = true;
}
//文字宽度
mTextWidth = (int) mPaint.measureText(mText);
}

2).whenOver()

/**
* 当结束是执行:
*/

private void whenOver() {
mPbTxtGone = true;
mFloat8Left = new float[]{//只有进度达到100时让进度圆角是四个
mPbBgHeight / 2, mPbBgHeight / 2,//左上圆角x,y
mPbBgHeight / 2, mPbBgHeight / 2,//右上圆角x,y
mPbBgHeight / 2, mPbBgHeight / 2,//右下圆角x,y
mPbBgHeight / 2, mPbBgHeight / 2//左下圆角x,y
};
}

3).drawProgress()

/**
* 绘制左侧:(进度条)
*
* @param canvas
*/

private void drawProgress(Canvas canvas) {
mPath.reset();
mRectF.set(0, mPbOnHeight / 2, mEndX, -mPbOnHeight / 2);
mPath.addRoundRect(mRectF, mFloat8Left, Path.Direction.CW);//顺时针画
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mPbOnColor);
canvas.drawPath(mPath, mPaint);//使用path绘制一端是圆头的线
}

4).drawRight()

/**
* 绘制左侧:(背景)
*
* @param canvas
*/

private void drawRight(Canvas canvas) {
float start = mProgressX + mPbTxtOffset / 2 + mTextWidth;
mPaint.setColor(mPbBgColor);
mPaint.setStrokeWidth(mPbBgHeight);
mPath.reset();
mRectF.set(start, mPbBgHeight / 2, mPBWidth, -mPbBgHeight / 2);
mPath.addRoundRect(mRectF, mFloat8Right, Path.Direction.CW);//顺时针画
canvas.drawPath(mPath, mPaint);//使用path绘制一端是圆头的线
}

xml里使用:


三、圆形进度条
1.自定义属性





2.代码实现:

/**
* 作者:张风捷特烈


* 时间:2018/11/9 0009:11:49


* 邮箱:1981462002@qq.com


* 说明:圆形进度条
*/

public class TolyRoundProgressBar extends TolyProgressBar {

private int mPbRadius = dp(30);//进度条半径
private int mMaxPaintWidth;

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

public TolyRoundProgressBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public TolyRoundProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TolyRoundProgressBar);
mPbRadius = (int) a.getDimension(R.styleable.TolyRoundProgressBar_z_pb_radius, mPbRadius);
mPbOnHeight = (int) (mPbBgHeight * 1.8f);//让进度大一点
a.recycle();

mPaint.setStyle(Paint.Style.STROKE);
mPaint.setDither(true);
}

@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mMaxPaintWidth = Math.max(mPbBgHeight, mPbOnHeight);
int expect = mPbRadius * 2 + mMaxPaintWidth + getPaddingLeft() + getPaddingRight();
int width = resolveSize(expect, widthMeasureSpec);
int height = resolveSize(expect, heightMeasureSpec);
int realWidth = Math.min(width, height);
mPaint.setStrokeCap(Paint.Cap.ROUND);

mPbRadius = (realWidth - getPaddingLeft() - getPaddingRight() - mMaxPaintWidth) / 2;
setMeasuredDimension(realWidth, realWidth);
}

@Override
protected synchronized void onDraw(Canvas canvas) {

String txt = getProgress() + "%";
float txtWidth = mPaint.measureText(txt);
float txtHeight = (mPaint.descent() + mPaint.ascent()) / 2;
canvas.save();
canvas.translate(getPaddingLeft() + mMaxPaintWidth / 2, getPaddingTop() + mMaxPaintWidth / 2);
drawDot(canvas);
mPaint.setStyle(Paint.Style.STROKE);
//背景
mPaint.setColor(mPbBgColor);
mPaint.setStrokeWidth(mPbBgHeight);
canvas.drawCircle(mPbRadius, mPbRadius, mPbRadius, mPaint);
//进度条
mPaint.setColor(mPbOnColor);
mPaint.setStrokeWidth(mPbOnHeight);
float sweepAngle = getProgress() * 1.0f / getMax() * 360;//完成角度
canvas.drawArc(
0, 0, mPbRadius * 2, mPbRadius * 2,
-90, sweepAngle, false, mPaint);
//文字
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mPbTxtColor);
canvas.drawText(txt, mPbRadius - txtWidth / 2, mPbRadius - txtHeight / 2, mPaint);
canvas.restore();
}

/**
* 绘制一圈点
*
* @param canvas
*/

private void drawDot(Canvas canvas) {
canvas.save();
int num = 40;
canvas.translate(mPbRadius, mPbRadius);
for (int i = 0; i < num; i++) {
canvas.save();
int deg = 360 / num * i;
canvas.rotate(deg);
mPaint.setStrokeWidth(dp(3));
mPaint.setColor(mPbBgColor);
mPaint.setStrokeCap(Paint.Cap.ROUND);
if (i * (360 / num) < getProgress() * 1.f / getMax() * 360) {
mPaint.setColor(mPbOnColor);
}
canvas.drawLine(0, mPbRadius * 3 / 4, 0, mPbRadius * 4 / 5, mPaint);
canvas.restore();
}
canvas.restore();
}
}




附录:简单自定义属性生成器

public class Attrs2Code {
@Test
public void main() {
File file = new File("C:\\Users\\Administrator\\Desktop\\attrs.xml");
initAttr("z_", file);
}

public static void initAttr(String preFix, File file) {
HashMap format = format(preFix, file);
String className = format.get("className");
String result = format.get("result");
StringBuilder sb = new StringBuilder();
sb.append("TypedArray a = context.obtainStyledAttributes(attrs, R.styleable." + className + ");\r\n");
format.forEach((s, s2) -> {
String styleableName = className + "_" + preFix + s;
if (s.contains("_")) {
String[] partStrArray = s.split("_");
s = "";
for (String part : partStrArray) {
String partStr = upAChar(part);
s += partStr;
}
}
if (s2.equals("dimension")) {
// mPbBgHeight = (int) a.getDimension(R.styleable.TolyProgressBar_z_pb_bg_height, mPbBgHeight);
sb.append("m" + s + " = (int) a.getDimension(R.styleable." + styleableName + ", m" + s + ");\r\n");
}
if (s2.equals("color")) {
// mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
sb.append("m" + s + " = a.getColor(R.styleable." + styleableName + ", m" + s + ");\r\n");
}
if (s2.equals("boolean")) {
// mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
sb.append("m" + s + " = a.getBoolean(R.styleable." + styleableName + ", m" + s + ");\r\n");
}
if (s2.equals("string")) {
// mPbTxtColor = a.getColor(R.styleable.TolyProgressBar_z_pb_txt_color, mPbTxtColor);
sb.append("m" + s + " = a.getString(R.styleable." + styleableName + ");\r\n");
}
});
sb.append("a.recycle();\r\n");
System.out.println(result);
System.out.println(sb.toString());
}

/**
* 读取文件+解析
*
* @param preFix 前缀
* @param file 文件路径
*/

public static HashMap format(String preFix, File file) {
HashMap container = new HashMap<>();
if (!file.exists() && file.isDirectory()) {
return null;
}
FileReader fr = null;
try {
fr = new FileReader(file);
//字符数组循环读取
char[] buf = new char[1024];
int len = 0;
StringBuilder sb = new StringBuilder();
while ((len = fr.read(buf)) != -1) {
sb.append(new String(buf, 0, len));
}
String className = sb.toString().split(""));
container.put("className", className);
String[] split = sb.toString().split("<");
String part1 = "private";
String type = "";//类型
String name = "";
String result = "";
String def = "";//默认值

StringBuilder sb2 = new StringBuilder();
for (String s : split) {
if (s.contains(preFix)) {
result = s.split(preFix)[1];
name = result.substring(0, result.indexOf("\""));
type = result.split("format=\"")[1];
type = type.substring(0, type.indexOf("\""));
container.put(name, type);
if (type.contains("color") || type.contains("dimension") || type.contains("integer")) {
type = "int";
def = "0";
}
if (result.contains("fraction")) {
type = "float";
def = "0.f";
}
if (result.contains("string")) {
type = "String";
def = "\"toly\"";
}
if (result.contains("boolean")) {
type = "boolean";
def = "false";

}
if (name.contains("_")) {
String[] partStrArray = name.split("_");
name = "";
for (String part : partStrArray) {
String partStr = upAChar(part);
name += partStr;
}
sb2.append(part1 + " " + type + " m" + name + "= " + def + ";\r\n");
}
container.put("result", sb2.toString());
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fr != null) {
fr.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return container;
}

/**
* 将字符串仅首字母大写
*
* @param str 待处理字符串
* @return 将字符串仅首字母大写
*/

public static String upAChar(String str) {
String a = str.substring(0, 1);
String tail = str.substring(1);
return a.toUpperCase() + tail;
}
}

代码下载:bobing107-IPhoneWatch_progressbar-master.zip

收起阅读 »

一个Android强大的饼状图

一、思路 1、空心图(一个大圆中心绘制一个小圆) 2、根据数据算出所占的角度 3、根据动画获取当前绘制的角度 4、根据当前角度获取Paint使用的颜色 5、动态绘制即将绘制的 和 绘制已经绘制的部分(最重要) 二、实现 1、空心图(一个大...
继续阅读 »

一、思路


  1、空心图(一个大圆中心绘制一个小圆)
2、根据数据算出所占的角度
3、根据动画获取当前绘制的角度
4、根据当前角度获取Paint使用的颜色
5、动态绘制即将绘制的 和 绘制已经绘制的部分(最重要)


二、实现


1、空心图(一个大圆中心绘制一个小圆)初始化数据


      paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL_AND_STROKE);

screenW = DensityUtils.getScreenWidth(context);

int width = DensityUtils.dip2px(context, 15);//圆环宽度
int widthXY = DensityUtils.dip2px(context, 10);//微调距离

int pieCenterX = screenW / 2;//饼状图中心X
int pieCenterY = screenW / 3;//饼状图中心Y
int pieRadius = screenW / 4;// 大圆半径

//整个饼状图rect
pieOval = new RectF();
pieOval.left = pieCenterX - pieRadius;
pieOval.top = pieCenterY - pieRadius + widthXY;
pieOval.right = pieCenterX + pieRadius;
pieOval.bottom = pieCenterY + pieRadius + widthXY;

//里面的空白rect
pieOvalIn = new RectF();
pieOvalIn.left = pieOval.left + width;
pieOvalIn.top = pieOval.top + width;
pieOvalIn.right = pieOval.right - width;
pieOvalIn.bottom = pieOval.bottom - width;

//里面的空白画笔
piePaintIn = new Paint();
piePaintIn.setAntiAlias(true);
piePaintIn.setStyle(Paint.Style.FILL);
piePaintIn.setColor(Color.parseColor("#f4f4f4"));

2、根据数据算出所占的角度


使用递归保证cakeValues的值的总和必为100,然后根据值求出角度


   private void settleCakeValues(int i) {
float sum = getSum(cakeValues, i);
CakeValue value = cakeValues.get(i);
if (sum <= 100f) {
value.setItemValue(100f - sum);
cakeValues.set(i, value);
} else {
value.setItemValue(0);
settleCakeValues(i - 1);
}
}
复制代码

3、根据动画获取当前绘制的角度


curAngle就是当前绘制的角度,drawArc()就是绘制的方法


cakeValueAnimator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float mAngle = obj2Float(animation.getAnimatedValue("angle"));
curAngle = mAngle;
drawArc();
}
});

4、根据当前角度获取Paint使用的颜色


根据当前的角度,计算当前是第几个item,通过
paint.setColor(Color.parseColor(cakeValues.get(colorIndex).getColors()));
来设置paint的颜色
复制代码

private int getCurItem(float curAngle) {
int res = 0;
for (int i = 0; i < itemFrame.length; i++) {
if (curAngle <= itemFrame[i] * ANGLE_NUM) {
res = i;
break;
}
}
return res;
}

5、动态绘制即将绘制的 和 绘制已经绘制的部分


最重要的一步,我的需求是4类,用不同的颜色




绘制当前颜色的扇形,curStartAngle扇形的起始位置,curSweepAngle扇形的终止位置


  paint.setColor(Color.parseColor(cakeValues.get(colorIndex).getColors()));
float curStartAngle = 0;
float curSweepAngle = curAngle;
if (curItem > 0) {
curStartAngle = itemFrame[curItem - 1] * ANGLE_NUM;
curSweepAngle = curAngle - (itemFrame[curItem - 1] * ANGLE_NUM);
}
canvas.drawArc(pieOval, curStartAngle, curSweepAngle, true, paint);

绘制已经绘制的扇形。根据curItem判断绘制过得扇形


for (int i = 0; i < curItem; i++) {
paint.setColor(Color.parseColor(cakeValues.get(i).getColors()));
if (i == 0) {
canvas.drawArc(pieOval, startAngle,(float) cakeValues.get(i).getItemValue() * ANGLE_NUM, true, paint);
continue;
}
canvas.drawArc(pieOval,itemFrame[i - 1] * ANGLE_NUM,(float) cakeValues.get(i).getItemValue() * ANGLE_NUM, true, paint);
}


绘制中心的圆


 canvas.drawArc(pieOvalIn, 0, 360, true, piePaintIn);

6、特别注意


isFirst判断是够是第一次绘制(绘制完成后,home键进入后台,再次进入,不需要动态绘制)


 @Override
protected void onDraw(Canvas canvas) {
if (isFirst && isDrawByAnim) {
drawCakeByAnim();
}
isFirst = false;
}
复制代码
isDrawByAnim判断是否需要动画绘制
drawCake()为静态绘制饼状图


public void surfaceCreated(SurfaceHolder holder) {
if (!isFirst||!isDrawByAnim)
drawCake();
}

更新


增加立体效果,提取配置参数


<declare-styleable name="CakeSurfaceView">
<attr name="isDrawByAnim" format="boolean"/>//是否动画
<attr name="isSolid" format="boolean"/>//是否立体
<attr name="duration" format="integer|reference"/>//动画时间
<attr name="defaultColor" format="string"/>//默认颜色

<attr name="ringWidth" format="integer|reference"/>//圆环宽度
<attr name="solidWidth" format="integer|reference"/>//立体宽度
<attr name="fineTuningWidth" format="integer|reference"/>//微调宽度
</declare-styleable>
复制代码
xml中使用
复制代码

<com.xp.xppiechart.view.CakeSurfaceView
android:id="@+id/assets_pie_chart"
android:background="#ffffff"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:defaultColor="#ff8712"
app:ringWidth="20"
app:solidWidth="5"
app:duration="3000"
app:isSolid="true"
app:isDrawByAnim="true"/>
复制代码


以上就是简单的实现动态绘制饼状图,待完善,以后会更新。如有建议和意见,请及时沟通。


代码下载:bobing107-CircularSectorProgressBar-master.zip

收起阅读 »

Android商品属性筛选与商品筛选!

前言这个次为大家带来的是一个完整的商品属性筛选与商品筛选。什么意思?都见过淘宝、京东等爱啪啪吧,里面有个商品详情,可以选择商品的属性,然后筛选出这个商品的具体型号,这样应该知道了吧?不知道也没关系,下面会有展示图。筛选属性最终完成关于商品筛选是有两种方式(至少...
继续阅读 »

前言

这个次为大家带来的是一个完整的商品属性筛选与商品筛选。什么意思?都见过淘宝、京东等爱啪啪吧,里面有个商品详情,可以选择商品的属性,然后筛选出这个商品的具体型号,这样应该知道了吧?不知道也没关系,下面会有展示图。

筛选属性最终完成
筛选属性最终完成

关于商品筛选是有两种方式(至少我只见到两种):

第一种: 将所有的商品的所有属性及详情返回给客户端,由客户端进行筛选。
淘宝用的就是这种。
第二种: 将所有的属性返回给客户端,客户选择完成属性后将属性发送给后台
,再由后台根据属性筛选出具体商品返回给客户端。
京东就是这样搞的。。

两种方式各有各的好处:

第一种:体验性特别好,用户感觉不到延迟,立即选中立即就筛选出了详情。就是客户端比较费劲。。。

第二种:客户端比较省时间,但是体验性太差了,你想想,在网络不是很通畅的时候,你选择一个商品还得等老半天。

因为当时我没有参加到这个接口的设计,导致一直在变化。。我才不会告诉不是后台不给力,筛选不出来才一股脑的将所有锅甩给客户端。

技术点

  1. 流式布局

     商品的属性并不是一样长的,所以需要自动适应内容的一个控件。
    推荐hongyang的博客。我就是照着那个搞的。
  2. RxJava

     不要问我,我不知道,我也是新手,我就是用它做出了效果,至于有没有
    用对,那我就不知道了。反正目的是达到了。
  3. Json解析???

准备

  1. FlowLayout
  2. RxJava

xml布局

这个部分的布局不是很难,只是代码量较多,咱们就省略吧,直接看效果吧

布局完成
布局完成

可以看到机身颜色、内存、版本下面都是空的,因为我们还没有将属性筛选出来。

数据分析

先看看整体的数据结构是怎么样的

数据结构
数据结构

每一个商品都有一个父类,仅作标识,不参与计算,比如数据中的华为P9就是一个商品的类目,在这下面有着各种属性组成的商品子类,这才是真正的商品。

而一个详细的商品是有三个基础属性所组成:

1. 版本
2. 内存
3. 制式

如上图中一个具体的商品的名称:"华为 P9全网通 3GB+32GB版 流光金 移动联通电信4G手机 双卡双待"

商品属性据结构
商品属性据结构

所以,要获得一个具体的商品是非常的简单,只需要客户选中的三个属性与上图中所对应的属性完全相同,就能得到这个商品。其中最关键的还是将所有的商品属性筛选出来。

筛选出所有属性及图片

本文中使用的数据是直接从Assets目录中直接读取的。

筛选出该商品的所有属性,怎么做呢?其实也是很简单的,直接for所有商品的所有属性,然后存储起来,去除重复的属性,那么最后剩下的就是该商品的属性了

 /**
* 初始化商品信息
*
  • 1. 提取所有的属性

  • *
  • 2. 提取所有颜色的照片

  • */

    private void initGoodsInfo() {
    //所有的颜色
    mColors = new ArrayList<>();
    //筛选过程中临时存放颜色
    mTempColors = new ArrayList<>();
    //所有的内存
    mMonerys = new ArrayList<>();
    //筛选过程中临时的内存
    mTempMonerys = new ArrayList<>();
    //所有的版本
    mVersions = new ArrayList<>();
    //筛选过程中的临时版本
    mTempVersions = new ArrayList<>();
    //获取到所有的商品
    shopLists = responseDto.getMsg().getChilds();
    callBack.refreshSuccess("¥" + responseDto.getPricemin() + " - " + responseDto.getPricemax(), responseDto.getMsg().getParent().getName());
    callBack.parentName(responseDto.getMsg().getParent().getName());
    //遍历商品
    Observable.from(shopLists)
    //转换对象 获取所有商品的属性集合
    .flatMap(childsEntity -> Observable.from(childsEntity.getAttrinfo().getAttrs()))
    .subscribe(attrsEntity -> {
    //判断颜色
    if (mActivity.getString(R.string.shop_color).equals(attrsEntity.getAttrname()) && !mTempColors.contains(attrsEntity.getAttrvalue())) {
    mColors.add(new TagInfo(attrsEntity.getAttrvalue()));
    mTempColors.add(attrsEntity.getAttrvalue());
    }
    //判断制式
    if (mActivity.getString(R.string.shop_standard).equals(attrsEntity.getAttrname()) && !mTempVersions.contains(attrsEntity.getAttrvalue())) {
    mVersions.add(new TagInfo(attrsEntity.getAttrvalue()));
    mTempVersions.add(attrsEntity.getAttrvalue());
    }
    //判断内存
    if (mActivity.getString(R.string.shop_monery).equals(attrsEntity.getAttrname()) && !mTempMonerys.contains(attrsEntity.getAttrvalue())) {
    mMonerys.add(new TagInfo(attrsEntity.getAttrvalue()));
    mTempMonerys.add(attrsEntity.getAttrvalue());
    }
    });

    // 提取出 每种颜色的照片
    tempImageColor = new ArrayList<>();
    mImages = new ArrayList<>();
    //遍历所有的商品列表
    Observable.from(shopLists)
    .subscribe(childsEntity -> {
    String color = childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue();
    if (!tempImageColor.contains(color)) {
    mImages.add(childsEntity.getShowimg());
    tempImageColor.add(color);
    }
    });
    // 提取出 每种颜色的照片

    //通知图片
    callBack.changeData(mImages, "¥" + responseDto.getPricemin() + " - " + responseDto.getPricemax());
    callBack.complete(null);
    }

    初始化属性列表

    属性之间是有一些关系的,比如我这里是以颜色为初始第一项,那么我就得根据颜色筛选出这个颜色下的所有内存,然后根据内存筛选出所有的版本。同时,只要颜色、内存、版本三个都选择了,就得筛选出这个商品。

    {颜色>内存>版本}>具体商品

    颜色

    初始化颜色,设置选择监听,一旦用户选择了某个颜色,那么需要获取这个颜色下的所有内存,并且要开始尝试获取商品详情。

    1. 初始化颜色

       /**
      * 初始化颜色
      *
      * @hint
      */

      private void initShopColor() {
      for (TagInfo mColor : mColors) {
      //初始化所有的选项为未选择状态
      mColor.setSelect(false);
      }
      tvColor.setText("\"未选择颜色\"");
      mColors.get(colorPositon).setSelect(true);
      colorAdapter = new ProperyTagAdapter(mActivity, mColors);
      rlShopColor.setAdapter(colorAdapter);
      colorAdapter.notifyDataSetChanged();
      rlShopColor.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);
      rlShopColor.setOnTagSelectListener((parent, selectedList) -> {
      colorPositon = selectedList.get(0);
      strColor = mColors.get(colorPositon).getText();
      // L.e("选中颜色:" + strColor);
      tvColor.setText("\"" + strColor + "\"");
      //获取颜色照片
      initColorShop();
      //查询商品详情
      iterationShop();
      });
      }
    2. 获取颜色下所有的内存和该颜色的照片

       /**
      * 初始化相应的颜色的商品 获得 图片
      */

      private void initColorShop() {
      //初始化 选项数据
      Observable.from(mMonerys).subscribe(tagInfo -> {
      tagInfo.setChecked(true);
      });
      L.e("开始筛选颜色下的内存----------------------------------------------------------------------------------");
      final List tempColorMemery = new ArrayList<>();
      //筛选内存
      Observable.from(shopLists)
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue().equals(strColor))
      .flatMap(childsEntity -> Observable.from(childsEntity.getAttrinfo().getAttrs()))
      .filter(attrsEntity -> mActivity.getString(R.string.shop_monery).equals(attrsEntity.getAttrname()))
      .subscribe(attrsEntity -> {
      tempColorMemery.add(attrsEntity.getAttrvalue());
      // L.e("内存:"+attrsEntity.getAttrvalue());
      });

      Observable.from(mTempMonerys)
      .filter(s -> !tempColorMemery.contains(s))
      .subscribe(s -> {
      L.e("没有的内存:" + s);
      mMonerys.get(mTempMonerys.indexOf(s)).setChecked(false);
      });
      momeryAdapter.notifyDataSetChanged();
      L.e("筛选颜色下的内存完成----------------------------------------------------------------------------------");

      //获取颜色的照片
      ImageHelper.loadImageFromGlide(mActivity, mImages.get(tempImageColor.indexOf(strColor)), ivShopPhoto);
      }
    1. 根据选中的属性查询是否存在该商品

       /**
      * 迭代 选择商品属性
      */

      private void iterationShop() {
      // 选择的内存 选择的版本 选择的颜色
      if (strMemory == null || strVersion == null || strColor == null)
      return;
      //隐藏购买按钮 显示为缺货
      resetBuyButton(false);
      Observable.from(shopLists)
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue().equals(strColor))
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(1).getAttrvalue().equals(strVersion))
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(2).getAttrvalue().equals(strMemory))
      .subscribe(childsEntity -> {
      L.e(childsEntity.getShopprice());
      tvPrice.setText("¥" + childsEntity.getShopprice());
      // ImageHelper.loadImageFromGlide(mActivity, Constant.IMAGE_URL + childsEntity.getShowimg(), ivShopPhoto);
      L.e("已找到商品:" + childsEntity.getName() + " id:" + childsEntity.getPid());
      selectGoods = childsEntity;
      tvShopName.setText(childsEntity.getName());
      //显示购买按钮
      resetBuyButton(true);
      initShopStagesCount++;
      });
      }

    内存

    通过前面一步,已经获取了所有的内存。这一步只需要展示该所有内存,设置选择监听,选择了某个内存后就根据 选择颜色>选择内存 获取所有的版本。并在在其中也是要iterationShop()查询商品的,万一你是往回点的时候呢?

    1. 初始化版本

       /**
      * 初始化内存
      */

      private void initShopMomery() {
      for (TagInfo mMonery : mMonerys) {
      mMonery.setSelect(false);
      Log.e(" ", "initShopMomery: " + mMonery.getText());
      }
      tvMomey.setText("\"未选择内存\"");
      mMonerys.get(momeryPositon).setSelect(true);
      //-----------------------------创建适配器
      momeryAdapter = new ProperyTagAdapter(mActivity, mMonerys);
      rlShopMomery.setAdapter(momeryAdapter);
      rlShopMomery.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);
      rlShopMomery.setOnTagSelectListener((parent, selectedList) -> {
      momeryPositon = selectedList.get(0);
      strMemory = mMonerys.get(momeryPositon).getText();
      // L.e("选中内存:" + strMemory);
      iterationShop();
      tvMomey.setText("\"" + strMemory + "\"");
      iterationVersion();
      });
      }
    2. 根据已选择的颜色和内存获取到版本

       /**
      * 迭代 获取版本信息
      */

      private void iterationVersion() {
      if (strColor == null || strMemory == null) {
      return;
      }
      // L.e("开始迭代版本");
      Observable.from(mVersions).subscribe(tagInfo -> {
      tagInfo.setChecked(true);
      });
      final List iterationTempVersion = new ArrayList<>();
      //1. 遍历出 这个颜色下的所有手机
      //2. 遍历出 这些手机的所有版本
      Observable.from(shopLists)
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(0).getAttrvalue().equals(strColor))
      .filter(childsEntity -> childsEntity.getAttrinfo().getAttrs().get(2).getAttrvalue().equals(strMemory))
      .flatMap(childsEntity -> Observable.from(childsEntity.getAttrinfo().getAttrs()))
      .filter(attrsEntity -> attrsEntity.getAttrname().equals(mActivity.getString(R.string.shop_standard)))
      .subscribe(attrsEntity -> {
      iterationTempVersion.add(attrsEntity.getAttrvalue());
      });

      Observable.from(mTempVersions).filter(s -> !iterationTempVersion.contains(s)).subscribe(s -> {
      mVersions.get(mTempVersions.indexOf(s)).setChecked(false);
      });
      versionAdapter.notifyDataSetChanged();
      // L.e("迭代版本完成");
      }

    版本

    其实到了这一步,已经算是完成了,只需要设置监听,获取选中的版本,然后开始查询商品。

        /**
    * 初始化版本
    */

    private void initShopVersion() {
    for (TagInfo mVersion : mVersions) {
    mVersion.setSelect(false);
    }
    tvVersion.setText("\"未选择版本\"");
    mVersions.get(versionPositon).setSelect(true);
    //-----------------------------创建适配器
    versionAdapter = new ProperyTagAdapter(mActivity, mVersions);
    rlShopVersion.setAdapter(versionAdapter);
    rlShopVersion.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_SINGLE);
    rlShopVersion.setOnTagSelectListener((parent, selectedList) -> {
    versionPositon = selectedList.get(0);
    strVersion = mVersions.get(versionPositon).getText();
    // L.e("选中版本:" + strVersion);
    iterationShop();
    tvVersion.setText("\"" + strVersion + "\"");
    });
    }

    完成

    最终效果图如下:

    筛选属性最终完成
    筛选属性最终完成

    不要在意后面的轮播图,那其实很简单的。

    代码下载:JiuYouYiShuSheng-Selector-master.zip

    收起阅读 »

    几句代码轻松拥有扫码功能!

    ZXingLite for Android 是ZXing的精简版,基于ZXing库优化扫码和生成二维码/条形码功能,扫码界面完全支持自定义,也可一行代码使用默认实现的扫码功能。总之你想要的都在这里。简单如斯,你不试试? Come on~ViewfinderVi...
    继续阅读 »

    ZXingLite for Android 是ZXing的精简版,基于ZXing库优化扫码和生成二维码/条形码功能,扫码界面完全支持自定义,也可一行代码使用默认实现的扫码功能。总之你想要的都在这里。

    简单如斯,你不试试? Come on~

    ViewfinderView属性说明

    属性值类型默认值说明
    maskColorcolor#60000000扫描区外遮罩的颜色
    frameColorcolor#7F1FB3E2扫描区边框的颜色
    cornerColorcolor#FF1FB3E2扫描区边角的颜色
    laserColorcolor#FF1FB3E2扫描区激光线的颜色
    labelTextstring扫描提示文本信息
    labelTextColorcolor#FFC0C0C0提示文本字体颜色
    labelTextSizedimension14sp提示文本字体大小
    labelTextPaddingdimension24dp提示文本距离扫描区的间距
    labelTextWidthdimension提示文本的宽度,默认为View的宽度
    labelTextLocationenumbottom提示文本显示位置
    frameWidthdimension扫码框宽度
    frameHeightdimension扫码框高度
    laserStyleenumline扫描激光的样式
    gridColumninteger20网格扫描激光列数
    gridHeightinteger40dp网格扫描激光高度,为0dp时,表示动态铺满
    cornerRectWidthdimension4dp扫描区边角的宽
    cornerRectHeightdimension16dp扫描区边角的高
    scannerLineMoveDistancedimension2dp扫描线每次移动距离
    scannerLineHeightdimension5dp扫描线高度
    frameLineWidthdimension1dp边框线宽度
    scannerAnimationDelayinteger20扫描动画延迟间隔时间,单位:毫秒
    frameRatiofloat0.625f扫码框与屏幕占比
    framePaddingLeftdimension0扫码框左边的内间距
    framePaddingTopdimension0扫码框上边的内间距
    framePaddingRightdimension0扫码框右边的内间距
    framePaddingBottomdimension0扫码框下边的内间距
    frameGravityenumcenter扫码框对齐方式

    引入

    Gradle:

    最新版本

    //AndroidX 版本
    implementation 'com.king.zxing:zxing-lite:2.0.3'

    v1.x 旧版本

    //AndroidX 版本
    implementation 'com.king.zxing:zxing-lite:1.1.9-androidx'

    //Android Support 版本
    implementation 'com.king.zxing:zxing-lite:1.1.9'
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的JitPack来compile)
    allprojects {
    repositories {
    //...
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    版本说明

    v2.x 基于CameraX重构震撼发布

    v2.x 相对于 v1.x 的优势

    • v2.x基于CameraX,抽象整体流程,可扩展性更高。
    • v2.x基于CameraX通过预览裁剪的方式确保预览界面不变形,无需铺满屏幕,就能适配(v1.x通过遍历Camera支持预览的尺寸,找到与屏幕最接近的比例,减少变形的可能性(需铺满屏幕,才能适配))

    v2.x 特别说明

    • v2.x如果您是通过继承CaptureActivity或CaptureFragment实现扫码功能,那么动态权限申请相关都已经在CaptureActivity或CaptureFragment处理好了。
    • v2.x如果您是通过继承CaptureActivity或CaptureFragment实现扫码功能,如果有想要修改默认配置,可重写initCameraScan方法,修改CameraScan的配置即可,如果无需修改配置,直接在跳转原界面的onActivityResult 接收扫码结果即可(更多具体详情可参见app中的使用示例)。
    关于CameraX
    • CameraX暂时还是Beta版,可能会存在一定的稳定性,如果您有这个考量,可以继续使用 ZXingLite 以前的 v1.x 版本。相信不久之后CameraX就会发布稳定版。

    v1.x 说明

    【v1.1.9】 如果您正在使用 1.x 版本请点击下面的链接查看分支版本,当前 2.x 版本已经基于 Camerx 进行重构,不支持升级,请在新项目中使用。

    查看AndroidX版 1.x 分支 请戳此处

    查看Android Support版 1.x 分支 请戳此处

    查看 1.x API帮助文档

    使用 v1.x 版本的无需往下看了,下面的示例和相关说明都是针对于当前最新版本。

    示例

    布局示例

    可自定义布局(覆写getLayoutId方法),布局内至少要保证有PreviewView。

    PreviewView 用来预览,布局内至少要保证有PreviewView,如果是继承CaptureActivity或CaptureFragment,控件id可覆写getPreviewViewId方法自定义

    ViewfinderView 用来渲染扫码视图,给用户起到一个视觉效果,本身扫码识别本身没有关系,如果是继承CaptureActivity或CaptureFragment,控件id可复写getViewfinderViewId方法自定义,默认为previewView,返回0表示无需ViewfinderView

    ivFlashlight 用来内置手电筒,如果是继承CaptureActivity或CaptureFragment,控件id可复写getFlashlightId方法自定义,默认为ivFlashlight。返回0表示无需内置手电筒。您也可以自己去定义

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.camera.view.PreviewView
    android:id="@+id/previewView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    <com.king.zxing.ViewfinderView
    android:id="@+id/viewfinderView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
    <ImageView
    android:id="@+id/ivFlashlight"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:src="@drawable/zxl_flashlight_selector"
    android:layout_marginTop="@dimen/zxl_flashlight_margin_top" />
    </FrameLayout>

    或在你的布局中添加

        <include layout="@layout/zxl_capture"/>

    代码示例 (二维码/条形码)

        //跳转的默认扫码界面
    startActivityForResult(new Intent(context,CaptureActivity.class),requestCode);

    //生成二维码
    CodeUtils.createQRCode(content,600,logo);
    //生成条形码
    CodeUtils.createBarCode(content, BarcodeFormat.CODE_128,800,200);
    //解析条形码/二维码
    CodeUtils.parseCode(bitmapPath);
    //解析二维码
    CodeUtils.parseQRCode(bitmapPath);

    CameraScan配置示例

        //获取CameraScan,扫码相关的配置设置。CameraScan里面包含部分支持链式调用的方法,即调用返回是CameraScan本身的一些配置建议在startCamera之前调用。
    getCameraScan().setPlayBeep(true)//设置是否播放音效,默认为false
    .setVibrate(true)//设置是否震动,默认为false
    .setCameraConfig(new CameraConfig())//设置相机配置信息,CameraConfig可覆写options方法自定义配置
    .setNeedAutoZoom(false)//二维码太小时可自动缩放,默认为false
    .setNeedTouchZoom(true)//支持多指触摸捏合缩放,默认为true
    .setDarkLightLux(45f)//设置光线足够暗的阈值(单位:lux),需要通过{@link #bindFlashlightView(View)}绑定手电筒才有效
    .setBrightLightLux(100f)//设置光线足够明亮的阈值(单位:lux),需要通过{@link #bindFlashlightView(View)}绑定手电筒才有效
    .bindFlashlightView(ivFlashlight)//绑定手电筒,绑定后可根据光线传感器,动态显示或隐藏手电筒按钮
    .setOnScanResultCallback(this)//设置扫码结果回调,需要自己处理或者需要连扫时,可设置回调,自己去处理相关逻辑
    .setAnalyzer(new MultiFormatAnalyzer(new DecodeConfig()))//设置分析器,DecodeConfig可以配置一些解码时的配置信息,如果内置的不满足您的需求,你也可以自定义实现,
    .setAnalyzeImage(true)//设置是否分析图片,默认为true。如果设置为false,相当于关闭了扫码识别功能
    .startCamera();//启动预览


    //设置闪光灯(手电筒)是否开启,需在startCamera之后调用才有效
    getCameraScan().enableTorch(torch);

    CameraScan配置示例(只需识别二维码的配置示例)

            //初始化解码配置
    DecodeConfig decodeConfig = new DecodeConfig();
    decodeConfig.setHints(DecodeFormatManager.QR_CODE_HINTS)//如果只有识别二维码的需求,这样设置效率会更高,不设置默认为DecodeFormatManager.DEFAULT_HINTS
    .setFullAreaScan(false)//设置是否全区域识别,默认false
    .setAreaRectRatio(0.8f)//设置识别区域比例,默认0.8,设置的比例最终会在预览区域裁剪基于此比例的一个矩形进行扫码识别
    .setAreaRectVerticalOffset(0)//设置识别区域垂直方向偏移量,默认为0,为0表示居中,可以为负数
    .setAreaRectHorizontalOffset(0);//设置识别区域水平方向偏移量,默认为0,为0表示居中,可以为负数

    //在启动预览之前,设置分析器,只识别二维码
    getCameraScan()
    .setVibrate(true)//设置是否震动,默认为false
    .setAnalyzer(new MultiFormatAnalyzer(decodeConfig));//设置分析器,如果内置实现的一些分析器不满足您的需求,你也可以自定义去实现

    如果直接使用CaptureActivity需在您项目的AndroidManifest中添加如下配置

        <activity
    android:name="com.king.zxing.CaptureActivity"
    android:screenOrientation="portrait"
    android:theme="@style/CaptureTheme"/>

    快速实现扫码有以下几种方式:

    1、直接使用CaptureActivity或者CaptureFragment。(纯洁的扫码,无任何添加剂)

    2、通过继承CaptureActivity或者CaptureFragment并自定义布局。(适用于大多场景,并无需关心扫码相关逻辑,自定义布局时需覆写getLayoutId方法)

    3、在你项目的Activity或者Fragment中实例化一个CameraScan即可。(适用于想在扫码界面写交互逻辑,又因为项目架构或其它原因,无法直接或间接继承CaptureActivity或CaptureFragment时使用)

    4、继承CameraScan自己实现一个,可参照默认实现类DefaultCameraScan,其它步骤同方式3。(扩展高级用法,谨慎使用)

    其他

    需使用JDK8+编译,在你项目中的build.gradle的android{}中添加配置:

    compileOptions {
    targetCompatibility JavaVersion.VERSION_1_8
    sourceCompatibility JavaVersion.VERSION_1_8
    }

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档

    代码下载:ZXingLite.zip

    收起阅读 »

    Android一个专注于App更新,一键傻瓜式集成App版本升级的开源库!

    AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版本升级的轻量开源库。(无需担心通知栏适配;无需担心重复点击下载;无需担心App安装等问题;这些AppUpdater都已帮您处理好。) 核心库主要包括app-update...
    继续阅读 »




    AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版本升级的轻量开源库。(无需担心通知栏适配;无需担心重复点击下载;无需担心App安装等问题;这些AppUpdater都已帮您处理好。) 核心库主要包括app-updater和app-dialog。

    下载更新和弹框提示分开,是因为这本来就是两个逻辑。完全独立开来能有效的解耦。

    • app-updater 主要负责后台下载更新App,无需担心下载时各种配置相关的细节,一键傻瓜式升级。
    • app-dialog 主要是提供常用的Dialog和DialogFragment,简化弹框提示,布局样式支持自定义。

    app-updater + app-dialog 配合使用,谁用谁知道。

    功能介绍

    •  专注于App更新一键傻瓜式升级
    •  够轻量,体积小
    •  支持监听下载过程
    •  支持下载失败,重新下载
    •  支持下载优先取本地缓存
    •  支持通知栏提示内容和过程全部可配置
    •  支持Android Q(10)
    •  支持取消下载
    •  支持使用OkHttpClient下载

    Gif 展示

    Image

    引入

    Maven:

        //app-updater
    <dependency>
    <groupId>com.king.app</groupId>
    <artifactId>app-updater</artifactId>
    <version>1.0.10</version>
    <type>pom</type>
    </dependency>

    //app-dialog
    <dependency>
    <groupId>com.king.app</groupId>
    <artifactId>app-dialog</artifactId>
    <version>1.0.10</version>
    <type>pom</type>
    </dependency>

    Gradle:


    //----------AndroidX 版本
    //app-updater
    implementation 'com.king.app:app-updater:1.0.10-androidx'
    //app-dialog
    implementation 'com.king.app:app-dialog:1.0.10-androidx'

    //----------Android Support 版本
    //app-updater
    implementation 'com.king.app:app-updater:1.0.10'
    //app-dialog
    implementation 'com.king.app:app-dialog:1.0.10'

    Lvy:

        //app-updater
    <dependency org='com.king.app' name='app-dialog' rev='1.0.10'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>

    //app-dialog
    <dependency org='com.king.app' name='app-dialog' rev='1.0.10'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
        allprojects {
    repositories {
    //...
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    示例

        //一句代码,傻瓜式更新
    new AppUpdater(getContext(),url).start();
        //简单弹框升级
    AppDialogConfig config = new AppDialogConfig(context);
    config.setTitle("简单弹框升级")
    .setOk("升级")
    .setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
    .setOnClickOk(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    new AppUpdater.Builder()
    .setUrl(mUrl)
    .build(getContext())
    .start();
    AppDialog.INSTANCE.dismissDialog();
    }
    });
    AppDialog.INSTANCE.showDialog(getContext(),config);
        //简单DialogFragment升级
    AppDialogConfig config = new AppDialogConfig(context);
    config.setTitle("简单DialogFragment升级")
    .setOk("升级")
    .setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
    .setOnClickOk(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    new AppUpdater.Builder()
    .setUrl(mUrl)
    .setFilename("AppUpdater.apk")
    .build(getContext())
    .setHttpManager(OkHttpManager.getInstance())//不设置HttpManager时,默认使用HttpsURLConnection下载,如果使用OkHttpClient实现下载,需依赖okhttp库
    .start();
    AppDialog.INSTANCE.dismissDialogFragment(getSupportFragmentManager());
    }
    });
    AppDialog.INSTANCE.showDialogFragment(getSupportFragmentManager(),config);

    更多使用详情,请查看app中的源码使用示例或直接查看API帮助文档

    混淆

    app-updater Proguard rules

    app-dialog Proguard rules

    代码下载:AppUpdater.zip

    收起阅读 »

    一个支持可拖动多边形,可拖动多边形的角改变其形状的任意多边形控件

    DragPolygonViewDragPolygonView for Android 是一个支持可拖动多边形,支持通过拖拽多边形的角改变其形状的任意多边形控件。特性说明 支持添加多个任意多边形 支持通过触摸多边形拖动改变其位置 支...
    继续阅读 »


    DragPolygonView

    DragPolygonView for Android 是一个支持可拖动多边形,支持通过拖拽多边形的角改变其形状的任意多边形控件。

    特性说明

    •  支持添加多个任意多边形
    •  支持通过触摸多边形拖动改变其位置
    •  支持通过触摸多边形的角改变其形状
    •  支持点击、长按、改变等事件监听
    •  支持多边形单选或多选模式

    Gif 展示

    Image

    DragPolygonView 自定义属性说明

    属性值类型默认值说明
    dpvStrokeWidthfloat4画笔描边的宽度
    dpvPointStrokeWidthMultiplierfloat1.0绘制多边形点坐标时基于画笔描边的宽度倍数
    dpvPointNormalColorcolor#FFE5574C多边形点的颜色
    dpvPointPressedColorcolor多边形点按下状态时的颜色
    dpvPointSelectedColorcolor多边形点选中状态时的颜色
    dpvLineNormalColorcolor#FFE5574C多边形边线的颜色
    dpvLinePressedColorcolor多边形边线按下状态的颜色
    dpvLineSelectedColorcolor多边形边线选中状态的颜色
    dpvFillNormalColorcolor#3FE5574C多边形填充的颜色
    dpvFillPressedColorcolor#7FE5574C多边形填充按下状态时的颜色
    dpvFillSelectedColorcolor#AFE5574C多边形填充选中状态时的颜色
    dpvAllowableOffsetsdimension16dp触点允许的误差偏移量
    dpvDragEnabledbooleantrue是否启用拖动多边形
    dpvChangeAngleEnabledbooleantrue是否启用多边形的各个角的角度支持可变
    dpvMultipleSelectionbooleanfalse是否是多选模式,默认:单选模式
    dpvClickToggleSelectedbooleanfalse是否点击就切换多边形的选中状态
    dpvAllowDragOutViewbooleanfalse是否允许多边形拖出视图范围
    dpvTextSizedimension16sp是否允许多边形拖出视图范围
    dpvTextNormalColorcolor#FFE5574C多边形文本的颜色
    dpvTextPressedColorcolor多边形文本按下状态的颜色
    dpvTextSelectedColorcolor多边形文本选中状态的颜色
    dpvShowTextbooleantrue是否显示多边形的文本
    dpvFakeBoldTextbooleanfalse多边形Text的字体是否为粗体

    引入

    Maven:

    <dependency>
    <groupId>com.king.view</groupId>
    <artifactId>dragpolygonview</artifactId>
    <version>1.0.2</version>
    <type>pom</type>
    </dependency>

    Gradle:

    implementation 'com.king.view:dragpolygonview:1.0.2'

    Lvy:

    <dependency org='com.king.view' name='dragpolygonview' rev='1.0.2'>
    <artifact name='$AID' ext='pom'></artifact>
    </dependency>
    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
    allprojects {
    repositories {
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    示例

    布局示例

        <com.king.view.dragpolygonview.DragPolygonView
    android:id="@+id/dragPolygonView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

    代码示例

        //添加多边形
    dragPolygonView.addPolygon(Polygon polygon);
    //添加多边形(多边形的各个点)
    dragPolygonView.addPolygon(PointF... points);
    //根据位置将多边形改为选中状态
    dragPolygonView.setPolygonSelected(int position);
    //改变监听
    dragPolygonView.setOnChangeListener(OnChangeListener listener);
    //点击监听
    dragPolygonView.setOnPolygonClickListener(OnPolygonClickListener listener);
    //长按监听
    dragPolygonView.setOnPolygonLongClickListener(OnPolygonLongClickListener listener)

    更多使用详情,请查看app中的源码使用示例

    代码下载:jenly1314-DragPolygonView-master.zip

    收起阅读 »

    Android运行时权限终极方案,用PermissionX

    痛点在哪里?没有人愿意编写处理 Android 运行时权限的代码,因为它真的太繁琐了。这是一项没有什么技术含量,但是你又不得不去处理的工作,因为不处理它程序就会崩溃。但如果处理起来比较简单也就算了,可事实上,Android 提供给我们的运行时权限 API 并不...
    继续阅读 »

    痛点在哪里?

    没有人愿意编写处理 Android 运行时权限的代码,因为它真的太繁琐了。

    这是一项没有什么技术含量,但是你又不得不去处理的工作,因为不处理它程序就会崩溃。但如果处理起来比较简单也就算了,可事实上,Android 提供给我们的运行时权限 API 并不友好。

    以一个拨打电话的功能为例,因为 CALL_PHONE 权限是危险权限,所以在我们除了要在 AndroidManifest.xml 中声明权限之外,还要在执行拨打电话操作之前进行运行时权限处理才行。

    权限声明如下:

    然后,编写如下代码来进行运行时权限处理

    class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    makeCallBtn.setOnClickListener {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
    call()
    } else {
    ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE), 1)
    }
    }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    when (requestCode) {
    1 -> {
    if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    call()
    } else {
    Toast.makeText(this, "You denied CALL_PHONE permission", Toast.LENGTH_SHORT).show()
    }
    }
    }
    }

    private fun call() {
    try {
    val intent = Intent(Intent.ACTION_CALL)
    intent.data = Uri.parse("tel:10086")
    startActivity(intent)
    } catch (e: SecurityException) {
    e.printStackTrace()
    }
    }

    }


    这段代码中真有正意义的功能逻辑就是 call() 方法中的内容,可是如果直接调用 call() 方法是无法实现拨打电话功能的,因为我们还没有申请 CALL_PHONE 权限。

    那么整段代码其他的部分就都是在处理 CALL_PHONE 权限申请。可以看到,这里需要先判断用户是否已授权我们拨打电话的权限,如果没有的话则要进行权限申请,然后还要在 onRequestPermissionsResult() 回调中处理权限申请的结果,最后才能去执行拨打电话的操作。

    你可能觉得,这也不算是很繁琐呀,代码量并不是很多。那是因为,目前我们还只是处理了运行时权限最简单的场景,而实际的项目环境中有着更加复杂的场景在等着我们。

    比如说,你的 App 可能并不只是单单申请一个权限,而是需要同时申请多个权限。虽然 ActivityCompat.requestPermissions() 方法允许一次性传入多个权限名,但是你在 onRequestPermissionsResult() 回调中就需要判断哪些权限被允许了,哪些权限被拒绝了,被拒绝的权限是否影响到应用程序的核心功能,以及是否要再次申请权限。

    而一旦牵扯到再次申请权限,就引出了一个更加复杂的问题。你申请的权限被用户拒绝过了一次,那么再次申请将很有可能再次被拒绝。为此,Android 提供了一个 shouldShowRequestPermissionRationale() 方法,用于判断是否需要向用户解释申请这个权限的原因,一旦 shouldShowRequestPermissionRationale() 方法返回 true,那么我们最好弹出一个对话框来向用户阐明为什么我们是需要这个权限的,这样可以增加用户同意授权的几率。

    是不是已经觉得很复杂了?不过还没完,Android 系统还提供了一个 “拒绝,不要再询问” 的选项,如下图所示:

    只要用户选择了这个选项,那么我们以后每次执行权限申请的代码都将会直接被拒绝。

    可是如果我的某项功能就是必须要依赖这个权限才行呢?没有办法,你只能提示用户去应用程序设置当中手动打开权限,程序方面已无法进行操作。

    可以看出,如果想要在项目中对运行时权限做出非常全面的处理,是一件相当复杂的事情。事实上,大部分的项目都没有将权限申请这块处理得十分恰当,这也是我编写 PermissionX 的理由。

    PermissionX 的实现原理

    在开始介绍 PermissionX 的具体用法之前,我们先来讨论一下它的实现原理。

    其实之前并不是没有人尝试过对运行时权限处理进行封装,我之前在做直播公开课的时候也向大家演示过一种运行时权限 API 的封装过程。

    但是,想要对运行时权限的 API 进行封装并不是一件容易的事,因为这个操作是有特定的上下文依赖的,一般需要在 Activity 中接收 onRequestPermissionsResult() 方法的回调才行,所以不能简单地将整个操作封装到一个独立的类中。

    为此,也衍生出了一系列特殊的封装方案,比如将运行时权限的操作封装到 BaseActivity 中,或者提供一个透明的 Activity 来处理运行时权限等。

    不过上述两种方案都不够轻量,因为改变 Activity 的继承结构这可是大事情,而提供一个透明的 Activty 则需要在 AndroidManifest.xml 中进行额外的声明。

    现在,业内普遍比较认可使用另外一种小技巧来进行实现。是什么小技巧呢?回想一下,之前所有申请运行时权限的操作都是在 Activity 中进行的,事实上,Android 在 Fragment 中也提供了一份相同的 API,使得我们在 Fragment 中也能申请运行时权限。

    但不同的是,Fragment 并不像 Activity 那样必须有界面,我们完全可以向 Activity 中添加一个隐藏的 Fragment,然后在这个隐藏的 Fragment 中对运行时权限的 API 进行封装。这是一种非常轻量级的做法,不用担心隐藏 Fragment 会对 Activity 的性能造成什么影响。

    这就是 PermissionX 的实现原理了,书中其实也已经介绍过了这部分内容。但是,在其实现原理的基础之上,后期我又增加了很多新功能,让 PermissionX 变得更加强大和好用,下面我们就来学习一下 PermissionX 的具体用法。

    基本用法

    要使用 PermissionX 之前,首先需要将其引入到项目当中,如下所示

    dependencies {
    ...
    implementation 'com.permissionx.guolindev:permissionx:1.1.1'
    }


    我在写本篇文章时 PermissionX 的最新版本是 1.1.1,想要查看它的当前最新版本,请访问 PermissionX 的主页:github.com/guolindev/P…

    PermissionX 的目的是为了让运行时权限处理尽可能的容易,因此怎么让 API 变得简单好用就是我优先要考虑的问题。

    比如同样实现拨打电话的功能,使用 PermissionX 只需要这样写:

    class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    makeCallBtn.setOnClickListener {
    PermissionX.init(this)
    .permissions(Manifest.permission.CALL_PHONE)
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    call()
    } else {
    Toast.makeText(this, "您拒绝了拨打电话权限", Toast.LENGTH_SHORT).show()
    }
    }
    }
    }

    ...

    }


    是的,PermissionX 的基本用法就这么简单。首先调用 init() 方法来进行初始化,并在初始化的时候传入一个 FragmentActivity 参数。由于 AppCompatActivity 是 FragmentActivity 的子类,所以只要你的 Activity 是继承自 AppCompatActivity 的,那么直接传入 this 就可以了。

    接下来调用 permissions() 方法传入你要申请的权限名,这里传入 CALL_PHONE 权限。你也可以在 permissions() 方法中传入任意多个权限名,中间用逗号隔开即可。

    最后调用 request() 方法来执行权限申请,并在 Lambda 表达式中处理申请结果。可以看到,Lambda 表达式中有 3 个参数:allGranted 表示是否所有申请的权限都已被授权,grantedList 用于记录所有已被授权的权限,deniedList 用于记录所有被拒绝的权限。

    因为我们只申请了一个 CALL_PHONE 权限,因此这里直接判断:如果 allGranted 为 true,那么就调用 call() 方法,否则弹出一个 Toast 提示。

    运行结果如下:

    怎么样?对比之前的写法,是不是觉得运行时权限处理没那么繁琐了?

    核心用法

    然而我们目前还只是处理了最普通的场景,刚才提到的,假如用户拒绝了某个权限,在下次申请之前,我们最好弹出一个对话框来向用户解释申请这个权限的原因,这个又该怎么实现呢?

    别担心,PermissionX 对这些情况进行了充分的考虑。

    onExplainRequestReason() 方法可以用于监听那些被用户拒绝,而又可以再次去申请的权限。从方法名上也可以看出来了,应该在这个方法中解释申请这些权限的原因。

    而我们只需要将 onExplainRequestReason() 方法串接到 request() 方法之前即可,如下所示:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    这种情况下,所有被用户拒绝的权限会优先进入 onExplainRequestReason() 方法进行处理,拒绝的权限都记录在 deniedList 参数当中。接下来,我们只需要在这个方法中调用 showRequestReasonDialog() 方法,即可弹出解释权限申请原因的对话框,如下所示:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
    showRequestReasonDialog(deniedList, "即将重新申请的权限是程序必须依赖的权限", "我已明白", "取消")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    showRequestReasonDialog() 方法接受 4 个参数:第一个参数是要重新申请的权限列表,这里直接将 deniedList 参数传入。第二个参数则是要向用户解释的原因,我只是随便写了一句话,这个参数描述的越详细越好。第三个参数是对话框上确定按钮的文字,点击该按钮后将会重新执行权限申请操作。第四个参数是一个可选参数,如果不传的话相当于用户必须同意申请的这些权限,否则对话框无法关闭,而如果传入的话,对话框上会有一个取消按钮,点击取消后不会重新进行权限申请,而是会把当前的申请结果回调到 request() 方法当中。

    另外始终要记得将所有申请的权限都在 AndroidManifest.xml 中进行声明:

    重新运行一下程序,效果如下图所示:

    当前版本解释权限申请原因对话框的样式还无法自定义,1.3.0 版本当中已支持了自定义权限提醒对话框样式的功能,详情请参阅 PermissionX 重磅更新,支持自定义权限提醒对话框 。

    当然,我们也可以指定要对哪些权限重新申请,比如上述申请的 3 个权限中,我认为 CAMERA 权限是必不可少的,而其他两个权限则可有可无,那么在重新申请的时候也可以只申请 CAMERA 权限:


    PermissionX.init(this)   
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.ACCESS_FINE_LOCATION)
    .onExplainRequestReason { deniedList ->
    val filteredList = deniedList.filter {
    it == Manifest.permission.CAMERA
    }
    showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白", "取消")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    这样当再次申请权限的时候就只会申请 CAMERA 权限,剩下的两个权限最终会被传入到 request() 方法的 deniedList 参数当中。

    解决了向用户解释权限申请原因的问题,接下来还有一个头疼的问题要解决:如果用户不理会我们的解释,仍然执意拒绝权限申请,并且还选择了拒绝且不再询问的选项,这该怎么办?通常这种情况下,程序层面已经无法再次做出权限申请,唯一能做的就是提示用户到应用程序设置当中手动打开权限。

    更多用法

    那么 PermissionX 是如何处理这种情况的呢?我相信绝对会给你带来惊喜。PermissionX 中还提供了一个 onForwardToSettings() 方法,专门用于监听那些被用户永久拒绝的权限。另外从方法名上就可以看出,我们可以在这里提醒用户手动去应用程序设置当中打开权限。代码如下所示:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
    showRequestReasonDialog(deniedList, "即将重新申请的权限是程序必须依赖的权限", "我已明白", "取消")
    }
    .onForwardToSettings { deniedList ->
    showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白", "取消")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    可以看到,这里又串接了一个 onForwardToSettings() 方法,所有被用户选择了拒绝且不再询问的权限都会进行到这个方法中处理,拒绝的权限都记录在 deniedList 参数当中。

    接下来,你并不需要自己弹出一个 Toast 或是对话框来提醒用户手动去应用程序设置当中打开权限,而是直接调用 showForwardToSettingsDialog() 方法即可。类似地,showForwardToSettingsDialog() 方法也接收 4 个参数,每个参数的作用和刚才的 showRequestReasonDialog() 方法完全一致,我这里就不再重复解释了。

    showForwardToSettingsDialog() 方法将会弹出一个对话框,当用户点击对话框上的我已明白按钮时,将会自动跳转到当前应用程序的设置界面,从而不需要用户自己慢慢进入设置当中寻找当前应用了。另外,当用户从设置中返回时,PermissionX 将会自动重新请求相应的权限,并将最终的授权结果回调到 request() 方法当中。效果如下图所示:

    同样,1.3.0 版本也支持了自定义这个对话框样式的功能,详情请参阅 PermissionX 重磅更新,支持自定义权限提醒对话框 。

    PermissionX 最主要的功能大概就是这些,不过我在使用一些 App 的时候发现,有些 App 喜欢在第一次请求权限之前就先弹出一个对话框向用户解释自己需要哪些权限,然后才会进行权限申请。这种做法是比较提倡的,因为用户同意授权的概率会更高。

    那么 PermissionX 中要如何实现这样的功能呢?

    其实非常简单,PermissionX 还提供了一个 explainReasonBeforeRequest() 方法,只需要将它也串接到 request() 方法之前就可以了,代码如下所示:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .explainReasonBeforeRequest()
    .onExplainRequestReason { deniedList ->
    showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白")
    }
    .onForwardToSettings { deniedList ->
    showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白")
    }
    .request { allGranted, grantedList, deniedList ->
    if (allGranted) {
    Toast.makeText(this, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
    } else {
    Toast.makeText(this, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
    }
    }


    这样,当每次请求权限时,会优先进入 onExplainRequestReason() 方法,弹出解释权限申请原因的对话框,用户点击我已明白按钮之后才会执行权限申请。效果如下图所示:

    不过,你在使用 explainReasonBeforeRequest() 方法时,其实还有一些关键的点需要注意。

    第一,单独使用 explainReasonBeforeRequest() 方法是无效的,必须配合 onExplainRequestReason() 方法一起使用才能起作用。这个很好理解,因为没有配置 onExplainRequestReason() 方法,我们怎么向用户解释权限申请原因呢?

    第二,在使用 explainReasonBeforeRequest() 方法时,如果 onExplainRequestReason() 方法中编写了权限过滤的逻辑,最终的运行结果可能和你期望的会不一致。这一点可能会稍微有点难理解,我用一个具体的示例来解释一下。

    观察如下代码:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .explainReasonBeforeRequest()
    .onExplainRequestReason { deniedList ->
    val filteredList = deniedList.filter {
    it == Manifest.permission.CAMERA
    }
    showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白")
    }
    ...


    这里在 onExplainRequestReason() 方法中编写了刚才用到的权限过滤逻辑,当有多个权限被拒绝时,我们只重新申请 CAMERA 权限。

    在没有加入 explainReasonBeforeRequest() 方法时,一切都可以按照我们所预期的那样正常运行。但如果加上了 explainReasonBeforeRequest() 方法,在执行权限请求之前会先进入 onExplainRequestReason() 方法,而这里将除了 CAMERA 之外的其他权限都过滤掉了,因此实际上 PermissionX 只会请求 CAMERA 这一个权限,剩下的权限将完全不会尝试去请求,而是直接作为被拒绝的权限回调到最终的 request() 方法当中。

    效果如下图所示:

    针对于这种情况,PermissionX 在 onExplainRequestReason() 方法中提供了一个额外的 beforeRequest 参数,用于标识当前上下文是在权限请求之前还是之后,借助这个参数在 onExplainRequestReason() 方法中执行不同的逻辑,即可很好地解决这个问题,示例代码如下:

    PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .explainReasonBeforeRequest()
    .onExplainRequestReason { deniedList, beforeRequest ->
    if (beforeRequest) {
    showRequestReasonDialog(deniedList, "为了保证程序正常工作,请您同意以下权限申请", "我已明白")
    } else {
    val filteredList = deniedList.filter {
    it == Manifest.permission.CAMERA
    }
    showRequestReasonDialog(filteredList, "摄像机权限是程序必须依赖的权限", "我已明白")
    }
    }
    ...


    可以看到,当 beforeRequest 为 true 时,说明此时还未执行权限申请,那么我们将完整的 deniedList 传入 showRequestReasonDialog() 方法当中。

    而当 beforeRequest 为 false 时,说明某些权限被用户拒绝了,此时我们只重新申请 CAMERA 权限,因为它是必不可少的,其他权限则可有可无。

    最终运行效果如下:

    代码下载:XPermission-master.zip

    收起阅读 »

    Android自定义View 雷达扫描效果

    最近在做一个项目,其中有一个页面是要做一个类似于雷达扫描的效果。于是找了其他应用的类似的效果参考一下,刚好我使用的华为手机里的手机管家--病毒查杀页面就是一个雷达扫描的效果。而且看它的样式也挺不错的,刚好符合我的要求。所以就决定仿照它的样式自定义一个类似效果的...
    继续阅读 »

    最近在做一个项目,其中有一个页面是要做一个类似于雷达扫描的效果。于是找了其他应用的类似的效果参考一下,刚好我使用的华为手机里的手机管家--病毒查杀页面就是一个雷达扫描的效果。而且看它的样式也挺不错的,刚好符合我的要求。所以就决定仿照它的样式自定义一个类似效果的RadarView。 这是华为手机管家的效果:

    图片
    我写完这个RadarView之后觉得这个View的实现虽然不难,却使用到了自定义属性、View的Measure、Paint、Canvas和坐标的计算等这些自定义View常用的知识,是一个不错的自定义View练习例子,所以决定写一篇博客把它记录起来。

    由于我需要雷达的扫描效果,所以画中间的百分比数字。RadarView可以根据自己的需求配置View的主题颜色、扫描颜色、扫描速度、圆圈数量、是否显示水滴等功能样式,方便实现各种样式的情况。下面是自定义RadarView的代码。

    public class RadarView extends View {

    //默认的主题颜色
    private int DEFAULT_COLOR = Color.parseColor("#91D7F4");

    // 圆圈和交叉线的颜色
    private int mCircleColor = DEFAULT_COLOR;
    //圆圈的数量 不能小于1
    private int mCircleNum = 3;
    //扫描的颜色 RadarView会对这个颜色做渐变透明处理
    private int mSweepColor = DEFAULT_COLOR;
    //水滴的颜色
    private int mRaindropColor = DEFAULT_COLOR;
    //水滴的数量 这里表示的是水滴最多能同时出现的数量。因为水滴是随机产生的,数量是不确定的
    private int mRaindropNum = 4;
    //是否显示交叉线
    private boolean isShowCross = true;
    //是否显示水滴
    private boolean isShowRaindrop = true;
    //扫描的转速,表示几秒转一圈
    private float mSpeed = 3.0f;
    //水滴显示和消失的速度
    private float mFlicker = 3.0f;

    private Paint mCirclePaint;// 圆的画笔
    private Paint mSweepPaint; //扫描效果的画笔
    private Paint mRaindropPaint;// 水滴的画笔

    private float mDegrees; //扫描时的扫描旋转角度。
    private boolean isScanning = false;//是否扫描

    //保存水滴数据
    private ArrayList mRaindrops = new ArrayList<>();

    public RadarView(Context context) {
    super(context);
    init();
    }

    public RadarView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    getAttrs(context, attrs);
    init();
    }

    public RadarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    getAttrs(context, attrs);
    init();
    }

    /**
    * 获取自定义属性值
    *
    * @param context
    * @param attrs
    */

    private void getAttrs(Context context, AttributeSet attrs) {
    if (attrs != null) {
    TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.RadarView);
    mCircleColor = mTypedArray.getColor(R.styleable.RadarView_circleColor, DEFAULT_COLOR);
    mCircleNum = mTypedArray.getInt(R.styleable.RadarView_circleNum, mCircleNum);
    if (mCircleNum < 1) {
    mCircleNum = 3;
    }
    mSweepColor = mTypedArray.getColor(R.styleable.RadarView_sweepColor, DEFAULT_COLOR);
    mRaindropColor = mTypedArray.getColor(R.styleable.RadarView_raindropColor, DEFAULT_COLOR);
    mRaindropNum = mTypedArray.getInt(R.styleable.RadarView_raindropNum, mRaindropNum);
    isShowCross = mTypedArray.getBoolean(R.styleable.RadarView_showCross, true);
    isShowRaindrop = mTypedArray.getBoolean(R.styleable.RadarView_showRaindrop, true);
    mSpeed = mTypedArray.getFloat(R.styleable.RadarView_speed, mSpeed);
    if (mSpeed <= 0) {
    mSpeed = 3;
    }
    mFlicker = mTypedArray.getFloat(R.styleable.RadarView_flicker, mFlicker);
    if (mFlicker <= 0) {
    mFlicker = 3;
    }
    mTypedArray.recycle();
    }
    }

    /**
    * 初始化
    */

    private void init() {
    // 初始化画笔
    mCirclePaint = new Paint();
    mCirclePaint.setColor(mCircleColor);
    mCirclePaint.setStrokeWidth(1);
    mCirclePaint.setStyle(Paint.Style.STROKE);
    mCirclePaint.setAntiAlias(true);

    mRaindropPaint = new Paint();
    mRaindropPaint.setStyle(Paint.Style.FILL);
    mRaindropPaint.setAntiAlias(true);

    mSweepPaint = new Paint();
    mSweepPaint.setAntiAlias(true);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //设置宽高,默认200dp
    int defaultSize = dp2px(getContext(), 200);
    setMeasuredDimension(measureWidth(widthMeasureSpec, defaultSize),
    measureHeight(heightMeasureSpec, defaultSize));
    }

    /**
    * 测量宽
    *
    * @param measureSpec
    * @param defaultSize
    * @return
    */

    private int measureWidth(int measureSpec, int defaultSize) {
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if (specMode == MeasureSpec.EXACTLY) {
    result = specSize;
    } else {
    result = defaultSize + getPaddingLeft() + getPaddingRight();
    if (specMode == MeasureSpec.AT_MOST) {
    result = Math.min(result, specSize);
    }
    }
    result = Math.max(result, getSuggestedMinimumWidth());
    return result;
    }

    /**
    * 测量高
    *
    * @param measureSpec
    * @param defaultSize
    * @return
    */

    private int measureHeight(int measureSpec, int defaultSize) {
    int result = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if (specMode == MeasureSpec.EXACTLY) {
    result = specSize;
    } else {
    result = defaultSize + getPaddingTop() + getPaddingBottom();
    if (specMode == MeasureSpec.AT_MOST) {
    result = Math.min(result, specSize);
    }
    }
    result = Math.max(result, getSuggestedMinimumHeight());
    return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {

    //计算圆的半径
    int width = getWidth() - getPaddingLeft() - getPaddingRight();
    int height = getHeight() - getPaddingTop() - getPaddingBottom();
    int radius = Math.min(width, height) / 2;

    //计算圆的圆心
    int cx = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
    int cy = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;

    drawCircle(canvas, cx, cy, radius);

    if (isShowCross) {
    drawCross(canvas, cx, cy, radius);
    }

    //正在扫描
    if (isScanning) {
    if (isShowRaindrop) {
    drawRaindrop(canvas, cx, cy, radius);
    }
    drawSweep(canvas, cx, cy, radius);
    //计算雷达扫描的旋转角度
    mDegrees = (mDegrees + (360 / mSpeed / 60)) % 360;

    //触发View重新绘制,通过不断的绘制实现View的扫描动画效果
    invalidate();
    }
    }

    /**
    * 画圆
    */

    private void drawCircle(Canvas canvas, int cx, int cy, int radius) {
    //画mCircleNum个半径不等的圆圈。
    for (int i = 0; i < mCircleNum; i++) {
    canvas.drawCircle(cx, cy, radius - (radius / mCircleNum * i), mCirclePaint);
    }
    }

    /**
    * 画交叉线
    */

    private void drawCross(Canvas canvas, int cx, int cy, int radius) {
    //水平线
    canvas.drawLine(cx - radius, cy, cx + radius, cy, mCirclePaint);

    //垂直线
    canvas.drawLine(cx, cy - radius, cx, cy + radius, mCirclePaint);
    }

    /**
    * 生成水滴。水滴的生成是随机的,并不是每次调用都会生成一个水滴。
    */

    private void generateRaindrop(int cx, int cy, int radius) {

    // 最多只能同时存在mRaindropNum个水滴。
    if (mRaindrops.size() < mRaindropNum) {
    // 随机一个20以内的数字,如果这个数字刚好是0,就生成一个水滴。
    // 用于控制水滴生成的概率。
    boolean probability = (int) (Math.random() * 20) == 0;
    if (probability) {
    int x = 0;
    int y = 0;
    int xOffset = (int) (Math.random() * (radius - 20));
    int yOffset = (int) (Math.random() * (int) Math.sqrt(1.0 * (radius - 20) * (radius - 20) - xOffset * xOffset));

    if ((int) (Math.random() * 2) == 0) {
    x = cx - xOffset;
    } else {
    x = cx + xOffset;
    }

    if ((int) (Math.random() * 2) == 0) {
    y = cy - yOffset;
    } else {
    y = cy + yOffset;
    }

    mRaindrops.add(new Raindrop(x, y, 0, mRaindropColor));
    }
    }
    }

    /**
    * 删除水滴
    */

    private void removeRaindrop() {
    Iterator iterator = mRaindrops.iterator();

    while (iterator.hasNext()) {
    Raindrop raindrop = iterator.next();
    if (raindrop.radius > 20 || raindrop.alpha < 0) {
    iterator.remove();
    }
    }
    }

    /**
    * 画雨点(就是在扫描的过程中随机出现的点)。
    */

    private void drawRaindrop(Canvas canvas, int cx, int cy, int radius) {
    generateRaindrop(cx, cy, radius);
    for (Raindrop raindrop : mRaindrops) {
    mRaindropPaint.setColor(raindrop.changeAlpha());
    canvas.drawCircle(raindrop.x, raindrop.y, raindrop.radius, mRaindropPaint);
    //水滴的扩散和透明的渐变效果
    raindrop.radius += 1.0f * 20 / 60 / mFlicker;
    raindrop.alpha -= 1.0f * 255 / 60 / mFlicker;
    }
    removeRaindrop();
    }

    /**
    * 画扫描效果
    */

    private void drawSweep(Canvas canvas, int cx, int cy, int radius) {
    //扇形的透明的渐变效果
    SweepGradient sweepGradient = new SweepGradient(cx, cy,
    new int[]{Color.TRANSPARENT, changeAlpha(mSweepColor, 0), changeAlpha(mSweepColor, 168),
    changeAlpha(mSweepColor, 255), changeAlpha(mSweepColor, 255)
    }, new float[]{0.0f, 0.6f, 0.99f, 0.998f, 1f});
    mSweepPaint.setShader(sweepGradient);
    //先旋转画布,再绘制扫描的颜色渲染,实现扫描时的旋转效果。
    canvas.rotate(-90 + mDegrees, cx, cy);
    canvas.drawCircle(cx, cy, radius, mSweepPaint);
    }

    /**
    * 开始扫描
    */

    public void start() {
    if (!isScanning) {
    isScanning = true;
    invalidate();
    }
    }

    /**
    * 停止扫描
    */

    public void stop() {
    if (isScanning) {
    isScanning = false;
    mRaindrops.clear();
    mDegrees = 0.0f;
    }
    }

    /**
    * 水滴数据类
    */

    private static class Raindrop {

    int x;
    int y;
    float radius;
    int color;
    float alpha = 255;

    public Raindrop(int x, int y, float radius, int color) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
    }

    /**
    * 获取改变透明度后的颜色值
    *
    * @return
    */

    public int changeAlpha() {
    return RadarView.changeAlpha(color, (int) alpha);
    }

    }

    /**
    * dp转px
    */

    private static int dp2px(Context context, float dpVal) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
    dpVal, context.getResources().getDisplayMetrics());
    }

    /**
    * 改变颜色的透明度
    *
    * @param color
    * @param alpha
    * @return
    */

    private static int changeAlpha(int color, int alpha) {
    int red = Color.red(color);
    int green = Color.green(color);
    int blue = Color.blue(color);
    return Color.argb(alpha, red, green, blue);
    }
    }

    自定义属性:在res/values下创建attrs.xml


























    效果图:

    效果图

    代码下载:mirrors-XHRadarView-master.zip
    收起阅读 »

    Android右侧边栏滚动选择

    Android右侧边栏滚动选择涉及到的内容:首先会ListView或RecyclerView的多布局。自定义View右侧拼音列表,简单地绘制并设立监听事件等。会使用pinyin4.jar第三方包来识别汉字的首字母(单独处理重庆多音问题)。将全部的城市列表转化为...
    继续阅读 »

    Android右侧边栏滚动选择

    涉及到的内容:

    1. 首先会ListView或RecyclerView的多布局。

    2. 自定义View右侧拼音列表,简单地绘制并设立监听事件等。

    3. 会使用pinyin4.jar第三方包来识别汉字的首字母(单独处理重庆多音问题)。

    4. 将全部的城市列表转化为{A a开头城市名...,B b开头城市名...}的格式,这个数据转化是重点 !!!

    5. 将第三步获取的数据来多布局展示出来。

    难点:

    1、RecyclerView的滑动问题

    2、RecyclerView的点击问题

    3、绘制SideBar

    先来看个图,看是不是你想要的

    1557800237747.gif

    实现思路

    根据城市和拼音列表,可以想到多布局,这里无非是把城市名称按其首字母进行排列后再填充列表,如果给你一组数据{A、城市1、城市2、B、城市3、城市4...}这样的数据让你填充你总会吧,无非就是两种布局,将拼音和汉字的背景设置不同就行;右侧是个自定义布局,别说你不会自定义布局,不会也行,这个很简单,无非是平分高度,通过drawText()绘制字母,然后进行滑动监听,右侧滑动或点击到哪里,左侧列表相应进行滚动即可。

    其实原先我已经通过ListView做过了,这次回顾使用RecyclerView再实现一次,发现还遇到了一些新东西,带你们看看。这次没有使用BaseQuickAdapter,使用多了都忘记原始的代码怎么敲了话不多说开撸吧

    1. 确定数据格式

    首先我们需要确定下Bean的数据格式,毕竟涉及到多布局

    public class ItemBean {

    private String itemName;//城市名或者字母A...
    private String itemType;//类型,区分是首字母还是城市名,是首字母的写“head”,不是的填入其它字母都行

    // 标记 拼音头,head为0
    public static final int TYPE_HEAD = 0;
    // 标记 城市名
    public static final int TYPE_CITY = 1;

    public int getType() {
    if (itemType.equals("head")) {
    return TYPE_HEAD;
    } else {
    return TYPE_CITY;
    }
    }
    ......Get Set方法
    }

    可以看到有两个字段,一个用来显示城市名或者字母,另一个用来区分是城市还是首字母。这里定义了个getType()方法,为字母的话返回0,城市名返回1

    2. 整理数据

    一般我们准备的数据都是这样的


    "mycityarray">
    北京市
    上海市
    广州市
    天津市
    石家庄市
    唐山市
    秦皇岛市
    邯郸市
    邢台市
    保定市
    张家口市
    承德市市
    沧州市
    廊坊市
    衡水市
    ......



    想要得到我们那样的数据,需要先获取这些城市名的首字母然后进行排序,这里我使用pinyin4j-2.5.0.jar进行汉字到拼音的转化,jar下载地址

    2.1 编写工具类

    public class HanziToPinYin {
    /**
    * 如果字符串string是汉字,则转为拼音并返回,返回的是首字母
    *
    @param string
    *
    @return
    */

    public static char toPinYin(String string){
    HanyuPinyinOutputFormat hanyuPinyin = new HanyuPinyinOutputFormat();
    hanyuPinyin.setCaseType(HanyuPinyinCaseType.UPPERCASE);
    hanyuPinyin.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
    hanyuPinyin.setVCharType(HanyuPinyinVCharType.WITH_U_UNICODE);
    String[] pinyinArray=null;
    char hanzi = string.charAt(0);
    try {
    //是否在汉字范围内
    if(hanzi>=0x4e00 && hanzi<=0x9fa5){
    pinyinArray = PinyinHelper.toHanyuPinyinStringArray(hanzi, hanyuPinyin);
    }
    } catch (BadHanyuPinyinOutputFormatCombination e) {
    e.printStackTrace();
    }
    //将获取到的拼音返回,只返回其首字母
    return pinyinArray[0].charAt(0);
    }
    }

    2.2 整理数据

    private List cityList;      //给定的所有的城市名
    private List itemList; //整理后的所有的item子项,可能是城市、可能是字母

    //初始化数据,将所有城市进行排序,且加上字母和它们一起形成新的集合
    private void initData(){

    itemList = new ArrayList<>();
    //获取所有的城市名
    String[] cityArray = getResources().getStringArray(R.array.mycityarray);
    cityList = Arrays.asList(cityArray);
    //将所有城市进行排序,排完后cityList内所有的城市名都是按首字母进行排序的
    Collections.sort(cityList, new CityComparator());

    //将剩余的城市加进去
    for (int i = 0; i < cityList.size(); i++) {

    String city = cityList.get(i);
    String letter = null; //当前所属的字母

    if (city.contains("重庆")) {
    letter = HanziToPinYin.toPinYin("崇庆") + "";
    } else {
    letter = HanziToPinYin.toPinYin(cityList.get(i)) + "";
    }

    if (letter.equals(currentLetter)) { //在A字母下,属于当前字母
    itemBean = new ItemBean();
    itemBean.setItemName(city); //把汉字放进去
    itemBean.setItemType(letter); //这里放入其它不是“head”的字符串就行
    itemList.add(itemBean);
    } else { //不在当前字母下,先将该字母取出作为独立的一个item
    //添加标签(B...)
    itemBean = new ItemBean();
    itemBean.setItemName(letter); //把首字母进去
    itemBean.setItemType("head"); //把head标签放进去
    currentLetter = letter;
    itemList.add(itemBean);

    //添加城市
    itemBean = new ItemBean();
    itemBean.setItemName(city); //把汉字放进去
    itemBean.setItemType(letter); //把拼音放进去
    itemList.add(itemBean);
    }
    }
    }

    经过以上步骤就将原先的数据整理成了以下形式排列的一组数据

    {
    {itemName:"A",itemType:"head"}
    {itemName:"阿拉善盟",itemType:"A"}
    {itemName:"安抚市",itemType:"A"}
    ...
    {itemName:"巴中市",itemType:"B"}
    {itemName:"白山市",itemType:"B"}
    ....
    }

    等等,上面有个Collections.sort(cityList, new CityComparator());letter = HanziToPinYin.toPinYin("崇庆") + "";你可能还会有疑惑,我就来多几嘴 因为pinyin4j.jar这个jar包在将汉字转为拼音的时候,会将重庆的拼音转为zhongqin,所以在排序和获取首字母的时候都需要单独处理

    public class CityComparator implements Comparator<String> {

    private RuleBasedCollator collator;

    public CityComparator() {
    collator = (RuleBasedCollator) Collator.getInstance(Locale.CHINA);
    }

    @Override
    public int compare(String lhs, String rhs) {

    lhs = lhs.replace("重庆", "崇庆");
    rhs = rhs.replace("重庆", "崇庆");
    CollationKey c1 = collator.getCollationKey(lhs);
    CollationKey c2 = collator.getCollationKey(rhs);

    return c1.compareTo(c2);
    }
    }

    这里先指定RuleBasedCollator语言环境为CHINA,然后在compare()比较方法里,如果遇到两边有"重庆"的字符串,就将其替换为”崇庆“,然后通过getCollationKey()获取首个字符然后进行比较。

    letter = HanziToPinYin.toPinYin("崇庆") + "";获取首字母的时候也是同样,不是获取"重庆"的首字母而是"崇庆"的首字母。

    看到这样的一组数据你总会根据多布局来给RecyclerView填充数据了吧

    3. RecyclerView填充数据

    既然涉及到多布局,那么有几种布局就该有几个ViewHolder,这次我将采用原始的写法,不用BaseQuickAdapter,那个太方便搞得我原始的都不会写了

    新建CityAdapter类,让这个适配器继承自RecyclerView.Adapter,并将泛型指定为RecyclerView.ViewHolder,其代表我们在CityAdapter中定义的内部类

    public class CityAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

    ......
    //字母头
    public static class HeadViewHolder extends RecyclerView.ViewHolder {
    private TextView tvHead;
    public HeadViewHolder(View itemView) {
    super(itemView);
    tvHead = itemView.findViewById(R.id.tv_item_head);
    }
    }

    //城市
    public static class CityViewHolder extends RecyclerView.ViewHolder {

    private TextView tvCity;
    public CityViewHolder(View itemView) {
    super(itemView);
    tvCity = itemView.findViewById(R.id.tv_item_city);
    }
    }
    }

    重写onCreateViewHolder()onBindViewHolder()getItemCount()方法,因为涉及多布局,还需重写getItemViewType()方法来区分是哪种布局

    完整代码如下

    public class CityAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    //数据项
    private List dataList;
    //点击事件监听接口
    private OnRecyclerViewClickListener onRecyclerViewClickListener;

    public void setOnItemClickListener(OnRecyclerViewClickListener onItemClickListener) {
    this.onRecyclerViewClickListener = onItemClickListener;
    }
    public CityAdapter(List dataList) {
    this.dataList = dataList;
    }
    //创建ViewHolder实例
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {

    if (viewType == 0) { //Head头字母名称
    View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_head, viewGroup,false);
    RecyclerView.ViewHolder headViewHolder = new HeadViewHolder(view);
    return headViewHolder;
    } else { //城市名
    View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_city, viewGroup,false);
    RecyclerView.ViewHolder cityViewHolder = new CityViewHolder(view);
    view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    if (onRecyclerViewClickListener != null) {
    onRecyclerViewClickListener.onItemClickListener(v);
    }
    }
    });
    return cityViewHolder;
    }
    }
    //对子项数据进行赋值
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {

    int itemType = dataList.get(position).getType();
    if (itemType == 0) {
    HeadViewHolder headViewHolder = (HeadViewHolder) viewHolder;
    headViewHolder.tvHead.setText(dataList.get(position).getItemName());
    } else {
    CityViewHolder cityViewHolder = (CityViewHolder) viewHolder;
    cityViewHolder.tvCity.setText(dataList.get(position).getItemName());
    }
    }
    //数据项个数
    @Override
    public int getItemCount() {
    return dataList.size();
    }
    //区分布局类型
    @Override
    public int getItemViewType(int position) {
    int type = dataList.get(position).getType();
    return type;
    }
    //字母头
    public static class HeadViewHolder extends RecyclerView.ViewHolder {
    private TextView tvHead;
    public HeadViewHolder(View itemView) {
    super(itemView);
    tvHead = itemView.findViewById(R.id.tv_item_head);
    }
    }
    //城市
    public static class CityViewHolder extends RecyclerView.ViewHolder {
    private TextView tvCity;
    public CityViewHolder(View itemView) {
    super(itemView);
    tvCity = itemView.findViewById(R.id.tv_item_city);
    }
    }
    }

    两种item布局都是只放了一个TextView控件

    这里有两处自己碰到和当时使用ListView不同的地方:

    1、RecyclerView没有setOnItemClickListener(),需要自己定义接口来实现 2、自己平时加载布局都直接是View view = LayoutInflater.from(context).inflate(R.layout.item_head, null);,也没发现什么问题,但此次就出现了Item子布局无法横向铺满父布局。 解决办法:将改为以下方式加载布局

    View view = LayoutInflater.from(context).inflate(R.layout.item_head, viewGroup,false);

    (如果遇到不能铺满状况也可能是RecyclerView没有明确宽高而是用权重代替的原因)

    建立的监听器

    public interface OnRecyclerViewClickListener {
    void onItemClickListener(View view);
    }


    4. 绘制侧边字母栏

    这里的自定义很简单,无非是定义画笔,然后在画布上通过drawText()方法来绘制Text即可。

    4.1 首先定义类SideBar继承自View,重写构造方法,并在三个方法内调用自定义的init();方法来初始化画笔

    public class SideBar extends View {
    //画笔
    private Paint paint;

    public SideBar(Context context) {
    super(context);
    init();
    }
    public SideBar(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
    }
    public SideBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
    }
    //初始化画笔工具
    private void init() {
    paint = new Paint();
    paint.setAntiAlias(true);//抗锯齿
    }
    }

    4.2 在onDraw()方法里绘制字母

    public static String[] characters = new String[]{"❤", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
    private int position = -1; //当前选中的位置
    private int defaultTextColor = Color.parseColor("#D2D2D2"); //默认拼音文字的颜色
    private int selectedTextColor = Color.parseColor("#2DB7E1"); //选中后的拼音文字的颜色

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int height = getHeight(); //当前控件高度
    int width = getWidth(); //当前控件宽度
    int singleHeight = height / characters.length; //每个字母占的长度

    for (int i = 0; i < characters.length; i++) {
    if (i == position) { //当前选中
    paint.setColor(selectedTextColor); //设置选中时的画笔颜色
    } else { //未选中
    paint.setColor(defaultTextColor); //设置未选中时的画笔颜色
    }
    paint.setTextSize(textSize); //设置字体大小

    //设置绘制的位置
    float xPos = width / 2 - paint.measureText(characters[i]) / 2;
    float yPos = singleHeight * i + singleHeight;

    canvas.drawText(characters[i], xPos, yPos, paint); //绘制文本
    }
    }

    通过以上两步,右侧边栏就算绘制完成了,但这只是静态的,如果要实现侧边栏滑动的时候,我们还需要监听其触摸事件

    4.3 定义触摸回调接口和设置监听器的方法

    //设置触摸位置改变的监听器的方法
    public void setOnTouchingLetterChangedListener(OnTouchingLetterChangedListener onTouchingLetterChangedListener) {
    this.onTouchingLetterChangedListener = onTouchingLetterChangedListener;
    }

    //触摸位置更改的接口
    public interface OnTouchingLetterChangedListener {
    void onTouchingLetterChanged(int position);
    }

    4.4 触摸事件

    @Override
    public boolean onTouchEvent(MotionEvent event) {

    int action = event.getAction();
    float y = event.getY();
    position = (int) (y / (getHeight() / characters.length)); //获取触摸的位置

    if (position >= 0 && position < characters.length) {
    //触摸位置变化的回调
    onTouchingLetterChangedListener.onTouchingLetterChanged(position);

    switch (action) {
    case MotionEvent.ACTION_UP:
    setBackgroundColor(Color.TRANSPARENT);//手指起来后的背景变化
    position = -1;
    invalidate();//重新绘制控件
    if (text_dialog != null) {
    text_dialog.setVisibility(View.INVISIBLE);
    }
    break;
    default://手指按下
    setBackgroundColor(touchedBgColor);
    invalidate();
    text_dialog.setText(characters[position]);//字母框的弹出
    break;
    }
    } else {
    setBackgroundColor(Color.TRANSPARENT);
    if (text_dialog != null) {
    text_dialog.setVisibility(View.INVISIBLE);
    }
    }
    return true; //一定要返回true,表示拦截了触摸事件
    }

    具体的解释如代码所示,当手指起来时,position为-1,当手指按下,更改背景并弹出字母框(这里的字母框其实就是一个TextView,通过显示隐藏来表示其弹出)

    5. Activity中使用

    itemList数据填充那些就不写了,在前面整理数据那部分

    //所有的item子项,可能是城市、可能是字母
    private List itemList;
    //目标项是否在最后一个可见项之后
    private boolean mShouldScroll;
    //记录目标项位置(要移动到的位置)
    private int mToPosition;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    //为左侧RecyclerView设立Item的点击事件
    cityAdapter.setOnItemClickListener(this);

    sideBar.setOnTouchingLetterChangedListener(new SideBar.OnTouchingLetterChangedListener() {
    @Override
    public void onTouchingLetterChanged(int position) {

    String city_label = SideBar.characters[position]; //滑动到的字母
    for (int i = 0; i < cityList.size(); i++) {
    if (itemList.get(i).getItemName().equals(city_label)) {
    moveToPosition(i); //直接滚过去
    // smoothMoveToPosition(recyclerView,i); //平滑的滚动
    tvDialog.setVisibility(View.VISIBLE);
    break;
    }
    if (i == cityList.size() - 1) {
    tvDialog.setVisibility(View.INVISIBLE);
    }
    }
    }
    });
    }

    //实战中可能会有选择完后此页面关闭,返回当前数据等操作,可在此处完成
    @Override
    public void onItemClickListener(View view) {
    int position = recyclerView.getChildAdapterPosition(view);
    Toast.makeText(view.getContext(), itemList.get(position).getItemName(), Toast.LENGTH_SHORT).show();
    }

    在使用ListView的时候,知道要移动到的位置position时,直接listView.setSelection(position)就可将当前的item移动到屏幕顶部,而RecyclerView的scrollToPosition(position)只是将item移动到屏幕内,所以需要我们通过scrollToPositionWithOffset()方法将其置顶

    private void moveToPosition(int position) {
    if (position != -1) {
    recyclerView.scrollToPosition(position);
    LinearLayoutManager mLayoutManager =
    (LinearLayoutManager) recyclerView.getLayoutManager();
    mLayoutManager.scrollToPositionWithOffset(position, 0);
    }
    }

    6. 总结

    再次说明下自己遇到的几个问题:

    1、点击问题,ListViewsetOnItemClickListener()方法,而RecyclerView没有,需要建立接口进行监听。 2、滑动问题,listViewsetSelection(position)滑动可以直接将该项滑至屏幕顶部,而recyclerView的 smoothScrollToPosition(position);只是将其移动至屏幕内,需要再次进行处理。 3、listViewisEnable() 方法可以设置字母Item不能点击,而城市名Item可以点击,recycleView的实现(直接在设立点击事件的时候,是头部就不设立点击事件就行) 4、item不充满全屏,加载布局的原因


    代码下载:AndroidSlidbar.zip

    收起阅读 »

    Android 抛弃旧有逆向方式,如何快速逆向:FakerAndroid

    FakerAndroidA tool translate apk file to common android project and support so hook and include il2cpp c++ scaffolding when apk is...
    继续阅读 »

    FakerAndroid

    A tool translate apk file to common android project and support so hook and include il2cpp c++ scaffolding when apk is a il2cpp game apk

    简介

    • 优雅地在一个Apk上写代码
    • 直接将Apk文件转换为可以进行二次开发的Android项目的工具,支持so hook,对于il2cpp的游戏apk直接生成il2cpp c++脚手架
    • 将痛苦的逆向环境,转化为舒服的开发环境,告别汇编,告别二进制,还有啥好说的~~

    特点

    • 提供Java层代码覆盖及继承替换的脚手架,实现java与smali混编
    • 提供so函数Hook Api
    • 对于il2cpp的游戏apk直接生成il2cpp c++脚手架
    • Java层标准的对原有Java api的AndroidStudio编码提示
    • Smali文件修改后运行或打包时自动回编译(AndroidStudio project 文件树模式下可以直接找到smali文件,支持对smali修改,最小文件数增量编译)
    • 对于il2cpp的游戏apk,标准的Jni对原有il2cpp脚本的编码提示
    • 无限的可能性和扩展性,能干啥你说了算~
    • Dex折叠,对敏感已经存在或后续接入的代码进行隐藏规避静态分析

    运行环境

    使用方式

    • 下载FakerAndroid.jar(2020/11/15/16:53:00)
    • cmd命令行 cd <FakerAndroid.jar平级目录>
    • cmd命令行 java -jar FakerAndroid.jar fk <apkpath>(项目生成路径与apk文件平级) 或 java -jar FakerAndroid.jar fk <apkpath> -o <outdir>
    • 例:java -jar FakerAndroid.jar fk D:\apk\test.apk或 java -jar FakerAndroid.jar fk D:\apk\test.apk -o D:\test

    或者使用方式

    • 下载FakerAndroid-AS.zip(2020/11/15/16:53:00)
    • AS->File-Settings->Plugin->SettingIcon->InstallPlugin Plugin From Disk(选择FakerAndroid-AS.zip-安装-启用)->重启AndroidStudio
    • AS->File->FakerAndroid->选择目标Apk文件

    生成的Android项目二次开发教程(图文教程)

    1、打开项目
    • Android studio直接打开工具生成的Android项目
    • 保持跟目录build.gradle中依赖固定,请勿配置AndroidGradlePlugin,且项目配置NDk版本为21
    • 存在已知缺陷,res下的部分资源文件编译不过,需要手动修复一下,部分Manifest标签无法编译需要手动修复
      (关于Res混淆手动实验了几个,如果遇到了这个问题,可以手动尝试,只要保证res/public.xml中的name对应的资源文件可以正常链路下去然后修复到可编译的程度,程序运行时一般是没有res问题,太完美的解决方案尚未完成)
    2、调试运行项目
    • 连接测试机机
    • Run项目
    3、进阶
    • 类调用
      借助javaScaffoding 在主模块(app/src/main/java)编写java代码对smali代码进行调用
    • 类替换
      在主模块(app/src/main/java)直接编写Java类,类名与要替换的类的smali文件路径对应
    • Smali 增量编译
      你可以使用传统的smali修改方式对smali代码进行修改,且编译方式为最小文件数增量编译,smali文件修改后javascaffoding会同步,比如遇到final或private的java元素无法掉用时可以先修改smali(执行一次编译后javaScaffoding会同步)
    • So Hook
      借助FakeCpp 使用jni对so函数进行hook替换
    • il2cpp unity游戏脚本二次开发
      借助il2cpp Scaffolding 和FakeCpp,使用jni对原il2cpp游戏脚本进行Hook调用
    • Dex折叠
      build.gradle 配置sensitiveOptions用于隐藏敏感的dex代码,以规避静态分析,(Dex缓存原因在app版本号不变的情况使用第一次缓存,配置项调试请卸载后运行)
    4、正在路上

    resources.arsc decode 兼容,目前混淆某些大型 apk Res decoder有问题
    各种不理想情况兼容

    5、兼容性

    1、目前某些大型的apk资做过资源文件混淆的会有问题!
    2、Google play 90% 游戏apk可以一马平川
    3、加固Apk需要先脱壳后才能,暴漏java api
    4、有自校验的Apk,须项目运行起来后自行检查破解
    5、Manifest莫名奇妙的问题,可以先尝试注释掉异常代码,逐步还原试试
    6、Java OOM issue
    7、AS打不开,试试Help->Change Memery Settings(搞大点)

    github地址:https://github.com/Efaker/FakerAndroid

    下载地址:FakerAndroid.zip


    收起阅读 »

    一行代码集成Android推送!一个轻量级、可插拔的Android消息推送框架。

    快速集成指南添加Gradle依赖1.先在项目根目录的 build.gradle 的 repositories 添加:allprojects { repositories { ... maven { url "https:...
    继续阅读 »

    快速集成指南

    添加Gradle依赖

    1.先在项目根目录的 build.gradle 的 repositories 添加:

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

    2.添加XPush主要依赖:

    dependencies {
    ...
    //推送核心库
    implementation 'com.github.xuexiangjys.XPush:xpush-core:1.0.1'
    //推送保活库
    implementation 'com.github.xuexiangjys.XPush:keeplive:1.0.1'
    }

    3.添加第三方推送依赖(根据自己的需求进行添加,当然也可以全部添加)

    dependencies {
    ...
    //选择你想要集成的推送库
    implementation 'com.github.xuexiangjys.XPush:xpush-jpush:1.0.1'
    implementation 'com.github.xuexiangjys.XPush:xpush-umeng:1.0.1'
    implementation 'com.github.xuexiangjys.XPush:xpush-huawei:1.0.1'
    implementation 'com.github.xuexiangjys.XPush:xpush-xiaomi:1.0.1'
    implementation 'com.github.xuexiangjys.XPush:xpush-xg:1.0.1'
    }

    初始化XPush配置

    1.注册消息推送接收器。方法有两种,选其中一种就行了。

    • 如果你想使用XPushManager提供的消息管理,直接在AndroidManifest.xml中注册框架默认提供的XPushReceiver。当然你也可以继承XPushReceiver,并重写相关方法。

    • 如果你想实现自己的消息管理,可继承AbstractPushReceiver类,重写里面的方法,并在AndroidManifest.xml中注册。

        <!--自定义消息推送接收器-->
    <receiver android:name=".push.CustomPushReceiver">
    <intent-filter>
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_CONNECT_STATUS_CHANGED" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_NOTIFICATION" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_NOTIFICATION_CLICK" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_MESSAGE" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_COMMAND_RESULT" />

    <category android:name="${applicationId}" />
    </intent-filter>
    </receiver>

    <!--默认的消息推送接收器-->
    <receiver android:name="com.xuexiang.xpush.core.receiver.impl.XPushReceiver">
    <intent-filter>
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_CONNECT_STATUS_CHANGED" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_NOTIFICATION" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_NOTIFICATION_CLICK" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_MESSAGE" />
    <action android:name="com.xuexiang.xpush.core.action.RECEIVE_COMMAND_RESULT" />

    <category android:name="${applicationId}" />
    </intent-filter>
    </receiver>

    注意,如果你的Android设备是8.0及以上的话,静态注册的广播是无法正常生效的,解决的方法有两种:

    • 动态注册消息推送接收器

    • 修改推送消息的发射器

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    //Android8.0静态广播注册失败解决方案一:动态注册
    XPush.registerPushReceiver(new CustomPushReceiver());

    //Android8.0静态广播注册失败解决方案二:修改发射器
    XPush.setIPushDispatcher(new Android26PushDispatcherImpl(CustomPushReceiver.class));
    }

    2.在AndroidManifest.xml的application标签下,添加第三方推送客户端实现类.

    需要注意的是,这里注册的PlatformNamePlatformCode必须要和推送客户端实现类中的一一对应才行。

    <!--name格式:XPush_[PlatformName]_[PlatformCode]-->
    <!--value格式:对应客户端实体类的全类名路径-->

    <!--如果引入了xpush-jpush库-->
    <meta-data
    android:name="XPush_JPush_1000"
    android:value="com.xuexiang.xpush.jpush.JPushClient" />

    <!--如果引入了xpush-umeng库-->
    <meta-data
    android:name="XPush_UMengPush_1001"
    android:value="com.xuexiang.xpush.umeng.UMengPushClient" />

    <!--如果引入了xpush-huawei库-->
    <meta-data
    android:name="XPush_HuaweiPush_1002"
    android:value="com.xuexiang.xpush.huawei.HuaweiPushClient" />

    <!--如果引入了xpush-xiaomi库-->
    <meta-data
    android:name="XPush_MIPush_1003"
    android:value="com.xuexiang.xpush.xiaomi.XiaoMiPushClient" />

    <!--如果引入了xpush-xg库-->
    <meta-data
    android:name="XPush_XGPush_1004"
    android:value="@string/xpush_xg_client_name" />

    3.添加第三方AppKey和AppSecret.

    这里的AppKey和AppSecret需要我们到各自的推送平台上注册应用后获得。注意如果使用了xpush-xiaomi,那么需要在AndroidManifest.xml添加小米的AppKey和AppSecret(注意下面的“\ ”必须加上,否则获取到的是float而不是String,就会导致id和key获取不到正确的数据)。

    <!--极光推送静态注册-->
    <meta-data
    android:name="JPUSH_CHANNEL"
    android:value="default_developer" />
    <meta-data
    android:name="JPUSH_APPKEY"
    android:value="a32109db64ebe04e2430bb01" />

    <!--友盟推送静态注册-->
    <meta-data
    android:name="UMENG_APPKEY"
    android:value="5d5a42ce570df37e850002e9" />
    <meta-data
    android:name="UMENG_MESSAGE_SECRET"
    android:value="4783a04255ed93ff675aca69312546f4" />

    <!--华为HMS推送静态注册-->
    <meta-data
    android:name="com.huawei.hms.client.appid"
    android:value="101049475"/>

    <!--小米推送静态注册,下面的“\ ”必须加上,否则将无法正确读取-->
    <meta-data
    android:name="MIPUSH_APPID"
    android:value="\ 2882303761518134164"/>
    <meta-data
    android:name="MIPUSH_APPKEY"
    android:value="\ 5371813415164"/>

    <!--信鸽推送静态注册-->
    <meta-data
    android:name="XGPUSH_ACCESS_ID"
    android:value="2100343759" />
    <meta-data
    android:name="XGPUSH_ACCESS_KEY"
    android:value="A7Q26I8SH7LV" />

    4.在Application中初始化XPush

    初始化XPush的方式有两种,根据业务需要选择一种方式就行了:

    • 静态注册
    /**
    * 静态注册初始化推送
    */
    private void initPush() {
    XPush.debug(BuildConfig.DEBUG);
    //静态注册,指定使用友盟推送客户端
    XPush.init(this, new UMengPushClient());
    XPush.register();
    }
    • 动态注册
    /**
    * 动态注册初始化推送
    */
    private void initPush() {
    XPush.debug(BuildConfig.DEBUG);
    //动态注册,根据平台名或者平台码动态注册推送客户端
    XPush.init(this, new IPushInitCallback() {
    @Override
    public boolean onInitPush(int platformCode, String platformName) {
    String romName = RomUtils.getRom().getRomName();
    if (romName.equals(SYS_EMUI)) {
    return platformCode == HuaweiPushClient.HUAWEI_PUSH_PLATFORM_CODE && platformName.equals(HuaweiPushClient.HUAWEI_PUSH_PLATFORM_NAME);
    } else if (romName.equals(SYS_MIUI)) {
    return platformCode == XiaoMiPushClient.MIPUSH_PLATFORM_CODE && platformName.equals(XiaoMiPushClient.MIPUSH_PLATFORM_NAME);
    } else {
    return platformCode == JPushClient.JPUSH_PLATFORM_CODE && platformName.equals(JPushClient.JPUSH_PLATFORM_NAME);
    }
    }
    });
    XPush.register();
    }

    如何使用XPush

    1、推送的注册和注销

    • 通过调用XPush.register(),即可完成推送的注册。

    • 通过调用XPush.unRegister(),即可完成推送的注销。

    • 通过调用XPush.getPushToken(),即可获取消息推送的Token(令牌)。

    • 通过调用XPush.getPlatformCode(),即可获取当前使用推送平台的码。

    2、推送的标签(tag)处理

    • 通过调用XPush.addTags(),即可添加标签(支持传入多个)。

    • 通过调用XPush.deleteTags(),即可删除标签(支持传入多个)。

    • 通过调用XPush.getTags(),即可获取当前设备所有的标签。

    需要注意的是,友盟推送和信鸽推送目前暂不支持标签的获取,华为推送不支持标签的所有操作,小米推送每次只支持一个标签的操作。

    3、推送的别名(alias)处理

    • 通过调用XPush.bindAlias(),即可绑定别名。

    • 通过调用XPush.unBindAlias(),即可解绑别名。

    • 通过调用XPush.getAlias(),即可获取当前设备所绑定的别名。

    需要注意的是,友盟推送和信鸽推送目前暂不支持别名的获取,华为推送不支持别名的所有操作。

    4、推送消息的接收

    • 通过调用XPushManager.get().register()方法,注册消息订阅MessageSubscriber,即可在任意地方接收到推送的消息。

    • 通过调用XPushManager.get().unregister()方法,即可取消消息的订阅。

    这里需要注意的是,消息订阅的回调并不一定是在主线程,因此在回调中如果进行了UI的操作,一定要确保切换至主线程。下面演示代码中使用了我的另一个开源库XAOP,只通过@MainThread注解就能自动切换至主线程,可供参考。

    /**
    * 初始化监听
    */
    @Override
    protected void initListeners() {
    XPushManager.get().register(mMessageSubscriber);
    }

    private MessageSubscriber mMessageSubscriber = new MessageSubscriber() {
    @Override
    public void onMessageReceived(CustomMessage message) {
    showMessage(String.format("收到自定义消息:%s", message));
    }

    @Override
    public void onNotification(Notification notification) {
    showMessage(String.format("收到通知:%s", notification));
    }
    };

    @MainThread
    private void showMessage(String msg) {
    tvContent.setText(msg);
    }


    @Override
    public void onDestroyView() {
    XPushManager.get().unregister(mMessageSubscriber);
    super.onDestroyView();
    }

    5、推送消息的过滤处理

    • 通过调用XPushManager.get().addFilter()方法,可增加对订阅推送消息的过滤处理。对于一些我们不想处理的消息,可以通过消息过滤器将它们筛选出来。

    • 通过调用XPushManager.get().removeFilter()方法,即可去除消息过滤器。

    /**
    * 初始化监听
    */
    @Override
    protected void initListeners() {
    XPushManager.get().addFilter(mMessageFilter);
    }

    private IMessageFilter mMessageFilter = new IMessageFilter() {
    @Override
    public boolean filter(Notification notification) {
    if (notification.getContent().contains("XPush")) {
    showMessage("通知被拦截");
    return true;
    }
    return false;
    }

    @Override
    public boolean filter(CustomMessage message) {
    if (message.getMsg().contains("XPush")) {
    showMessage("自定义消息被拦截");
    return true;
    }
    return false;
    }
    };

    @Override
    public void onDestroyView() {
    XPushManager.get().removeFilter(mMessageFilter);
    super.onDestroyView();
    }

    6、推送通知的点击处理

    对于通知的点击事件,我们可以处理得更优雅,自定义其点击后的动作,打开我们想让用户看到的页面。

    我们可以在全局消息推送的接收器IPushReceiver中的onNotificationClick回调中,增加打开指定页面的操作。

    @Override
    public void onNotificationClick(Context context, XPushMsg msg) {
    super.onNotificationClick(context, msg);
    //打开自定义的Activity
    Intent intent = IntentUtils.getIntent(context, TestActivity.class, null, true);
    intent.putExtra(KEY_PARAM_STRING, msg.getContent());
    intent.putExtra(KEY_PARAM_INT, msg.getId());
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    ActivityUtils.startActivity(intent);
    }

    需要注意的是,这需要你在消息推送平台推送的通知使用的是自定义动作或者打开指定页面类型,并且传入的Intent uri 内容满足如下格式:

    • title:通知的标题

    • content:通知的内容

    • extraMsg:通知附带的拓展字段,可存放json或其他内容

    • keyValue:通知附带的键值对

    xpush://com.xuexiang.xpush/notification?title=这是一个通知&content=这是通知的内容&extraMsg=xxxxxxxxx&keyValue={"param1": "1111", "param2": "2222"}

    当然你也可以自定义传入的Intent uri 格式,具体可参考项目中的XPushNotificationClickActivityAndroidManifest.xml


    代码下载:XPush.zip

    收起阅读 »

    一行代码完成http请求!WelikeAndroid 一款引入即用的便捷开发框架

    #WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.WelikeAndroid目前包含五个大模块:异常安全隔离模...
    继续阅读 »

    #WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,
    使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.

    WelikeAndroid目前包含五个大模块:

    • 异常安全隔离模块(实验阶段):当任何线程抛出任何异常,我们的异常隔离机制都会让UI线程继续运行下去.
    • Http模块: 一行代码完成POST、GET请求和Download,支持上传, 高度优化Disk的缓存加载机制,
      自由设置缓存大小、缓存时间(也支持永久缓存和不缓存).
    • Bitmap模块: 一行代码完成异步显示图片,无需考虑OOM问题,支持加载前对图片做自定义处理.
    • Database模块: 支持NotNull,Table,ID,Ignore等注解,Bean无需Getter和Setter,一键式部署数据库.
    • ui操纵模块: 我们为Activity基类做了完善的封装,继承基类可以让代码更加优雅.
    • :请不要认为功能相似,框架就不是原创,源码摆在眼前,何不看一看?

    使用WelikeAndroid需要以下权限:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />

    ##下文将教你如何圆润的使用WelikeAndroid:
    ###通过WelikeContext在任意处取得上下文:

    • WelikeContext.getApplication(); 就可以取得当前App的上下文
    • WelikeToast.toast("你好!"); 简单一步弹出Toast.

    ##WelikeGuard(异常安全隔离机制用法):

    • 第一步,开启异常隔离机制:
    WelikeGuard.enableGuard();
    • 第二步,注册一个全局异常监听器:

    WelikeGuard.registerUnCaughtHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {

    WelikeGuard.newThreadToast("出现异常了: " + ex.getMessage() );

    }
    });
    • 你也可以自定义异常:

    /**
    *
    * 自定义的异常,当异常被抛出后,会自动回调onCatchThrowable函数.
    */
    @Catch(process = "onCatchThrowable")
    public class CustomException extends IllegalAccessError {

    public static void onCatchThrowable(Thread t){
    WeLog.e(t.getName() + " 抛出了一个异常...");
    }
    }
    • 另外,继承自UncaughtThrowable的异常我们不会对其进行拦截.

    使用Welike做屏幕适配:

    Welike的ViewPorter类提供了屏幕适配的Fluent-API,我们可以通过一组流畅的API轻松做好屏幕适配.

            ViewPorter.from(button).ofScreen().divWidth(2).commit();//宽度变为屏幕的二分之一
    ViewPorter.from(button).of(viewGroup).divHeight(2).commit();//高度变为viewGroup的二分之一
    ViewPorter.from(button).div(2).commit();//宽度和高度变为屏幕的四分之一
    ViewPorter.from(button).of(this).fillWidth().fillHeight().commit();//宽度和高度铺满Activity
    ViewPorter.from(button).sameAs(imageView).commit();//button的宽度和高度和imageView一样

    WelikeHttp入门:

    • 第一步,取得WelikeHttp默认实例.
    WelikeHttp welikeHttp = WelikeHttp.getDefault();
    • 第二步,发送一个Get请求.
    HttpParams params = new HttpParams();
    params.putParams("app","qr.get",
    "data","Test");//一次性放入两对 参数 和 值

    //发送Get请求
    HttpRequest request = welikeHttp.get("http://api.k780.com:88", params, new HttpResultCallback() {
    @Override
    public void onSuccess(String content) {
    super.onSuccess(content);
    WelikeToast.toast("返回的JSON为:" + content);
    }

    @Override
    public void onFailure(HttpResponse response) {
    super.onFailure(response);
    WelikeToast.toast("JSON请求发送失败.");
    }

    @Override
    public void onCancel(HttpRequest request) {
    super.onCancel(request);
    WelikeToast.toast("请求被取消.");
    }
    });

    //取消请求,会回调onCancel()
    request.cancel();

    当然,我们为满足需求提供了多种扩展的Callback,目前我们提供以下Callback供您选择:

    • HttpCallback(响应为byte[]数组)
    • FileUploadCallback(仅在上传文件时使用)
    • HttpBitmapCallback(建议使用Bitmap模块)
    • HttpResultCallback(响应为String)
    • DownloadCallback(仅在download时使用)

    如需自定义Http模块的配置(如缓存时间),请查看HttpConfig.

    WelikeBitmap入门:

    • 第一步,取得默认的WelikeBitmap实例:

    //取得默认的WelikeBitmap实例
    WelikeBitmap welikeBitmap = WelikeBitmap.getDefault();
    • 第二步,异步加载一张图片:
    BitmapRequest request = welikeBitmap.loadBitmap(imageView,
    "http://img0.imgtn.bdimg.com/it/u=937075122,1381619862&fm=21&gp=0.jpg",
    android.R.drawable.btn_star,//加载中显示的图片
    android.R.drawable.ic_delete,//加载失败时显示的图片
    new BitmapCallback() {

    @Override
    public Bitmap onProcessBitmap(byte[] data) {
    //如果需要在加载时处理图片,可以在这里处理,
    //如果不需要处理,就返回null或者不复写这个方法.
    return null;
    }

    @Override
    public void onPreStart(String url) {
    super.onPreStart(url);
    //加载前回调
    WeLog.d("===========> onPreStart()");
    }

    @Override
    public void onCancel(String url) {
    super.onCancel(url);
    //请求取消时回调
    WeLog.d("===========> onCancel()");
    }

    @Override
    public void onLoadSuccess(String url, Bitmap bitmap) {
    super.onLoadSuccess(url, bitmap);
    //图片加载成功后回调
    WeLog.d("===========> onLoadSuccess()");
    }

    @Override
    public void onRequestHttp(HttpRequest request) {
    super.onRequestHttp(request);
    //图片需要请求http时回调
    WeLog.d("===========> onRequestHttp()");
    }

    @Override
    public void onLoadFailed(HttpResponse response, String url) {
    super.onLoadFailed(response, url);
    //请求失败时回调
    WeLog.d("===========> onLoadFailed()");
    }
    });
    • 如果需要自定义Config,请看BitmapConfig这个类.

    ##WelikeDAO入门:

    • 首先写一个Bean.

    /*表名,可有可无,默认为类名.*/
    @Table(name="USER",afterTableCreate="afterTableCreate")
    public class User{
    @ID
    public int id;//id可有可无,根据自己是否需要来加.

    /*这个注解表示name字段不能为null*/
    @NotNull
    public String name;

    public static void afterTableCreate(WelikeDao dao){
    //在当前的表被创建时回调,可以在这里做一些表的初始化工作
    }
    }
    • 然后将它写入到数据库
    WelikeDao db = WelikeDao.instance("Welike.db");
    User user = new User();
    user.name = "Lody";
    db.save(user);
    • 从数据库取出Bean

    User savedUser = db.findBeanByID(1);
    • SQL复杂条件查询
    List<User> users = db.findBeans().where("name = Lody").or("id = 1").find();
    • 更新指定ID的Bean
    User wantoUpdateUser = new User();
    wantoUpdateUser.name = "NiHao";
    db.updateDbByID(1,wantoUpdateUser);
    • 删除指ID定的Bean
    db.deleteBeanByID(1);
    • 更多实例请看DEMO和API文档.

    ##十秒钟学会WelikeActivity

    • 我们将Activity的生命周期划分如下:

    =>@initData(所有标有InitData注解的方法都最早在子线程被调用)
    =>initRootView(bundle)
    =>@JoinView(将标有此注解的View自动findViewByID和setOnClickListener)
    =>onDataLoaded(数据加载完成时回调)
    =>点击事件会回调onWidgetClick(View Widget)

    ###关于@JoinView的细节:

    • 有以下三种写法:
    @JoinView(name = "welike_btn")
    Button welikeBtn;
    @JoinView(id = R.id.welike_btn)
    Button welikeBtn;
    @JoinView(name = "welike_btn",click = false)
    Button welikeBtn;
    • clicktrue时会自动调用view的setOnClickListener方法,并在onWidgetClick回调.
    • 当需要绑定的是一个Button的时候, click属性默认为true,其它的View则默认为false.
    收起阅读 »

    Android上一个非常优雅好用的日历,全面自定义UI,自定义周起始

    CalenderViewAndroid上一个非常优雅、高度自定义、性能高效的日历控件,完美支持周视图,支持标记、自定义颜色、农历等,任意控制月视图显示、任意日期拦截条件、自定义周起始等。Canvas绘制,极速性能、占用内存低,,支持简单定制即可实现任意自定义布...
    继续阅读 »


    CalenderView

    Android上一个非常优雅、高度自定义、性能高效的日历控件,完美支持周视图,支持标记、自定义颜色、农历等,任意控制月视图显示、任意日期拦截条件、自定义周起始等。Canvas绘制,极速性能、占用内存低,,支持简单定制即可实现任意自定义布局、自定义UI,支持收缩展开、性能非常高效, 这个控件内存和效率优势相当明显,而且真正做到收缩+展开,适配多种场景,支持同时多种颜色标记日历事务,支持多点触控,你真的想不到日历还可以如此优雅!更多参考用法请移步Demo,Demo实现了一些精美的自定义效果,用法仅供参考。

    插拔式设计

    插拔式设计:好比插座一样,插上灯泡就会亮,插上风扇就会转,看用户需求什么而不是看插座有什么,只要是电器即可。此框架使用插拔式,既可以在编译时指定年月日视图,如:app:month_view="xxx.xxx.MonthView.class",也可在运行时动态更换年月日视图,如:CalendarView.setMonthViewClass(MonthView.Class),从而达到UI即插即用的效果,相当于框架不提供UI实现,让UI都由客户端实现,不至于日历UI都千篇一律,只需遵守插拔式接口即可随意定制,自由化程度非常高。

    AndroidStudio请使用3.5以上版本

    support使用版本

    implementation 'com.haibin:calendarview:3.6.8'

    Androidx使用版本

    implementation 'com.haibin:calendarview:3.6.9'
    <dependency>
    <groupId>com.haibin</groupId>
    <artifactId>calendarview</artifactId>
    <version>3.6.9</version>
    <type>pom</type>
    </dependency>

    混淆proguard-rules

    -keepclasseswithmembers class * {
    public <init>(android.content.Context);
    }

    或者针对性的使用混淆,请自行配置测试!

    -keep class your project path.MonthView {
    public <init>(android.content.Context);
    }
    -keep class your project path.WeekBar {
    public <init>(android.content.Context);
    }
    -keep class your project path.WeekView {
    public <init>(android.content.Context);
    }
    -keep class your project path.YearView {
    public <init>(android.content.Context);
    }


    特别的,请注意不要复制这三个路径,自行替换您自己的自定义路径

    app:month_view="com.haibin.calendarviewproject.simple.SimpleMonthView"
    app:week_view="com.haibin.calendarviewproject.simple.SimpleWeekView"
    app:week_bar_view="com.haibin.calendarviewproject.EnglishWeekBar"

    使用方法

     <com.haibin.calendarview.CalendarLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    app:default_status="shrink"
    app:calendar_show_mode="only_week_view"
    app:calendar_content_view_id="@+id/recyclerView">

    <com.haibin.calendarview.CalendarView
    android:id="@+id/calendarView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#fff"
    app:month_view="com.haibin.calendarviewproject.simple.SimpleCalendarCardView"
    app:week_view="com.haibin.calendarviewproject.simple.SimpleWeekView"
    app:week_bar_view="com.haibin.calendarviewproject.EnglishWeekBar"
    app:calendar_height="50dp"
    app:current_month_text_color="#333333"
    app:current_month_lunar_text_color="#CFCFCF"
    app:min_year="2004"
    app:other_month_text_color="#e1e1e1"
    app:scheme_text="假"
    app:scheme_text_color="#333"
    app:scheme_theme_color="#333"
    app:selected_text_color="#fff"
    app:selected_theme_color="#333"
    app:week_start_with="mon"
    app:week_background="#fff"
    app:month_view_show_mode="mode_only_current"
    app:week_text_color="#111" />

    <android.support.v7.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#d4d4d4" />
    </com.haibin.calendarview.CalendarLayout>

    CalendarView attrs

    <declare-styleable name="CalendarView">

    <attr name="calendar_padding" format="dimension" /><!--日历内部左右padding-->

    <attr name="month_view" format="color" /> <!--自定义类日历月视图路径-->
    <attr name="week_view" format="string" /> <!--自定义类周视图路径-->
    <attr name="week_bar_height" format="dimension" /> <!--星期栏的高度-->
    <attr name="week_bar_view" format="color" /> <!--自定义类周栏路径,通过自定义则 week_text_color week_background xml设置无效,当仍可java api设置-->
    <attr name="week_line_margin" format="dimension" /><!--线条margin-->

    <attr name="week_line_background" format="color" /><!--线条颜色-->
    <attr name="week_background" format="color" /> <!--星期栏的背景-->
    <attr name="week_text_color" format="color" /> <!--星期栏文本颜色-->
    <attr name="week_text_size" format="dimension" /><!--星期栏文本大小-->

    <attr name="current_day_text_color" format="color" /> <!--今天的文本颜色-->
    <attr name="current_day_lunar_text_color" format="color" /><!--今天的农历文本颜色-->

           <attr name="calendar_height" format="string" /> <!--日历每项的高度,56dp-->
    <attr name="day_text_size" format="string" /> <!--天数文本大小-->
    <attr name="lunar_text_size" format="string" /> <!--农历文本大小-->

    <attr name="scheme_text" format="string" /> <!--标记文本-->
    <attr name="scheme_text_color" format="color" /> <!--标记文本颜色-->
    <attr name="scheme_month_text_color" format="color" /> <!--标记天数文本颜色-->
    <attr name="scheme_lunar_text_color" format="color" /> <!--标记农历文本颜色-->

    <attr name="scheme_theme_color" format="color" /> <!--标记的颜色-->

    <attr name="selected_theme_color" format="color" /> <!--选中颜色-->
    <attr name="selected_text_color" format="color" /> <!--选中文本颜色-->
    <attr name="selected_lunar_text_color" format="color" /> <!--选中农历文本颜色-->

    <attr name="current_month_text_color" format="color" /> <!--当前月份的字体颜色-->
    <attr name="other_month_text_color" format="color" /> <!--其它月份的字体颜色-->

    <attr name="current_month_lunar_text_color" format="color" /> <!--当前月份农历节假日颜色-->
    <attr name="other_month_lunar_text_color" format="color" /> <!--其它月份农历节假日颜色-->

    <!-- 年视图相关 -->
    <attr name="year_view_month_text_size" format="dimension" /> <!-- 年视图月份字体大小 -->
    <attr name="year_view_day_text_size" format="dimension" /> <!-- 年视图月份日期字体大小 -->
    <attr name="year_view_month_text_color" format="color" /> <!-- 年视图月份字体颜色 -->
    <attr name="year_view_day_text_color" format="color" /> <!-- 年视图日期字体颜色 -->
    <attr name="year_view_scheme_color" format="color" /> <!-- 年视图标记颜色 -->

    <attr name="min_year" format="integer" />  <!--最小年份1900-->
     <attr name="max_year" format="integer" /> <!--最大年份2099-->
    <attr name="min_year_month" format="integer" /> <!--最小年份对应月份-->
    <attr name="max_year_month" format="integer" /> <!--最大年份对应月份-->

    <!--月视图是否可滚动-->
    <attr name="month_view_scrollable" format="boolean" />
    <!--周视图是否可滚动-->
    <attr name="week_view_scrollable" format="boolean" />
    <!--年视图是否可滚动-->
    <attr name="year_view_scrollable" format="boolean" />
           
    <!--配置你喜欢的月视图显示模式模式-->
    <attr name="month_view_show_mode">
    <enum name="mode_all" value="0" /> <!--全部显示-->
    <enum name="mode_only_current" value="1" /> <!--仅显示当前月份-->
    <enum name="mode_fix" value="2" /> <!--自适应显示,不会多出一行,但是会自动填充-->
    </attr>

    <!-- 自定义周起始 -->
    <attr name="week_start_with">
    <enum name="sun" value="1" />
    <enum name="mon" value="2" />
    <enum name="sat" value="7" />
    </attr>

    <!-- 自定义选择模式 -->
    <attr name="select_mode">
    <enum name="default_mode" value="0" />
    <enum name="single_mode" value="1" />
    <enum name="range_mode" value="2" />
    </attr>

    <!-- 当 select_mode=range_mode -->
    <attr name="min_select_range" format="integer" />
    <attr name="max_select_range" format="integer" />
    </declare-styleable>

    CalendarView api


    public void setRange(int minYear, int minYearMonth, int minYearDay,
    int maxYear, int maxYearMonth, int maxYearDay) ;//置日期范围

    public int getCurDay(); //今天
    public int getCurMonth(); //当前的月份
    public int getCurYear(); //今年

    public boolean isYearSelectLayoutVisible();//年月份选择视图是否打开
    public void closeYearSelectLayout();//关闭年月视图选择布局
    public void showYearSelectLayout(final int year); //快速弹出年份选择月份

    public void setOnMonthChangeListener(OnMonthChangeListener listener);//月份改变事件

    public void setOnYearChangeListener(OnYearChangeListener listener);//年份切换事件

    public void setOnCalendarSelectListener(OnCalendarSelectListener listener)//日期选择事件

    public void setOnCalendarLongClickListener(OnCalendarLongClickListener listener);//日期长按事件

    public void setOnCalendarLongClickListener(OnCalendarLongClickListener listener, boolean preventLongPressedSelect);//日期长按事件

    public void setOnCalendarInterceptListener(OnCalendarInterceptListener listener);//日期拦截和日期有效性绘制

    public void setSchemeDate(Map<String, Calendar> mSchemeDates);//标记日期

    public void update();//动态更新

    public Calendar getSelectedCalendar(); //获取选择的日期

    /**
    * 特别的,如果你需要自定义或者使用其它选择器,可以用以下方法进行和日历联动
    */
    public void scrollToCurrent();//滚动到当前日期

    public void scrollToCurrent(boolean smoothScroll);//滚动到当前日期

    public void scrollToYear(int year);//滚动到某一年

    public void scrollToPre();//滚动到上一个月

    public void scrollToNext();//滚动到下一个月

    public void scrollToCalendar(int year, int month, int day);//滚动到指定日期

    public Calendar getMinRangeCalendar();//获得最小范围日期

    public Calendar getMaxRangeCalendar();//获得最大范围日期

    /**
    * 设置背景色
    *
    * @param monthLayoutBackground 月份卡片的背景色
    * @param weekBackground 星期栏背景色
    * @param lineBg 线的颜色
    */
    public void setBackground(int monthLayoutBackground, int weekBackground, int lineBg)

    /**
    * 设置文本颜色
    *
    * @param curMonthTextColor 当前月份字体颜色
    * @param otherMonthColor 其它月份字体颜色
    * @param lunarTextColor 农历字体颜色
    */
    public void setTextColor(int curMonthTextColor,int otherMonthColor,int lunarTextColor)

    /**
    * 设置选择的效果
    *
    * @param style 选中的style CalendarCardView.STYLE_FILL or CalendarCardView.STYLE_STROKE
    * @param selectedThemeColor 选中的标记颜色
    * @param selectedTextColor 选中的字体颜色
    */
    public void setSelectedColor(int style, int selectedThemeColor, int selectedTextColor)

    /**
    * 设置标记的色
    *
    * @param style 标记的style CalendarCardView.STYLE_FILL or CalendarCardView.STYLE_STROKE
    * @param schemeColor 标记背景色
    * @param schemeTextColor 标记字体颜色
    */
    public void setSchemeColor(int style, int schemeColor, int schemeTextColor)


    /**
    * 设置星期栏的背景和字体颜色
    *
    * @param weekBackground 背景色
    * @param weekTextColor 字体颜色
    */
    public void setWeeColor(int weekBackground, int weekTextColor)

    CalendarLayout api

    public void expand(); //展开

    public void shrink(); //收缩

    public boolean isExpand();//是否展开了

    CalendarLayout attrs


    <!-- 日历显示模式 -->
    <attr name="calendar_show_mode">
    <enum name="both_month_week_view" value="0" /><!-- 默认都有 -->
    <enum name="only_week_view" value="1" /><!-- 仅周视图 -->
    <enum name="only_month_view" value="2" /><!-- 仅月视图 -->
    </attr>

    <attr name="default_status">
    <enum name="expand" value="0" /> <!--默认展开-->
    <enum name="shrink" value="1" /><!--默认搜索-->
    </attr>

    <attr name="calendar_content_view_id" format="integer" /><!--内容布局id-->

    代码下载:CalendarView.zip

    收起阅读 »

    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件

    前言 基于安卓平台的SlidingMenu侧滑菜单组件(github.com/jfeinstein1… 实现了鸿蒙化迁移和重构,代码已经开源到(gitee.com/isrc\_ohos/… 欢迎各位下载使用并提出宝贵意见! 背景 SlidingMen...
    继续阅读 »

    前言


    基于安卓平台的SlidingMenu侧滑菜单组件(github.com/jfeinstein1…


    实现了鸿蒙化迁移和重构,代码已经开源到(gitee.com/isrc\_ohos/…


    欢迎各位下载使用并提出宝贵意见!


    背景


    SlidingMenu_ohos提供了一个侧滑菜单的导航框架,使菜单可以隐藏在手机屏幕的左侧、右侧或左右两侧。当用户使用时,通过左滑或者右滑的方式调出,既节省了主屏幕的空间,也方便用户操作,在很多主流APP中都有广泛的应用。


    效果展示


    由于菜单从左右两侧调出的显示效果相似,此处仅以菜单从左侧调出为例进行效果展示。


    组件未启用时,应用显示主页面。单指触摸屏幕左侧并逐渐向右滑动,菜单页面逐渐显示,主页面逐渐隐藏。向右滑动的距离超过某个阈值时,菜单页面全部显示,效果如图1所示。


    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


    图1 菜单展示和隐藏效果图


    Sample解析


    Sample部分的内容较为简单,主要包含两个部分。一是创建SlidingMenu_ohos组件的对象,可根据用户的实际需求,调用Library的接口,对组件的具体属性进行设置。二是将设置好的组件添加到Ability中。下面将详细介绍组件的使用方法。


    1、导入SlidingMenu类


    import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu;

    2、设置Ability的布局


    此布局用作为主页面的布局,在组件隐藏的时候显示。


    DirectionalLayout directionalLayout = 
    (DirectionalLayout)LayoutScatter.getInstance(this).parse(ResourceTable.Layout_activity_main,null,false);setUIContent(directionalLayout);

    3、实例化组件的对象


    SlidingMenu slidingMenu = null;
    try {
    //初始化SlidingMenu实例
    slidingMenu = new SlidingMenu(this);
    } catch (IOException e) {
    e.printStackTrace();
    } catch (NotExistException e) {
    e.printStackTrace();
    }

    4、设置组件属性


    此步骤可以根据具体需求,设置组件的位置、触发范围、布局、最大宽度等属性。


    //设置菜单放置位置
    slidingMenu.setMode(SlidingMenu.LEFT);
    //设置组件的触发范围
    slidingMenu.setTouchScale(100);
    //设置组件的布局
    slidingMenu.setMenu(ResourceTable.Layout_layout_left_menu);
    //设置菜单最大宽度
    slidingMenu.setMenuWidth(800);

    5、关联Ability


    attachToAbility()方法是Library提供的重要方法,用于将菜单组件关联到Ability。其参数SLIDING_WINDOW和SLIDING_CONTENT是菜单的不同模式,SLIDING_WINDOW模式下的菜单包含Title / ActionBar部分,菜单需在整个手机页面上显示,如图2所示;SLIDING_CONTENT模式下的菜单不包括包含Title / ActionBar部分,菜单可以在手机页面的局部范围内显示,如图3所示。


    try {
    //关联Ability,获取页面展示根节点
    slidingMenu.attachToAbility(directionalLayout,this, SlidingMenu.SLIDING_WINDOW);
    } catch (NotExistException e) {
    e.printStackTrace();
    } catch (WrongTypeException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    }

    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


    图2 SLIDING_WINDOW展示效果图


    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


    图3 SLIDING_CONTENT展示效果图


    Library解析


    Library的工程结构如下图所示,CustomViewAbove表示主页面,CustomViewBehind表示菜单页面,SlidingMenu主要用于控制主页面位于菜单页面的上方,还可以设置菜单的宽度、触发范围、显示模式等属性。为了方便解释,以下均以手指从左侧触摸屏幕并向右滑动为例进行讲解,菜单均采用SLIDING_WINDOW的显示模式。


    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


    图4 Library的工程结构


    1、CustomViewAbove主页面


    CustomViewAbove需要监听触摸、移动、抬起和取消等Touch事件,并记录手指滑动的距离和速度。


    (1)对Touch事件的处理


    Touch事件决定了菜单的显示、移动和隐藏。例如:在菜单的触发范围内,手指向右滑动(POINT_MOVE)时,菜单会跟随滑动到手指所在位置。手指抬起(PRIMARY_POINT_UP)或者取消滑动(CANCEL)时,会依据手指滑动的距离和速度决定菜单页面的下一状态是全部隐藏还是全部显示。


     switch (action) {
    //按下
    case TouchEvent.PRIMARY_POINT_DOWN:
    .....
    mInitialMotionX=mLastMotionX=ev.getPointerPosition(mActivePointerId).getX();
    break;
    //滑动
    case TouchEvent.POINT_MOVE:
    ......
    //菜单滑动到此时手指所在位置(x)
    left_scrollto(x);
    break;
    //抬起
    case TouchEvent.PRIMARY_POINT_UP:
    ......
    //获得菜单的下一状态(全屏显示或者全部隐藏)
    int nextPage = determineTargetPage(pageOffset, initialVelocity,totalDelta);
    //设置菜单的下一状态
    setCurrentItemInternal(nextPage,initialVelocity);
    ......
    endDrag();
    break;
    //取消
    case TouchEvent.CANCEL:
    ......
    //根据菜单当前状态mCurItem设置菜单下一状态
    setCurrentItemInternal(mCurItem);
    //结束拖动
    endDrag();
    break;
    }

    (2)对滑动的距离和速度的处理


    手指抬起时,滑动的速度和距离分别大于最小滑动速度和最小移动距离,判定此时的操作为快速拖动,菜单立即弹出并全部显示,如图5所示。


    private int determineTargetPage(float pageOffset, int velocity, int deltaX) {
    //获得当前菜单状态,0:左侧菜单正在展示,1:菜单隐藏,2:右侧菜单正在展示
    int targetPage = getCurrentItem();
    //针对快速拖动的判断
    if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
    if (velocity > 0 && deltaX > 0) {
    targetPage -= 1;
    } else if (velocity < 0 && deltaX < 0){
    targetPage += 1;
    }
    }
    }

    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


    图5 快速拖动效果图


    当手指抬起并且不满足快速拖动标准时,需要根据滑动距离判断菜单的隐藏或显示。若菜单已展开的部分超过自身宽度的1/2,菜单立即弹出全部显示,,效果图如图1所示;若不足自身宽度的1/2,则立即弹回全部隐藏,效果图如图6所示。


    //获得当前菜单状态,0:左侧菜单正在展示,1:菜单隐藏,2:右侧菜单正在展示
    switch (mCurItem){
    case 0:
    targetPage=1-Math.round(pageOffset);
    break;
    case 1:
    //菜单隐藏时,首先要判断此时菜单的放置状态是左侧还是右侧
    if(current_state == SlidingMenu.LEFT){
    targetPage = Math.round(1-pageOffset);
    }
    if(current_state == SlidingMenu.RIGHT){
    targetPage = Math.round(1+pageOffset);
    }
    break;
    case 2:
    targetPage = Math.round(1+pageOffset);
    break;
    }

    鸿蒙开源第三方组件——SlidingMenu_ohos侧滑菜单组件


    图6 缓慢拖动效果图


    (3)菜单显示和隐藏的实现


    主页面的左侧边线与手指的位置绑定,当手指向右滑动时,主页面也会随手指向右滑动,在这个过程中菜单页面渐渐展示出来,实现菜单页面随手指滑动慢慢展开的视觉效果。


    void setCurrentItemInternal(int item,int velocity) {
    //获得菜单的目标状态
    item = mViewBehind.getMenuPage(item);
    mCurItem = item;
    final int destX = getDestScrollX(mCurItem);
    /*菜单放置状态为左侧,通过设置主页面的位置实现菜单的弹出展示或弹回隐藏
    1.destX=0,主页面左侧边线与屏幕左侧边线对齐,菜单被全部遮挡,实现菜单弹回隐藏
    2.destX=MenuWidth,主页面左侧边线向右移动与菜单总宽度相等的距离,实现菜单弹出展示*/
    if (mViewBehind.getMode() == SlidingMenu.LEFT) {
    mContent.setLeft(destX);
    mViewBehind.scrollBehindTo(destX);
    }
    ......
    }

    // 菜单放置在左侧时的菜单滑动操作
    public void left_scrollto(float x) {
    //当menu的展示宽度大于最大宽度时仅展示最大宽度
    if(x>getMenuWidth()){
    x=getMenuWidth();
    }
    //主页面(主页面左侧边线)和菜单(菜单右侧边线)分别移动到指定位置X
    mContent.setLeft((int)x);
    mViewBehind.scrollBehindTo((int)x);
    }

    2、CustomViewBehind 菜单页面


    CustomViewBehind为菜单页面,逻辑相比于主页面简单许多。主要负责根据主页面中的Touch事件改变自身状态值,同时向外暴露接口,用于设置或者获取菜单页面的最大宽度、自身状态等属性。


    // 设置菜单最大宽度
    public void setMenuWidth(int menuWidth) {
    this.menuWidth = menuWidth;
    }

    // 获得菜单最大宽度
    public int getMenuWidth() {
    return menuWidth;
    }

    3. SlidingMenu


    分别实例化CustomViewAbove和CustomViewBehind的对象,并按照主页面在上菜单页面在下的顺序分别添加到SlidingMenu的容器中。


    //添加菜单子控件
    addComponent(mViewBehind, behindParams);
    //添加主页面子控件
    addComponent(mViewAbove, aboveParams);

    项目贡献人


    徐泽鑫 郑森文 朱伟 陈美汝 王佳思 张馨心


    作者:朱伟ISRC

    收起阅读 »

    想做图表?Android优秀图表库MPAndroidChart

    嗨,你终于来啦 ~ 等你好久啦~ 喜欢的小伙伴欢迎关注,我会定期分享Android知识点及解析,还会不断更新的BATJ面试专题,欢迎大家前来探讨交流,如有好的文章也欢迎投稿。 前言 在项目当中很多时候要对数据进行分析就要用到图表,在gitHub上有很多优秀的...
    继续阅读 »

    嗨,你终于来啦 ~ 等你好久啦~ 喜欢的小伙伴欢迎关注,我会定期分享Android知识点及解析,还会不断更新的BATJ面试专题,欢迎大家前来探讨交流,如有好的文章也欢迎投稿。

    前言


    在项目当中很多时候要对数据进行分析就要用到图表,在gitHub上有很多优秀的图表开源库,今天给大家分享的就是MPAndroidChart中的柱状图。简单介绍一下MPAndroidChart:他可以实现图表的拖动,3D,局部查看,数据动态展示等功能。


    官方源码地址:github.com/PhilJay/MPA…


    废话就不多说啦,先给看大家看看效果图哟



























    操作步骤


    第一步:需要将依赖的库添加到你的项目中



    implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0-alpha'

    implementation 'com.google.android.material:material:1.0.0'



    第二步:xml中


       <com.github.mikephil.charting.charts.BarChart
    android:id="@+id/chart1"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    />

    第三步:ValueFormatter.java


      /**
    * Class to format all values before they are drawn as labels.
    */

    public abstract class ValueFormatter implements IAxisValueFormatter, IValueFormatter {

    /**
    * <b>DO NOT USE</b>, only for backwards compatibility and will be removed in future versions.
    *
    * @param value the value to be formatted
    * @param axis the axis the value belongs to
    * @return formatted string label
    */

    @Override
    @Deprecated
    public String getFormattedValue(float value, AxisBase axis) {
    return getFormattedValue(value);
    }

    /**
    * <b>DO NOT USE</b>, only for backwards compatibility and will be removed in future versions.
    * @param value the value to be formatted
    * @param entry the entry the value belongs to - in e.g. BarChart, this is of class BarEntry
    * @param dataSetIndex the index of the DataSet the entry in focus belongs to
    * @param viewPortHandler provides information about the current chart state (scale, translation, ...)
    * @return formatted string label
    */

    @Override
    @Deprecated
    public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
    return getFormattedValue(value);
    }

    /**
    * Called when drawing any label, used to change numbers into formatted strings.
    *
    * @param value float to be formatted
    * @return formatted string label
    */

    public String getFormattedValue(float value) {
    return String.valueOf(value);
    }

    /**
    * Used to draw axis labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param value float to be formatted
    * @param axis axis being labeled
    * @return formatted string label
    */

    public String getAxisLabel(float value, AxisBase axis) {
    return getFormattedValue(value);
    }

    /**
    * Used to draw bar labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param barEntry bar being labeled
    * @return formatted string label
    */

    public String getBarLabel(BarEntry barEntry) {
    return getFormattedValue(barEntry.getY());
    }

    /**
    * Used to draw stacked bar labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param value current value to be formatted
    * @param stackedEntry stacked entry being labeled, contains all Y values
    * @return formatted string label
    */

    public String getBarStackedLabel(float value, BarEntry stackedEntry) {
    return getFormattedValue(value);
    }

    /**
    * Used to draw line and scatter labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param entry point being labeled, contains X value
    * @return formatted string label
    */

    public String getPointLabel(Entry entry) {
    return getFormattedValue(entry.getY());
    }

    /**
    * Used to draw pie value labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param value float to be formatted, may have been converted to percentage
    * @param pieEntry slice being labeled, contains original, non-percentage Y value
    * @return formatted string label
    */

    public String getPieLabel(float value, PieEntry pieEntry) {
    return getFormattedValue(value);
    }

    /**
    * Used to draw radar value labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param radarEntry entry being labeled
    * @return formatted string label
    */

    public String getRadarLabel(RadarEntry radarEntry) {
    return getFormattedValue(radarEntry.getY());
    }

    /**
    * Used to draw bubble size labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param bubbleEntry bubble being labeled, also contains X and Y values
    * @return formatted string label
    */

    public String getBubbleLabel(BubbleEntry bubbleEntry) {
    return getFormattedValue(bubbleEntry.getSize());
    }

    /**
    * Used to draw high labels, calls {@link #getFormattedValue(float)} by default.
    *
    * @param candleEntry candlestick being labeled
    * @return formatted string label
    */

    public String getCandleLabel(CandleEntry candleEntry) {
    return getFormattedValue(candleEntry.getHigh());
    }

    }

    第四步:MyValueFormatter


        public class MyValueFormatter extends ValueFormatter{
    private final DecimalFormat mFormat;
    private String suffix;

    public MyValueFormatter(String suffix) {
    mFormat = new DecimalFormat("0000");
    this.suffix = suffix;
    }

    @Override
    public String getFormattedValue(float value) {
    return mFormat.format(value) + suffix;
    }

    @Override
    public String getAxisLabel(float value, AxisBase axis) {
    if (axis instanceof XAxis) {
    return mFormat.format(value);
    } else if (value > 0) {
    return mFormat.format(value) + suffix;
    } else {
    return mFormat.format(value);
    }
    }
    }

    第五步:MainAcyivity


      package detongs.hbqianze.him.linechart;
    import android.os.Bundle;
    import android.util.Log;
    import android.view.WindowManager;
    import android.widget.TextView;

    import androidx.appcompat.app.AppCompatActivity;

    import com.github.mikephil.charting.charts.BarChart;
    import com.github.mikephil.charting.components.XAxis;
    import com.github.mikephil.charting.components.YAxis;
    import com.github.mikephil.charting.data.BarData;
    import com.github.mikephil.charting.data.BarDataSet;
    import com.github.mikephil.charting.data.BarEntry;
    import com.github.mikephil.charting.interfaces.datasets.IBarDataSet;
    import com.github.mikephil.charting.interfaces.datasets.IDataSet;
    import com.github.mikephil.charting.utils.ColorTemplate;

    import java.util.ArrayList;

    import detongs.hbqianze.him.linechart.chart.MyValueFormatter;
    import detongs.hbqianze.him.linechart.chart.ValueFormatter;

    public class MainActivity extends AppCompatActivity {



    private BarChart chart;
    private TextView te_cache;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
    WindowManager.LayoutParams.FLAG_FULLSCREEN);
    setContentView(R.layout.activity_main);


    chart = findViewById(R.id.chart1);
    te_cache = findViewById(R.id.te_cache);


    chart.getDescription().setEnabled(false);

    //设置最大值条目,超出之后不会有值
    chart.setMaxVisibleValueCount(60);

    //分别在x轴和y轴上进行缩放
    chart.setPinchZoom(true);
    //设置剩余统计图的阴影
    chart.setDrawBarShadow(false);
    //设置网格布局
    chart.setDrawGridBackground(true);
    //通过自定义一个x轴标签来实现2,015 有分割符符bug
    ValueFormatter custom = new MyValueFormatter(" ");
    //获取x轴线
    XAxis xAxis = chart.getXAxis();

    //设置x轴的显示位置
    xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
    //设置网格布局
    xAxis.setDrawGridLines(true);
    //图表将避免第一个和最后一个标签条目被减掉在图表或屏幕的边缘
    xAxis.setAvoidFirstLastClipping(false);
    //绘制标签 指x轴上的对应数值 默认true
    xAxis.setDrawLabels(true);
    xAxis.setValueFormatter(custom);
    //缩放后x 轴数据重叠问题
    xAxis.setGranularityEnabled(true);
    //获取右边y标签
    YAxis axisRight = chart.getAxisRight();
    axisRight.setStartAtZero(true);
    //获取左边y轴的标签
    YAxis axisLeft = chart.getAxisLeft();
    //设置Y轴数值 从零开始
    axisLeft.setStartAtZero(true);

    chart.getAxisLeft().setDrawGridLines(false);
    //设置动画时间
    chart.animateXY(600,600);

    chart.getLegend().setEnabled(true);

    getData();
    //设置柱形统计图上的值
    chart.getData().setValueTextSize(10);
    for (IDataSet set : chart.getData().getDataSets()){
    set.setDrawValues(!set.isDrawValuesEnabled());
    }



    }



    public void getData(){
    ArrayList<BarEntry> values = new ArrayList<>();
    Float aFloat = Float.valueOf("2015");
    Log.v("xue","aFloat+++++"+aFloat);
    BarEntry barEntry = new BarEntry(aFloat,Float.valueOf("100"));
    BarEntry barEntry1 = new BarEntry(Float.valueOf("2016"),Float.valueOf("210"));
    BarEntry barEntry2 = new BarEntry(Float.valueOf("2017"),Float.valueOf("300"));
    BarEntry barEntry3 = new BarEntry(Float.valueOf("2018"),Float.valueOf("450"));
    BarEntry barEntry4 = new BarEntry(Float.valueOf("2019"),Float.valueOf("300"));
    BarEntry barEntry5 = new BarEntry(Float.valueOf("2020"),Float.valueOf("650"));
    BarEntry barEntry6 = new BarEntry(Float.valueOf("2021"),Float.valueOf("740"));
    values.add(barEntry);
    values.add(barEntry1);
    values.add(barEntry2);
    values.add(barEntry3);
    values.add(barEntry4);
    values.add(barEntry5);
    values.add(barEntry6);
    BarDataSet set1;

    if (chart.getData() != null &&
    chart.getData().getDataSetCount() > 0) {
    set1 = (BarDataSet) chart.getData().getDataSetByIndex(0);
    set1.setValues(values);
    chart.getData().notifyDataChanged();
    chart.notifyDataSetChanged();
    } else {
    set1 = new BarDataSet(values, "点折水");
    set1.setColors(ColorTemplate.VORDIPLOM_COLORS);
    set1.setDrawValues(false);

    ArrayList<IBarDataSet> dataSets = new ArrayList<>();
    dataSets.add(set1);

    BarData data = new BarData(dataSets);
    chart.setData(data);

    chart.setFitBars(true);
    }
    //绘制图表
    chart.invalidate();

    }

    }




    github地址:https://github.com/PhilJay/MPAndroidChart

    下载地址:MPAndroidChart-master.zip

    收起阅读 »

    Android仿魅族桌面悬浮球!

    背景 游戏内的悬浮窗通常情况下只出现在游戏内,用做切换账号、客服中心等功能的快速入口。本文将介绍几种实现方案,以及我们踩过的坑 1、方案一:应用外悬浮窗+栈顶权限/生命周期回调 通常实现悬浮窗,首先考虑到的会是要使用悬浮窗权限,用WindowManager...
    继续阅读 »

    背景



    游戏内的悬浮窗通常情况下只出现在游戏内,用做切换账号、客服中心等功能的快速入口。本文将介绍几种实现方案,以及我们踩过的坑



    1、方案一:应用外悬浮窗+栈顶权限/生命周期回调


    通常实现悬浮窗,首先考虑到的会是要使用悬浮窗权限,用WindowManager在设备界面上addView实现(UI层级较高,应用外显示)


    1、弹出悬浮窗需要用到悬浮窗权限

    <!--悬浮窗权限-->
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>



    2、判断悬浮窗游戏内外显示


    方式一:使用栈顶权限获取当前


    //需要声明权限


    //判断当前是否在后台
    private boolean isAppIsInBackground(Context context) {
    boolean isInBackground = true;
    ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT_WATCH) {
    List runningProcesses = am.getRunningAppProcesses();
    for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) {
    //前台程序
    if (processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
    for (String activeProcess : processInfo.pkgList) {
    if (activeProcess.equals(context.getPackageName())) {
    isInBackground = false;
    }
    }
    }
    }
    } else {
    List taskInfo = am.getRunningTasks(1);
    ComponentName componentInfo = taskInfo.get(0).topActivity;
    if (componentInfo.getPackageName().equals(context.getPackageName())) {
    isInBackground = false;
    }
    }

    return isInBackground;


    这里考虑到这种方案网上有很多具体案例,在这里就不实现了。但是这种方案有如下缺点:


    1、适配问题,悬浮窗权限在不同设备上由于不同产商实现不同,适配难。


    2、向用户申请权限,打开率较低,体验较差


    2、方案二:addContentView实现


    原理:Activity的接口中除了我们常用的setContentView接口外,还有addContentView接口。利用该接口可以在Activity上添加View。


    这里你可能会问:


    1、那只能在一个Activity上添加吧?


    没错,是只能在当前Activity上添加,但是由于游戏通常也就在一个Activity跑,因此基本上是可以接受的。


    2、只add一个view,那拖动怎么实现?


    LayoutParams params = new LayoutParams(mWidth, mHeight);
    params.setMargins(mLeft, mTop, 0, 0);
    setLayoutParams(params);


    通过更新LayoutParams调整子View在父View中的位置就能实现


    具体代码如下:


    /**
    * @author zhuxiaoxin
    * 可拖拽贴边的view
    */

    public class DragViewLayout extends RelativeLayout {

    //手指拖拽得到的位置
    int mLeft, mRight, mTop, mBottom;

    //view所在的位置
    int mLastX, mLastY;

    /**
    * 屏幕宽度|高度
    */

    int mScreenWidth, mScreenHeight;

    /**
    * view的宽度|高度
    */

    int mWidth, mHeight;


    /**
    * 是否在拖拽过程中
    */

    boolean isDrag = false;

    /**
    * 系统最小滑动距离
    * @param context
    */

    int mTouchSlop = 0;

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

    public DragViewLayout(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public DragViewLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mScreenWidth = context.getResources().getDisplayMetrics().widthPixels;
    mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    @Override
    protected void onFinishInflate()
    {
    super.onFinishInflate();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event)
    {
    int action = event.getAction();
    switch (action) {
    case MotionEvent.ACTION_DOWN:
    mLeft = getLeft();
    mRight = getRight();
    mTop = getTop();
    mBottom = getBottom();
    mLastX = (int) event.getRawX();
    mLastY = (int) event.getRawY();
    break;
    case MotionEvent.ACTION_MOVE:
    int x = (int) event.getRawX();
    int y = (int) event.getRawY();
    int dx = x - mLastX;
    int dy = y - mLastY;
    if (Math.abs(dx) > mTouchSlop) {
    isDrag = true;
    }
    mLeft += dx;
    mRight += dx;
    mTop += dy;
    mBottom += dy;
    if (mLeft < 0) {
    mLeft = 0;
    mRight = mWidth;
    }
    if (mRight >= mScreenWidth) {
    mRight = mScreenWidth;
    mLeft = mScreenWidth - mWidth;
    }
    if (mTop < 0) {
    mTop = 0;
    mBottom = getHeight();
    }
    if (mBottom > mScreenHeight) {
    mBottom = mScreenHeight;
    mTop = mScreenHeight - mHeight;
    }
    mLastX = x;
    mLastY = y;
    //根据拖动举例设置view的margin参数,实现拖动效果
    LayoutParams params = new LayoutParams(mWidth, mHeight);
    params.setMargins(mLeft, mTop, 0, 0);
    setLayoutParams(params);
    break;
    case MotionEvent.ACTION_UP:
    //手指抬起,执行贴边动画
    if (isDrag) {
    startAnim();
    isDrag = false;
    }
    break;
    }
    return super.dispatchTouchEvent(event);
    }

    //执行贴边动画
    private void startAnim(){
    ValueAnimator valueAnimator;
    if (mLeft < mScreenWidth / 2) {
    valueAnimator = ValueAnimator.ofInt(mLeft, 0);
    } else {
    valueAnimator = ValueAnimator.ofInt(mLeft, mScreenWidth - mWidth);
    }
    //动画执行时间
    valueAnimator.setDuration(100);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation)
    {
    mLeft = (int)animation.getAnimatedValue();
    //动画执行依然是使用设置margin参数实现
    LayoutParams params = new LayoutParams(mWidth, mHeight);
    params.setMargins(mLeft, getTop(), 0, 0);
    setLayoutParams(params);
    }
    });
    valueAnimator.start();
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b)
    {
    super.onLayout(changed, l, t, r, b);
    if (mWidth == 0) {
    //获取view的高宽
    mWidth = getWidth();
    mHeight = getHeight();
    }
    }

    }


    /**
    *
    @author zhuxiaoxin
    * 37悬浮窗基础view
    */

    public class SqAddFloatView extends DragViewLayout {

    private RelativeLayout mFloatContainer;

    public SqAddFloatView(final Context context, final int floatImgId) {
    super(context);
    setClickable(true);
    final ImageView floatView = new ImageView(context);
    floatView.setImageResource(floatImgId);
    floatView.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
    Toast.makeText(context, "点击了悬浮球", Toast.LENGTH_SHORT).show();
    }
    });
    LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    addView(floatView, params);
    }

    public void show(Activity activity) {
    FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
    if(mFloatContainer == null) {
    mFloatContainer = new RelativeLayout(activity);
    }
    RelativeLayout.LayoutParams floatViewParams = new RelativeLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT);
    floatViewParams.setMargins(0, (int) (mScreenHeight * 0.4), 0, 0);
    mFloatContainer.addView(this, floatViewParams);
    activity.addContentView(mFloatContainer, params);

    }
    }


    在Activity中使用


    SqAddFloatView(this, R.mipmap.ic_launcher).show(this)


    3、方案三:WindowManager+应用内层级实现


    WindowManger中的层级有如下两个(其实是一样的~)可以实现在Activity上增加View


            /**
    * Start of types of sub-windows. The {@link #token} of these windows
    * must be set to the window they are attached to. These types of
    * windows are kept next to their attached window in Z-order, and their
    * coordinate space is relative to their attached window.
    */

    public static final int FIRST_SUB_WINDOW = 1000;

    /**
    * Window type: a panel on top of an application window. These windows
    * appear on top of their attached window.
    */

    public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;


    具体实现时,WindowManger相关的核心代码如下:


        public void show() {
    floatLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
    WindowManager.LayoutParams.WRAP_CONTENT,
    //最最重要的一句 WindowManager.LayoutParams.FIRST_SUB_WINDOW,
    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
    | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
    PixelFormat.RGBA_8888);
    floatLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
    floatLayoutParams.x = mMinWidth;
    floatLayoutParams.y = (int)(mScreenHeight * 0.4);
    mWindowManager.addView(this, floatLayoutParams);
    }


    添加完view如何更新位置?


    使用WindowManager的updateViewLayout方法


    mWindowManager.updateViewLayout(DragViewLayout.this, floatLayoutParams);


    完整代码如下:


    DragViewLayout:


    public class DragViewLayout extends RelativeLayout {

    //view所在位置
    int mLastX, mLastY;

    //屏幕高宽
    int mScreenWidth, mScreenHeight;

    //view高宽
    int mWidth, mHeight;

    /**
    * 是否在拖拽过程中
    */

    boolean isDrag = false;

    /**
    * 系统最小滑动距离
    * @param context
    */

    int mTouchSlop = 0;

    WindowManager.LayoutParams floatLayoutParams;
    WindowManager mWindowManager;

    //手指触摸位置
    private float xInScreen;
    private float yInScreen;
    private float xInView;
    public float yInView;


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

    public DragViewLayout(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public DragViewLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mScreenWidth = context.getResources().getDisplayMetrics().widthPixels;
    mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
    mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
    }

    @Override
    protected void onFinishInflate()
    {
    super.onFinishInflate();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event)
    {
    int action = event.getAction();
    switch (action) {
    case MotionEvent.ACTION_DOWN:
    mLastX = (int) event.getRawX();
    mLastY = (int) event.getRawY();
    yInView = event.getY();
    xInView = event.getX();
    xInScreen = event.getRawX();
    yInScreen = event.getRawY();
    break;
    case MotionEvent.ACTION_MOVE:
    int dx = (int) event.getRawX() - mLastX;
    int dy = (int) event.getRawY() - mLastY;
    if (Math.abs(dx) > mTouchSlop || Math.abs(dy) > mTouchSlop) {
    isDrag = true;
    }
    xInScreen = event.getRawX();
    yInScreen = event.getRawY();
    mLastX = (int) event.getRawX();
    mLastY = (int) event.getRawY();
    //拖拽时调用WindowManager updateViewLayout更新悬浮球位置
    updateFloatPosition(false);
    break;
    case MotionEvent.ACTION_UP:
    if (isDrag) {
    //执行贴边
    startAnim();
    isDrag = false;
    }
    break;
    default:
    break;
    }
    return super.dispatchTouchEvent(event);
    }

    //更新悬浮球位置
    private void updateFloatPosition(boolean isUp) {
    int x = (int) (xInScreen - xInView);
    int y = (int) (yInScreen - yInView);
    if(isUp) {
    x = isRightFloat() ? mScreenWidth : 0;
    }
    if(y < 0) {
    y = 0;
    }
    if(y > mScreenHeight - mHeight) {
    y = mScreenHeight - mHeight;
    }
    floatLayoutParams.x = x;
    floatLayoutParams.y = y;
    //更新位置
    mWindowManager.updateViewLayout(this, floatLayoutParams);
    }

    /**
    * 是否靠右边悬浮
    * @return
    */

    boolean isRightFloat() {
    return xInScreen > mScreenWidth / 2;
    }


    //执行贴边动画
    private void startAnim(){
    ValueAnimator valueAnimator;
    if (floatLayoutParams.x < mScreenWidth / 2) {
    valueAnimator = ValueAnimator.ofInt(floatLayoutParams.x, 0);
    } else {
    valueAnimator = ValueAnimator.ofInt(floatLayoutParams.x, mScreenWidth - mWidth);
    }
    valueAnimator.setDuration(200);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation)
    {
    floatLayoutParams.x = (int)animation.getAnimatedValue();
    mWindowManager.updateViewLayout(DragViewLayout.this, floatLayoutParams);
    }
    });
    valueAnimator.start();
    }

    //悬浮球显示
    public void show() {
    floatLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT,
    WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
    | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
    PixelFormat.RGBA_8888);
    floatLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
    floatLayoutParams.x = 0;
    floatLayoutParams.y = (int)(mScreenHeight * 0.4);
    mWindowManager.addView(this, floatLayoutParams);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b)
    {
    super.onLayout(changed, l, t, r, b);
    if (mWidth == 0) {
    //获取悬浮球高宽
    mWidth = getWidth();
    mHeight = getHeight();
    }
    }
    }


    悬浮窗View


    public class SqWindowManagerFloatView extends DragViewLayout {


    public SqWindowManagerFloatView(final Context context, final int floatImgId) {
    super(context);
    setClickable(true);
    final ImageView floatView = new ImageView(context);
    floatView.setImageResource(floatImgId);
    floatView.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
    Toast.makeText(context, "点击了悬浮球", Toast.LENGTH_SHORT).show();
    }
    });
    LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    addView(floatView, params);
    }
    }


    使用:


    SqWindowManagerFloatView(this, R.mipmap.float_icon).show()


    4、小结


    1、方案一需要用到多个权限,显然是不合适的。


    2、方案二简单方便,但是用到了Activity的addContentView方法,在某些游戏引擎上使用会有问题。因为有些游戏引擎不是在Activity上跑的,而是在NativeActivity上跑


    3、方案三是我们当前采用的方案,目前还暂未发现有显示不出来之类的问题~


    4、本文讲述的方案只是Demo哈,实际使用还需要考虑刘海屏的问题,本文暂未涉及


    代码下载:way-Doughnut-master.zip 收起阅读 »

    安卓自定义view - 2048 小游戏

    为了学习自定义 ViewGroup,正碰巧最近无意间玩了下 2048 的游戏,因此这里就来实现一个 2048 小游戏。想必很多人应该是玩过这个游戏的,如果没有玩过的可以下载玩一下。下图是我实现的效果。2048 游戏规则游戏规则比较简单,共有如下几个步骤:向一个...
    继续阅读 »

    为了学习自定义 ViewGroup,正碰巧最近无意间玩了下 2048 的游戏,因此这里就来实现一个 2048 小游戏。想必很多人应该是玩过这个游戏的,如果没有玩过的可以下载玩一下。下图是我实现的效果。

    2048 游戏规则

    游戏规则比较简单,共有如下几个步骤:

    1. 向一个方向移动,所有格子会向那个方向移动
    2. 相同的数字合并,即相加
    3. 每次移动时,空白处会随机出现一个数字2或4
    4. 当界面不可移动时,即格子被数字填满,游戏结束,网格中出现 2048 的数字游戏胜利,反之游戏失败。

    2048 游戏算法

    算法主要是讨论上下左右四个方向如何合并以及移动,这里我以向左和向上来说明,而向下和向右就由读者自行推导,因为十分相似。

    向左移动算法

    先来看下面两张图,第一张是初始状态,可以看到网格中有个数字 2。在这里用二维数组来描述。它的位置应该是第2行第2列 。第二张则是它向左移动后的效果图,可以看到 2 已经被移动到最左边啦!

    我们最常规的想法就是首先遍历这个二维数组,找到这个数的位置,接着合并和移动。所以第一步肯定是循环遍历。

    int i;
    for (int x = 0; x < 4; x++) {
    for (int y = 0; y < 4; ) {
    Model model = models[x][y];
    int number = model.getNumber();
    if (number == 0) {
    y++;
    continue;
    } else {
    // 找到不为零的位置. 下面就需要进行合并和移动处理

    }
    }
    }


    上面的代码非常简单,这里引入了 Model 类,这个类是封装了网格单元的数据和网格视图。定义如下:先不纠结视图的绘制,我们先把算法理清楚,算法搞明白了也就解决一大部分了,其他就是自定义 View 的知识。上述的过程就是,遍历整个网格,找到不为零的网格位置。


    public class Model {

    private int number;
    /**
    * 单元格视图.
    */

    private CellView cellView;

    public Model(int number, CellView cellView) {
    this.number = number;
    this.cellView = cellView;
    }

    public int getNumber() {
    return number;
    }

    public void setNumber(int number) {
    this.number = number;
    }

    public CellView getCellView() {
    return cellView;
    }

    public void setCellView(CellView cellView) {
    this.cellView = cellView;
    }
    }




    让我们来思考一下,合并要做什么,那么我们再来看一张图。

    从这张图中我们可以看到在第一行的最后两个网格单元都是2,当向左移动时,根据 2048 游戏规则,我们需要将后面的一个2 和前面的 2 进行合并(相加)运算。是不是可以推理,我们找到第一个不为零的数的位置,然后找到它右边第一个不为零的数,判断他们是否相等,如果相等就合并。算法如下:

    int i;
    for (int x = 0; x < 4; x++) {
    for (int y = 0; y < 4; ) {
    Model model = models[x][y];
    int number = model.getNumber();
    if (number == 0) {
    y++;
    continue;
    } else {
    // 找到不为零的位置. 下面就需要进行合并和移动处理
    // 这里的 y + 1 就是找到这个数的右侧
    for (i = y + 1; i < 4; i++) {
    if (models[x][i].getNumber() == 0) {
    continue;
    } else if (models[x][y].getNumber() == models[x][i].getNumber()) {
    // 找到相等的数
    // 合并,相加操作
    models[x][y].setNumber(
    models[x][y].getNumber() + models[x][i].getNumber())

    // 将这个数清0
    models[x][i].setNumber(0);

    break;
    } else {
    break;
    }
    }

    // 防止陷入死循环,所以必须要手动赋值,将其跳出。
    y = i;
    }
    }
    }


    通过上面的过程,我们就将这个数右侧的第一个相等的数进行了合并操作,是不是也好理解的。不理解的话可以在草稿纸上多画一画,多推导几次。

    搞定了合并操作,现在就是移动了,移动肯定是要将所有数据的单元格都移动到左侧,移动的条件是,找到第一个不为零的数的坐标,继续向前找到第一个数据为零即空白单元格的位置,将数据覆盖它,并将后一个单元格数据清空。算法如下:

    for (int x = 0; x < 4; x++) {
    for (y = 0; y < 4; y++) {
    if (models[x][y].getNumber() == 0) {
    continue;
    } else {
    // 找到当前数前面为零的位置,即空格单元
    for (int j = y; j >= 0 && models[x][j - 1].getNumber() == 0; j--) {
    // 数据向前移动,即数据覆盖.
    models[j - 1][y].setNumber(
    models[j][y].getNumber())
    // 清空数据
    models[j][y].setNumber(0)
    }
    }
    }
    }

    到此向左移动算法完毕,接着就是向上移动的算法。

    向上移动算法

    有了向左移动的算法思维,理解向上的操作也就变得容易一些啦!首先我们先来看合并,合并的条件也就是找到第一个不为零的数,然后找到它下一行第一个不为零且相等的数进行合并。算法如下:

    int i = 0;
    for (int y = 0; y < 4; y++) {
    for (x = 3; x >= 0; ) {
    if (models[x][y].getNumber() == 0) {
    continue;
    } else {
    for (i = x + 1; i < 4; i++) {
    if (models[i][y].getNumber() == 0) {
    continue;
    } else if (models[x][y].getNumber() == models[i][y].getNumber()) {
    models[x][y].setNumber(
    models[x][y].getNumber() + models[i][y].getNumber();
    )

    models[i][y].setNumber(0);

    break;
    } else {
    break;
    }
    }
    }
    }
    }


    移动的算法也类似,即找到第一个不为零的数前面为零的位置,即空格单元的位置,将数据覆盖并将后一个单元格的数据清空。

    for (int x = 0; x < 4; x++) {
    for (int y = 0; y < 4; y++) {
    if (models[x][y].getNumber() == 0) {
    continue;
    } else {
    for (int j = x; x >
    0 && models[j - 1][y].getNumber() == 0; j--) {
    models[j -1][y].setNumber(models[j][y].getNumber());

    models[j][y].setNumber(0);
    }
    }
    }
    }


    到此,向左移动和向上移动的算法就描述完了,接下来就是如何去绘制视图逻辑啦!

    网格单元绘制

    首先先忽略数据源,我们只是单纯的绘制网格,有人可能说了我们不用自定义的方式也能实现,我只想说可以,但是不推荐。如果使用自定义 ViewGroup,将每一个小的单元格作为单独的视图。这样扩展性更好,比如我做了对随机显示的单元格加上动画。

    既然是自定义 ViewGroup, 那我们就创建一个类并继承 ViewGroup,其定义如下:

    public class Play2048Group extends ViewGroup {

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

    public Play2048Group(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public Play2048Group(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    ......
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    .....
    }

    }


    我们要根据子视图的大小来测量容器的大小,在 onLayout 中摆放子视图。为了更好的交给其他开发者使用,我们尽量可以让 view 能被配置。那么就要自定义属性。

    1. 自定义属性

    这里只是提供了设置网格单元行列数,其实这里我我只取两个值的最大值作为行列的值。













    1. 布局中加载自定义属性

    可以看到将传入的 row 和 column 取大的作为行列数。

    public Play2048Group(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Play2048Group);

    try {
    mRow = a.getInteger(R.styleable.Play2048Group_row, 4);
    mColumn = a.getInteger(R.styleable.Play2048Group_column, 4);
    // 保持长宽相等排列, 取传入的最大值
    if (mRow > mColumn) {
    mColumn = mRow;
    } else {
    mRow = mColumn;
    }

    init();

    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    a.recycle();
    }
    }


    1. 网格子视图

    因为整个网格有一个个网格单元组成,其中每一个网格单元都是一个 view, 这个 view 其实也就只是绘制了一个矩形,然后在矩形的中间绘制文字。考虑文章篇幅,我这里只截取 onMeasure 和 onDraw 方法。

     @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    // 我这里直接写死了,当然为了屏幕适配,这个值应该由外部传入的,
    // 这里就当我留下的作业吧 😄
    setMeasuredDimension(130, 130);
    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // 绘制矩形.
    canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);

    // 如果当前单元格的数据不为0,就绘制。
    // 如果为零,就使用背景的颜色作为画笔绘制,这么做就是为了不让它显示出来😳
    if (!mNumber.equalsIgnoreCase("0")) {
    mTextPaint.setColor(Color.parseColor("#E451CD"));
    canvas.drawText(mNumber,
    (float) (getMeasuredWidth() - bounds.width()) / 2,
    (float) (getMeasuredHeight() / 2 + bounds.height() / 2), mTextPaint);
    } else {
    mTextPaint.setColor(Color.parseColor("#E4CDCD"));
    canvas.drawText(mNumber,
    (float) (getMeasuredWidth() - bounds.width()) / 2,
    (float) (getMeasuredHeight() / 2 + bounds.height() / 2), mTextPaint);
    }
    }



    1. 测量容器视图

    由于网格是行列数都相等,则宽和高都相等。那么所有的宽加起来除以 row, 所有的高加起来除以 column 就得到了最终的宽高, 不过记得要加上边距。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    int width = 0;
    int height = 0;

    int count = getChildCount();

    MarginLayoutParams layoutParams =
    (MarginLayoutParams)getChildAt(0).getLayoutParams();

    // 每一个单元格都有左边距和上边距
    int leftMargin = layoutParams.leftMargin;
    int topMargin = layoutParams.topMargin;

    for (int i = 0; i < count; i++) {
    CellView cellView = (CellView) getChildAt(i);
    cellView.measure(widthMeasureSpec, heightMeasureSpec);

    int childW = cellView.getMeasuredWidth();
    int childH = cellView.getMeasuredHeight();

    width += childW;
    height += childH;
    }

    // 需要加上每个单元格的左边距和上边距
    setMeasuredDimension(width / mRow + (mRow + 1) * leftMargin,
    height / mRow + (mColumn + 1) * topMargin);
    }


    1. 布局子视图(网格单元)

    布局稍微麻烦点,主要是在换行处的计算有点绕。首先我们找一下什么时候是该换行了,如果是 4 * 4 的 16 宫格,我们可以知道每一行的开头应该是 0、4、8、12,如果要用公式来表示的就是: temp = mRow * (i / mRow), 这里的 mRow 为行数,i 为索引。

    我们这里首先就是要确定每一行的第一个视图的位置,后面的视图就好确定了, 下面是推导过程:

    第一行: 
    网格1:
    left = lefMargin;
    top = topMargin;
    right = leftMargin + width;
    bottom = topMargin + height;

    网格2:
    left = leftMargin + width + leftMargin
    top = topMargin;
    right = leftMargin + width + leftMargin + width
    bottom = topMargin + height

    网格3:
    left = leftMargin + width + leftMargin + width + leftMargin
    right = leftMargin + width + leftMargin + width + leftMargin + width

    ...
    第二行:
    网格1:
    left = leftMargin
    top = topMargin + height
    right = leftMargin + width
    bottom = topMargin + height + topMargin + height

    网格2:
    left = leftMargin + width + leftMargin
    top = topMargin + height + topMargin
    right = leftMargin + width + lefMargin + width
    bottom = topMargin + height + topMargin + height


    上面的应该很简单的吧,这是根据画图的方式直观的排列,我们可以归纳总结,找出公式。

    除了每一行的第一个单元格的 left, right 都相等。 其他的可以用一个公式来总结:

    left = leftMargin * (i - temp + 1) + width * (i - temp)
    right = leftMargin * (i - temp + 1) + width * (i - temp + 1)

    可以随意带数值进入然后对比画图看看结果,比如(1, 1) 即第二行第二列。

    temp = row * (i / row) => 4 * 1 = 4

    left = leftMargin * (5 - 4 + 1) + width * (5 - 4)
    = leftMargin * 2 + width

    right = leftMargin * (5 - 4 + 1) + width * (5 - 4 + 1)
    = lefMargin * 2 + width * 2

    和上面的手动计算完全一样,至于为什么 i = 5 那是因为 i 循环到第二行的第二列为 5


    除了第一行第一个单元格其他的 top, bottom 可以用公式:

    top = height * row + topMargin * row + topMargin
    bottom = height * (row + 1) + topMargin(row + 1)


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
    CellView cellView = (CellView) getChildAt(i);
    MarginLayoutParams layoutParams = (MarginLayoutParams) cellView.getLayoutParams();
    int leftMargin = layoutParams.leftMargin;
    int topMargin = layoutParams.topMargin;

    int width = cellView.getMeasuredWidth();
    int height = cellView.getMeasuredHeight();

    int left = 0, top = 0, right = 0, bottom = 0;

    // 每一行开始, 0, 4, 8, 12...
    int temp = mRow * (i / mRow);
    // 每一行的开头位置.
    if (i == temp) {
    left = leftMargin;
    right = width + leftMargin;
    } else {
    left = leftMargin * (i - temp + 1) + width * (i - temp);
    right = leftMargin * (i - temp + 1) + + width * (i - temp + 1);
    }

    int row = i / mRow;
    if (row == 0) {
    top = topMargin;
    bottom = height + topMargin;
    } else {
    top = height * row + topMargin * row + topMargin;
    bottom = height * (row + 1) + (row + 1) * topMargin;
    }

    cellView.layout(left, top, right, bottom);
    }
    }


    1. 初始数据
    private void init() {
    models = new Model[mRow][mColumn];
    cells = new ArrayList<>(mRow * mColumn);

    for (int i = 0; i < mRow * mColumn; i++) {
    CellView cellView = new CellView(getContext());
    MarginLayoutParams params = new MarginLayoutParams(
    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

    params.leftMargin = 10;
    params.topMargin = 10;
    cellView.setLayoutParams(params);

    Model model = new Model(0, cellView);
    cells.add(model);

    addView(cellView, i);
    }
    }


    以上就是未带数据源的宫格绘制过程,接下来开始接入数据源来动态改变宫格的数据啦!

    动态改变数据

    1. 初始化数据源,随机显示一个数据 2
    private void init() {
    ... 省略部分代码.....

    int i = 0;
    for (int x = 0; x < mRow; x++) {
    for (int y = 0; y < mColumn; y++) {
    models[x][y] = cells.get(i);
    i++;
    }
    }

    // 生成一个随机数,初始化数据.
    mRandom = new Random();
    rand = mRandom.nextInt(mRow * mColumn);
    Model model = cells.get(rand);
    model.setNumber(2);
    CellView cellView = model.getCellView();
    cellView.setNumber(2);

    // 初始化时空格数为总宫格个数 - 1
    mAllCells = mRow * mColumn - 1;

    // 程序动态变化这个值,用来判断当前宫格还有多少空格可用.
    mEmptyCells = mAllCells;


    ... 省略部分代码.....
    }


    1. 计算随机数生成的合法单元格位置

    生成的随机数据必须在空白的单元格上。

     private void nextRand() {
    // 如果所有宫格被填满则游戏结束,
    // 当然这里也有坑,至于怎么发现,你多玩几次机会发现,
    // 这个坑我就不填了,有兴趣的可以帮我填一下😄😄
    if (mEmptyCells <= 0) {
    findMaxValue();
    gameOver();
    return;
    }

    int newX, newY;

    if (mEmptyCells != mAllCells || mCanMove == 1) {
    do {
    // 通过伪随机数获取新的空白位置
    newX = mRandom.nextInt(mRow);
    newY = mRandom.nextInt(mColumn);
    } while (models[newX][newY].getNumber() != 0);

    int temp = 0;

    do {
    temp = mRandom.nextInt(mRow);
    } while (temp == 0 || temp == 2);

    Model model = models[newX][newY];
    model.setNumber(temp + 1);
    CellView cellView = model.getCellView();
    cellView.setNumber(model.getNumber());
    playAnimation(cellView);

    // 空白格子减1
    mEmptyCells--;
    }
    }


    1. 向左移动

    算法是我们前面推导的,最后调用 drawAll() 绘制单元格文字, 以及调用 nextRand() 生成新的随机数。

    public void left() {
    if (leftRunnable == null) {
    leftRunnable = new Runnable() {
    @Override
    public void run() {
    int i;
    for (int x = 0; x < mRow; x++) {
    for (int y = 0; y < mColumn; ) {
    Model model = models[x][y];
    int number = model.getNumber();
    if (number == 0) {
    y++;
    continue;
    } else {
    // 找到不为零的位置. 往后找不为零的数进行运算.
    for (i = y + 1; i < mColumn; i++) {
    Model model1 = models[x][i];
    int number1 = model1.getNumber();
    if (number1 == 0) {
    continue;
    } else if (number == number1) {
    // 如果找到和这个相同的,则进行合并运算(相加)。
    int temp = number + number1;
    model.setNumber(temp);
    model1.setNumber(0);

    mEmptyCells++;
    break;
    } else {
    break;
    }
    }

    y = i;
    }
    }
    }

    for (int x = 0; x < mRow; x++) {
    for (int y = 0; y < mColumn; y++) {
    Model model = models[x][y];
    int number = model.getNumber();
    if (number == 0) {
    continue;
    } else {
    for (int j = y; (j > 0) && models[x][j - 1].getNumber() == 0; j--) {
    models[x][j - 1].setNumber(models[x][j].getNumber());
    models[x][j].setNumber(0);

    mCanMove = 1;
    }
    }
    }
    }

    drawAll();
    nextRand();
    }
    };
    }

    mExecutorService.execute(leftRunnable);
    }

    1. 随机单元格动画
    private void playAnimation(final CellView cellView) {
    mainHandler.post(new Runnable() {
    @Override
    public void run() {
    ObjectAnimator animator = ObjectAnimator.ofFloat(
    cellView, "alpha", 0.0f, 1.0f);
    animator.setDuration(300);
    animator.start();
    }
    });
    }


    代码下载:i1054959069-simple-2048-games-master.zip

    收起阅读 »

    一个你想象不到的验证码输入框!

    之所以造这个轮子,是因为之前有这样的需求,然后也用过其它类似开源的库(VerificationCodeView),但是需求随着需求的变动,之前使用的库就不太满足现有的需求。所以最近抽空写了一个。 支持设置框数量 支持设置框的风格样式&nbs...
    继续阅读 »

    SplitEditText for Android 是一个灵活的分割编辑框。常常应用于 验证码输入 、密码输入 、等场景。

    之所以造这个轮子,是因为之前有这样的需求,然后也用过其它类似开源的库(VerificationCodeView),但是需求随着需求的变动,之前使用的库就不太满足现有的需求。所以最近抽空写了一个。

    特性说明

    •  支持设置框数量
    •  支持设置框的风格样式
    •  支持根据状态区分框颜色
    •  基于EditText实现,更优雅


    SplitEditText 自定义属性说明

    属性值类型默认值说明
    setStrokeWidthdimension1dp画笔描边的宽度
    setBorderColorcolor#FF666666边框颜色
    setInputBorderColorcolor#FF1E90FF已输入文本的边框颜色
    setFocusBorderColorcolor焦点框的边框颜色
    setBoxBackgroundColorcolor框的背景颜色
    setBorderCornerRadiusdimension0dp框的圆角大小(当 BorderSpacing 为 0dp 时,只有最左和最右两端的框有圆角)
    setBorderSpacingdimension8dp框与框之间的间距大小
    setMaxLengthinteger6允许输入的最大长度(框个数量)
    setBorderStyleenumbox边框风格
    setTextStyleenumplain_text文本风格(可以是明文或者密文,默认:明文)
    setCipherMaskstring*密文掩码(当 TextStyle 为密文时,可自定义密文掩码)
    setFakeBoldTextbooleanfalse是否是粗体

    引入

    Maven:


    com.king.view
    splitedittext
    1.0.0
    pom

    Gradle:

    //AndroidX
    implementation 'com.king.view:splitedittext:1.0.0'

    如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)

    allprojects {
    repositories {
    maven { url 'https://dl.bintray.com/jenly/maven' }
    }
    }

    示例

    布局示例

        
    android:id="@+id/splitEditText"
    android:layout_width="match_parent"
    android:layout_height="45dp"
    android:inputType="number"/>

    代码示例

    Kotlin

        //设置监听
    splitEditText.setOnTextInputListener(object : SplitEditText.OnTextInputListener {
    override fun onTextInputChanged(text: String, length: Int) {
    //TODO 文本输入改变
    }

    override fun onTextInputCompleted(text: String) {
    //TODO 文本输入完成
    }

    })

    Java

        //设置监听
    splitEditText.setOnTextInputListener(new SplitEditText.OnTextInputListener(){

    @Override
    public void onTextInputChanged(String text, int length) {
    //TODO 文本输入改变
    }

    @Override
    public void onTextInputCompleted(String text) {
    //TODO 文本输入完成
    }
    });

    更多使用详情,请查看app中的源码使用示例

    代码下载:jenly1314-SplitEditText-master



    收起阅读 »

    你还没用Logger?用了他我才知道屌

    Logger简单,漂亮,强大的android日志 配置下载 implementation 'com.orhanobut:logger:2.2.0' 初始化 Logger.addLogAdapter(new AndroidLogAdapter()); 使用 ...
    继续阅读 »

    Logger

    简单,漂亮,强大的android日志


    配置

    下载


    implementation 'com.orhanobut:logger:2.2.0'

    初始化


    Logger.addLogAdapter(new AndroidLogAdapter());

    使用


    Logger.d("hello");

    输出


    属性

    Logger.d("debug");
    Logger.e("error");
    Logger.w("warning");
    Logger.v("verbose");
    Logger.i("information");
    Logger.wtf("What a Terrible Failure");

    支持字符串格式参数


    Logger.d("hello %s", "world");

    支持集合(仅适用于调试日志)


    Logger.d(MAP);
    Logger.d(SET);
    Logger.d(LIST);
    Logger.d(ARRAY);

    Json和Xml支持(输出将处于调试级别)


    Logger.json(JSON_CONTENT);
    Logger.xml(XML_CONTENT);

    高级用法

    FormatStrategy formatStrategy = PrettyFormatStrategy.newBuilder()
    .showThreadInfo(false) // (可选)是否显示线程信息。默认为 true
    .methodCount(0) // (可选)要显示的方法行数。默认为 2
    .methodOffset(7) // (可选)隐藏内部方法调用直到偏移量。默认值5
    .logStrategy(customLog) // (可选)将日志策略更改为打印输出。默认LogCat
    .tag("My custom tag") // (可选)每个日志的全局标记。默认PRETTY_LOGGER
    .build();

    Logger.addLogAdapter(new AndroidLogAdapter(formatStrategy));

    日志开启

    日志适配器通过检查此功能来检查日志是否应打印。


    如果要禁用/隐藏输出日志,请重写isLoggable'方法。true会打印日志消息,false` 会忽略它。


    Logger.addLogAdapter(new AndroidLogAdapter() {
    @Override public boolean isLoggable(int priority, String tag) {
    return BuildConfig.DEBUG;
    }
    });

    将日志保存到文件

    //TODO: 稍后将添加更多信息


    Logger.addLogAdapter(new DiskLogAdapter());

    将自定义标记添加到Csv格式策略


    FormatStrategy formatStrategy = CsvFormatStrategy.newBuilder()
    .tag("custom")
    .build();

    Logger.addLogAdapter(new DiskLogAdapter(formatStrategy));

    工作原理


    更多


    • 使用过滤器以获得更好的结果。或者你的自定义标签


    • 确保已禁用“环绕”选项


    • 也可以通过更改设置来简化输出。





    • Timber 集成
      // 将methodOffset设置为5以隐藏内部方法调用
      Timber.plant(new Timber.DebugTree() {
      @Override protected void log(int priority, String tag, String message, Throwable t) {
      Logger.log(priority, tag, message, t);
      }
      });


    github地址:https://github.com/orhanobut/logger
    下载地址:
    master.zip


    收起阅读 »

    Android RecyclerView 通用适配器

    使用方式【最新版本号以这里为准】由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!由于JCenter关...
    继续阅读 »

    使用方式

    【最新版本号以这里为准】

    由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!
    由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!
    由于JCenter关闭服务,从1.4.5版本开始改为发布到MavenCentral,引用方式有更新!!!

    #last-version请查看上面的最新版本号

    #只支持AndroidX

    #从1.4.5版本开始GroupId、ArtifactId均有更新,请按如下方式引用
    implementation "com.lwkandroid.library:rcvadapter:last-version"

    基础功能

    • 快速实现适配器,支持多种ViewType模式
    • 支持添加HeaderView、FooterView、EmptyView
    • 支持滑到底部加载更多
    • 支持每条Item显示的动画
    • 支持嵌套Section(1.1.0版本新增)
    • 支持悬浮标签StickyLayout(1.2.0版本新增)

    效果图






    使用方式

    1. 当Item样式一样时,只需继承RcvSingleAdapter<T>即可,示例:

    public class TestSingleAdapter extends RcvSingleAdapter<TestData>
    {
    public TestSingleAdapter(Context context, List<TestData> datas)
    {
    super(context, android.R.layout.simple_list_item_1, datas);
    }

    @Override
    public void onBindView(RcvHolder holder, TestData itemData, int position)
    {
    //在这里绑定UI和数据,RcvHolder中提供了部分快速设置数据的方法,详情请看源码
    holder.setTvText(android.R.id.text1, itemData.getContent());
    }
    }


    2. 当Item样式不一样时,即存在多种ViewType类型的Item,需要将每种ViewType的Item单独实现,再关联到RcvMultiAdapter<T>中,示例:

    //第一步:每种Item分别继承RcvBaseItemView<T>
    public class LeftItemView extends RcvBaseItemView<TestData>
    {
    @Override
    public int getItemViewLayoutId()
    {
    //这里返回该Item的布局id
    return R.layout.layout_item_left;
    }

    @Override
    public boolean isForViewType(TestData item, int position)
    {
    //这里判断何时引用该Item
    return position % 2 == 0;
    }

    @Override
    public void onBindView(RcvHolder holder, TestData testData, int position)
    {
    //在这里绑定UI和数据,RcvHolder中提供了部分快速设置数据的方法,详情请看源码
    holder.setTvText(R.id.tv_left, testData.getContent());
    }
    }

    //第二步:将所有Item关联到适配器中
    public class TestMultiAdapter extends RcvMultiAdapter<TestData>
    {
    public TestMultiAdapter(Context context, List<TestData> datas)
    {
    super(context, datas);
    //只需在构造方法里将所有Item关联进来,无论多少种ViewType都轻轻松松搞定
    addItemView(new LeftItemView());
    addItemView(new RightItemView());
    }
    }


    3.优雅的添加HeaderView、FooterView、EmptyView,只需要在RecyclerView设置LayoutManager后调用相关方法即可:

    //要先设置LayoutManager
    mRecyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));

    //添加HeaderView(若干个)
    mAdapter.addHeaderView(headerView01,headerView02,headerView03...);

    //添加FooterView(若干个)
    mAdapter.addFooterView(footerView01,footerView02,footerView03...);

    //添加EmptyView(只能设置一个)
    //设置了EmptyView后,当数据量为0的时候会显示EmptyView
    mAdapter.setEmptyView(emptyView);
    或者
    mAdapter.setEmptyView(layoutId);


    4.设置滑动到底部自动加载更多,先上示例代码吧:

    自1.4.3版本开始删除了之前的调用方式

    //可以先设置加载样式,继承RcvBaseLoadMoreView实现自定义样式
    //不设置的话会使用默认的样式,参考RcvDefLoadMoreView源码
    RcvDefLoadMoreView loadMoreView = new RcvDefLoadMoreView.Builder()
    .setBgColor(Color.GREEN)
    .setTextColor(Color.RED)
    .build(this);
    mAdapter.setLoadMoreLayout(loadMoreView);
    //再开启并设置监听
    mAdapter.enableLoadMore(true);
    mAdapter.setOnLoadMoreListener(RcvLoadMoreListener listener);
    //禁止加载更多,通常用在配合下拉刷新的过程中
    mAdapter.enableLoadMore(false);

    注:
    ① 默认的样式实现是类RcvDefLoadMoreView
    ② 如需自定义样式,只需继承RcvBaseLoadMoreView,只要重写各状态UI的实现,无须关心状态切换,可参考RcvDefLoadMoreView内的实现方式。

    5.设置Item显示动画,先直接上代码:

    //使用默认的动画(Alpha动画)
    mAdapter.enableItemShowingAnim(true);

    //使用自定义动画
    mAdapter.enableItemShowingAnim(true, ? extends RcvBaseAnimation);

    注:
    ①默认动画的实现是类RcvAlphaInAnim
    ②自定义样式需要继承RcvBaseAnimation,可参考RcvAlphaInAnim内部实现。

    6.设置Item点击监听:

        //设置OnItemClickListener
    mAdapter.setOnItemClickListener(new RcvItemViewClickListener<TestData>()
    {
    @Override
    public void onItemViewClicked(RcvHolder holder, TestData testData, int position)
    {
    //onClick回调
    }
    });

    //设置OnItemLongClickListener
    mAdapter.setOnItemLongClickListener(new RcvItemViewLongClickListener<TestData>()
    {
    @Override
    public void onItemViewLongClicked(RcvHolder holder, TestData testData, int position)
    {
    //onLongClick回调
    }
    });


    7. 添加分割线,直接上代码:

    1.2.9版本针对分割线进行了重写,原有方法不变,新增支持自定义颜色和部分快速创建的方法:

    #适用于LinearLayoutManager
    //创建默认竖直排列的分割线
    RcvLinearDecoration.createDefaultVertical(Context context);
    //创建自定义色值默认竖直排列的分割线
    RcvLinearDecoration.createDefaultVertical(int color);
    //创建默认水平排列的分割线
    RcvLinearDecoration.createDefaultHorizontal(Context context);
    //创建自定义色值默认水平排列的分割线
    RcvLinearDecoration.createDefaultHorizontal(int color);
    //构造方法:默认Drawable分割线
    new RcvLinearDecoration(Context context, int orientation);
    //构造方法:自定义Drawable分割线
    new RcvLinearDecoration(Context context, Drawable drawable, int orientation);
    //构造方法:自定义Drawable分割线
    new RcvLinearDecoration(Context context, @DrawableRes int drawableResId, int orientation);
    //构造方法:自定义Color分割线(宽度或者高度默认1px)
    new RcvLinearDecoration(@ColorInt int color, int orientation);
    //构造方法:自定义Color分割线
    new RcvLinearDecoration(@ColorInt int color, int size, int orientation);

    #适用于GridLayoutManager、StaggeredGridLayoutManager
    //创建默认分割线
    RcvGridDecoration.createDefault(Context context);
    //创建自定义色值默认分割线
    RcvGridDecoration.createDefault(int color);
    //构造方法:默认Drawable的分割线
    new RcvGridDecoration(Context context);
    //构造方法:自定义Drawable的分割线
    new RcvGridDecoration(Context context, Drawable drawable);
    //构造方法:自定义Drawable的分割线
    new RcvGridDecoration(Context context, @DrawableRes int drawableResId);
    //构造方法:自定义Color的分割线(默认分割线宽高均为1px)
    new RcvGridDecoration(@ColorInt int color);
    //构造方法:自定义Color的分割线
    new RcvGridDecoration(@ColorInt int color, int width, int height);

    注:
    ①是直接设置给RecyclerView的,不是设置给适配器的,不要看错哦
    ②支持自定义drawable当分割线

    8.嵌套Section,稍微复杂一点,配合代码讲解:

    1.4.0版本开始删除以前的使用方法,采用下面的方式

    带有Section功能的适配器为RcvSectionMultiLabelAdapterRcvSectionSingleLabelAdapter,需要指定两个泛型,第一个代表Section,第二个代表普通数据Data, 两者都支持多种Data类型的子布局,唯一不同的是,RcvSectionMultiLabelAdapter还支持多种Section类型的子布局,但不可以和RcvStickyLayout联动,而RcvSectionSingleLabelAdapter 仅支持一种Section类型的子布局,但是可以和RcvStickyLayout联动。需要注意的是,传给适配器的数据均需要自行预处理,用RcvSectionWrapper封装后才可传入适配器。

    #只有一种Section类型,配合多种Data类型的适配器
    public class TestSectionAdapter extends RcvSectionSingleLabelAdapter<TestSection, TestData>
    {
    public TestSectionAdapter(Context context, List<RcvSectionWrapper<TestSection, TestData>> datas)
    {
    super(context, datas);
    }

    @Override
    protected RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>[] createDataItemViews()
    {
    return new RcvBaseItemView[]{new DataItemView01(), new DataItemView02()};
    }

    @Override
    public int getSectionLabelLayoutId()
    {
    return R.layout.layout_section_label;
    }

    @Override
    public void onBindSectionLabelView(RcvHolder holder, TestSection section, int position)
    {
    holder.setTvText(R.id.tv_section_label, section.getSection());
    }

    //第一种Data ItemView
    private class DataItemView01 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
    {
    @Override
    public int getItemViewLayoutId()
    {
    return R.layout.adapter_item_long;
    }

    @Override
    public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
    {
    return !item.isSection() && item.getData().getType() == 0;
    }

    @Override
    public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
    {
    TextView textView = holder.findView(R.id.tv_item_long);
    textView.setBackgroundColor(Color.GREEN);
    textView.setText("第一种数据类型:" + wrapper.getData().getContent());
    }
    }

    //第二种Data ItemView
    private class DataItemView02 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
    {
    @Override
    public int getItemViewLayoutId()
    {
    return R.layout.adapter_item_short;
    }

    @Override
    public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
    {
    return !item.isSection() && item.getData().getType() != 0;
    }

    @Override
    public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
    {
    TextView textView = holder.findView(R.id.tv_item_short);
    textView.setBackgroundColor(Color.RED);
    textView.setText("第二种数据类型:" + wrapper.getData().getContent());
    }
    }
    }

    #多种Section类型,配合多种Data类型的适配器
    public class TestSectionMultiLabelAdapter extends RcvSectionMultiLabelAdapter<TestSection, TestData>
    {
    public TestSectionMultiLabelAdapter(Context context, List<RcvSectionWrapper<TestSection, TestData>> datas)
    {
    super(context, datas);
    }

    @Override
    protected RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>[] createLabelItemViews()
    {
    return new RcvBaseItemView[]{new LabelItemView01(), new LabelItemView02()};
    }

    @Override
    protected RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>[] createDataItemViews()
    {
    return new RcvBaseItemView[]{new DataItemView01(), new DataItemView02()};
    }


    //第一种Label ItemView
    private class LabelItemView01 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
    {
    @Override
    public int getItemViewLayoutId()
    {
    return R.layout.layout_section_label;
    }

    @Override
    public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
    {
    return item.isSection() && item.getSection().getType() == 0;
    }

    @Override
    public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
    {
    holder.setTvText(R.id.tv_section_label, wrapper.getSection().getSection());
    }
    }

    //第二种Label ItemView
    private class LabelItemView02 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
    {
    @Override
    public int getItemViewLayoutId()
    {
    return R.layout.layout_section_label02;
    }

    @Override
    public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
    {
    return item.isSection() && item.getSection().getType() != 0;
    }

    @Override
    public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
    {
    holder.setTvText(R.id.tv_section_label, wrapper.getSection().getSection());
    }
    }

    //第一种Data ItemView
    private class DataItemView01 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
    {
    @Override
    public int getItemViewLayoutId()
    {
    return R.layout.adapter_item_long;
    }

    @Override
    public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
    {
    return !item.isSection() && item.getData().getType() == 0;
    }

    @Override
    public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
    {
    TextView textView = holder.findView(R.id.tv_item_long);
    textView.setBackgroundColor(Color.GREEN);
    textView.setText("第一种数据类型:" + wrapper.getData().getContent());
    }
    }

    //第二种Data ItemView
    private class DataItemView02 extends RcvBaseItemView<RcvSectionWrapper<TestSection, TestData>>
    {
    @Override
    public int getItemViewLayoutId()
    {
    return R.layout.adapter_item_short;
    }

    @Override
    public boolean isForViewType(RcvSectionWrapper<TestSection, TestData> item, int position)
    {
    return !item.isSection() && item.getData().getType() != 0;
    }

    @Override
    public void onBindView(RcvHolder holder, RcvSectionWrapper<TestSection, TestData> wrapper, int position)
    {
    TextView textView = holder.findView(R.id.tv_item_short);
    textView.setBackgroundColor(Color.RED);
    textView.setText("第二种数据类型:" + wrapper.getData().getContent());
    }
    }
    }

    注:
    ①传给适配器的数据集合内实体类必须经过RcvSectionWrapper包装。
    ②向外公布的方法(例如点击监听)的实体类泛型不能传错。

    9.悬浮标签StickyLayout

    适配器方面无需改动,直接使用RcvSectionSingleLabelAdapter即可,在RecyclerView同级布局下添加RcvStickyLayout,然后在代码中关联起来即可:

        // xml布局中添加RcvStickyLayout:
    <FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
    android:id="@+id/rcv_sticky"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

    <com.lwkandroid.rcvadapter.ui.RcvStickyLayout
    android:id="@+id/stickyLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>
    </FrameLayout>



    //代码中关联RecyclerView
    RecyclerView recyclerView = (RecyclerView) findViewById(R.id.rcv_sticky);
    /...省略设置RecyclerView的LayoutMananger和Adapter.../
    RcvStickyLayout stickyLayout = (RcvStickyLayout) findViewById(R.id.stickyLayout);
    stickyLayout.attachToRecyclerView(recyclerView);

    上面就是大部分基础功能的使用方法了,想了解更多方法请看源码。

    混淆配置

    -dontwarn com.lwkandroid.rcvadapter.**
    -keep class com.lwkandroid.rcvadapter.**{*;}


    待实现功能

    • 暂时未想到

    开源参考

    1. https://github.com/hongyangAndroid/baseAdapter
    2. https://github.com/CymChad/BaseRecyclerViewAdapterHelper
    收起阅读 »

    一行代码解决RxJava 内存泄漏

    xLifeRxLife,相较于trello/RxLifecycle、uber/AutoDispose,具有如下优势:直接支持在主线程回调支持在子线程订阅观察者简单易用,学习成本低性能更优,在实现上更加简单友情提示: RxLife与RxHttp搭配使用,味道更佳...
    继续阅读 »

    xLife

    RxLife,相较于trello/RxLifecycleuber/AutoDispose,具有如下优势:

    • 直接支持在主线程回调
    • 支持在子线程订阅观察者
    • 简单易用,学习成本低
    • 性能更优,在实现上更加简单

    友情提示: RxLife与RxHttp搭配使用,味道更佳

    RxLife详细介绍:https://juejin.im/post/5cf3e1235188251c064815f1

    Gradle引用

    jitpack添加到项目的build.gradle文件中,如下:

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

    注:RxLife 2.1.0 版本起,已全面从JCenter迁移至jitpack

    新版本仅支持AndroidX项目

    dependencies {
    //kotlin协程
    implementation 'com.github.liujingxing.rxlife:rxlife-coroutine:2.1.0'

    //rxjava2
    implementation 'com.github.liujingxing.rxlife:rxlife-rxjava2:2.1.0'

    //rxjava3
    implementation 'com.github.liujingxing.rxlife:rxlife-rxjava3:2.1.0'
    }

    注意:RxJava2 使用Rxlife.asXxx方法;RxJava3使用Rxlife.toXxx方法

    非AndroidX项目

    非AndroidX项目,请使用旧版本RxLife

    implementation 'com.rxjava.rxlife:rxlife:2.0.0'

    由于Google在19年就停止了非AndroidX库的更新,故rxlife旧版本不再维护,请尽快将项目迁移至AndroidX

    #Usage

    1、FragmentActivity/Fragment

    FragmentActivity/Fragment销毁时,自动关闭RxJava管道

    Observable.timer(5, TimeUnit.SECONDS)
    .as(RxLife.as(this)) //此时的this FragmentActivity/Fragment对象
    .subscribe(aLong -> {
    Log.e("LJX", "accept =" + aLong);
    });

    2、View

    View被移除时,自动关闭RxJava管道

    Observable.timer(5, TimeUnit.SECONDS)
    .as(RxLife.as(this)) //此时的this 为View对象
    .subscribe(aLong -> {
    Log.e("LJX", "accept =" + aLong);
    });

    3、ViewModel

    Activity/Fragment销毁时,自动关闭RxJava管道,ViewModel需要继承ScopeViewModel类,如下

    public class MyViewModel extends ScopeViewModel {

    public MyViewModel(@NonNull Application application) {
    super(application);
    }

    public void test(){
    Observable.interval(1, 1, TimeUnit.SECONDS)
    .as(RxLife.asOnMain(this)) //继承ScopeViewModel后,就可以直接传this
    .subscribe(aLong -> {
    Log.e("LJX", "MyViewModel aLong=" + aLong);
    });
    }
    }

    注意: 一定要在Activity/Fragment通过以下方式获取ViewModel对象,否则RxLife接收不到生命周期的回调


    MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);

    4、任意类

    Activity/Fragment销毁时,自动关闭RxJava管道,任意类需要继承BaseScope类,如P层:

    public class Presenter extends BaseScope {

    public Presenter(LifecycleOwner owner) {
    super(owner); //添加生命周期监听
    }

    public void test(){
    Observable.interval(1, 1, TimeUnit.SECONDS)
    .as(RxLife.as(this)) //继承BaseScope后,就可以直接传this
    .subscribe(aLong -> {
    Log.e("LJX", "accept aLong=" + aLong);
    });
    }
    }

    5、kotlin用户

    由于as是kotlin中的一个关键字,所以在kotlin中,我们并不能直接使用as(RxLife.as(this)),可以如下编写

    Observable.intervalRange(1, 100, 0, 200, TimeUnit.MILLISECONDS)
    .`as`(RxLife.`as`(this))
    .subscribe { aLong ->
    Log.e("LJX", "accept=" + aLong)
    }

    当然,相信没多少人会喜欢这种写法,故,RxLife针对kotlin用户,新增更为便捷的写法,如下:

    Observable.intervalRange(1, 100, 0, 200, TimeUnit.MILLISECONDS)
    .life(this)
    .subscribe { aLong ->
    Log.e("LJX", "accept=" + aLong)
    }

    使用life 操作符替代as操作符即可,其它均一样

    6、小彩蛋

    asOnMain操作符

    RxLife还提供了asOnMain操作符,它可以指定下游的观察者在主线程中回调,如下:

    Observable.timer(5, TimeUnit.SECONDS)
    .as(RxLife.asOnMain(this))
    .subscribe(aLong -> {
    //在主线程回调
    Log.e("LJX", "accept =" + aLong);
    });

    //等价于
    Observable.timer(5, TimeUnit.SECONDS)
    .observeOn(AndroidSchedulers.mainThread())
    .as(RxLife.as(this))
    .subscribe(aLong -> {
    //在主线程回调
    Log.e("LJX", "accept =" + aLong);
    });

    kotlin 用户使用lifeOnMain替代asOnMain操作符,其它均一样

    注意: RxLife类里面as操作符,皆适用于Flowable、ParallelFlowable、Observable、Single、Maybe、Completable这6个被观察者对象

    混淆

    RxLife作为开源库,可混淆,也可不混淆,如果不希望被混淆,请在proguard-rules.pro文件添加以下代码

    -keep class com.rxjava.rxlife.**{*;}


    代码下载:327744707-rxjava-RxLife-master.zip

    收起阅读 »

    Android替换系统dialog风格后的通用提示框工具类

    DialogUtilsApp一、介绍替换系统dialog风格后的通用提示框工具类,可以覆盖lib下的定义资源,改变现有的颜色风格,需要改变布局风格,可下载项目后自行调整APP 使用示例项目,libs下含有已编译最新的aar资源。dialogutilslib a...
    继续阅读 »

    DialogUtilsApp

    一、介绍

    替换系统dialog风格后的通用提示框工具类,可以覆盖lib下的定义资源,改变现有的颜色风格,需要改变布局风格,可下载项目后自行调整

    • APP 使用示例项目,libs下含有已编译最新的aar资源。
    • dialogutilslib arr资源项目,需要引入的资源包项目。
    • aar文件生成,在工具栏直接Gradle - (项目名) - dialogutilslib - Tasks - build - assemble,直到编译完成
    • aar文件位置,打开项目所在文件夹,找到 dialogutilslib\build\outputs\aar 下。

    二、工程引入工具包准备

    下载项目,可以在APP项目的libs文件下找到DialogUtilsLib.aar文件(已编译为最新版),引入自己的工程 引入aar

    dependencies {
    implementation files('libs/DialogUtilsLib-release.aar')
    ...
    }

    三、使用

    注意下方只做了基础展示,dialog的都会返回对应的utils对象,registerActivityLifecycleCallbacks方法设置后,activity销毁时会自动把显示在此activity上的dialog一起关闭。

    • application初始化设置
    public class App extends Application {

    @Override
    public void onCreate() {
    super.onCreate();

    //初始化dialog工具类设置
    DialogLibInitSetting.getInstance()
    //设置debug
    .setDebug(BuildConfig.DEBUG)
    //注册全局activity生命周期监听
    .registerActivityLifecycleCallbacks(this);

    }
    }
    • 普通dialog
                DialogLibCommon.create(this)
    .setMessage("普通对话框1")
    .setAlias("text1")
    .setOnBtnMessage(()->{
    //描述区域点击时触发
    })
    .noShowCancel()
    .show();
    • 自定义dialog
                ImageView imageView = new ImageView(this);
    imageView.setImageDrawable(getResources().getDrawable(R.mipmap.ic_launcher));
    DialogLibCustom.create(this)
    .noShowCancel()
    .setAlias("text2")
    .show(imageView);
    • 输入型dialog
                DialogLibInput.create(this)
    .setMessage("输入信息")
    .setAlias("text3")
    //自动弹出键盘
    .setPopupKeyboard()
    .setOnBtnOk(str -> {
    Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
    return true;
    })
    .show();
    • 等待型dialog
                DialogLibLoading.create(this)
    .setTimeoutClose(2000)
    .setAlias("text4")
    .setOnLoading(() -> {
    Toast.makeText(MainActivity.this, "我是显示对话框前触发的", Toast.LENGTH_SHORT).show();
    })
    .show();
    • 完全自定义型dialog
                final DialogLibAllCustom dialog = DialogLibAllCustom.create(this)
    .setCancelable(true)
    .setAlias("text5");

    TextView view = new TextView(this);
    view.setBackgroundResource(R.color.design_default_color_secondary);
    view.setText("这是一个完全自定义布局的对话框,对话框显示后需要手动关闭");
    view.setOnClickListener(v2 -> {
    dialog.closeDialog();
    });

    dialog.show(view);
    • 密码输入型dialog
                  DialogLibInput.create(this)
    .setMessage("123")
    .setLength(6)
    .setInputType(EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD)
    .setAlias("text6")
    //设置显示密码隐藏/显示图片
    .setShowLookPassword()
    //自动弹出键盘
    .setPopupKeyboard()
    .setOnBtnOk(str -> {
    Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
    return true;
    })
    .show();

    四、资源覆盖,改变颜色、字体大小、默认文字

    • colors下可覆盖资源及注释,默认黑色和白色不建议覆盖,前景色:字体的颜色,背景色:布局的背景色
    <resources>
    <!--黑色-->
    <color name="dialog_utils_lib_black">#FF000000</color>
    <!--白色-->
    <color name="dialog_utils_lib_white">#FFFFFFFF</color>

    <!--dialog的标题文字的前景色,适用于所有带标题的dialog-->
    <color name="dialog_utils_lib_title_fg">@color/dialog_utils_lib_black</color>
    <!--dialog的 确认 按钮文字的前景色-->
    <color name="dialog_utils_lib_ok_fg">@color/dialog_utils_lib_white</color>
    <!--dialog的 取消 按钮文字的前景色-->
    <color name="dialog_utils_lib_cancel_fg">@color/dialog_utils_lib_white</color>
    <!--dialog的 确认 按钮文字的背景色-->
    <color name="dialog_utils_lib_ok_bg">#22C5A3</color>
    <!--dialog的 取消 按钮文字的背景色-->
    <color name="dialog_utils_lib_cancel_bg">#F8A01A</color>
    <!--dialog的输入框下方显示2个按钮时,中间分隔的背景色-->
    <color name="dialog_utils_lib_button_split_bg">@color/dialog_utils_lib_white</color>

    <!--dialog的内容文字的前景色,适用于 DialogLibCommonUtils-->
    <color name="dialog_utils_lib_content_fg">@color/dialog_utils_lib_black</color>

    <!--dialog的输入框文字的前景色,适用于 DialogLibInputUtils-->
    <color name="dialog_utils_lib_input_fg">@color/dialog_utils_lib_black</color>
    <!--dialog的输入框下方分割线的背景色,适用于 DialogLibInputUtils-->
    <color name="dialog_utils_lib_input_split_line">@color/dialog_utils_lib_ok_bg</color>

    <!--dialog的加载框加载等待区域的背景色-->
    <color name="dialog_utils_lib_loading_content_bg">#FFc4c4c4</color>
    <!--dialog的加载框加载等待区域文字提示的前景色-->
    <color name="dialog_utils_lib_loading_content_text_fg">@color/dialog_utils_lib_white</color>
    </resources>
    • dimens下字体大小资源
    <resources>
    <dimen name="dialog_utils_lib_text_size_normal">14sp</dimen>

    <!--标题字体大小,统一设定-->
    <dimen name="dialog_utils_lib_title_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
    <!--确定 字体大小,统一设定-->
    <dimen name="dialog_utils_lib_ok_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
    <!--取消 字体大小,统一设定-->
    <dimen name="dialog_utils_lib_cancel_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
    <!--内容 字体大小,适用于 DialogLibCommonUtils的提示内容区域-->
    <dimen name="dialog_utils_lib_content_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
    <!--输入框 字体大小,适用于 DialogLibInputUtils 输入区域-->
    <dimen name="dialog_utils_lib_input_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>
    <!--加载框 字体大小,适用于 DialogLibLoadingUtils 提示内容区域-->
    <dimen name="dialog_utils_lib_loading_text_size">@dimen/dialog_utils_lib_text_size_normal</dimen>

    <!--dialog 宽度占屏幕宽度的百分比,取值0-1之间,不包含边界,竖屏时的系数-->
    <item name="dialog_utils_lib_portrait_width_factor" format="float" type="dimen">0.85</item>
    <!--dialog 宽度占屏幕宽度的百分比,取值0-1之间,不包含边界,横屏时的系数-->
    <item name="dialog_utils_lib_landscape_width_factor" format="float" type="dimen">0.5</item>
    </resources>
    • strings下资源定义,注意:如果你的项目存在多语言,则必须覆盖
    <resources>
    <string name="dialog_utils_lib_ok">确定</string>
    <string name="dialog_utils_lib_cancel">取消</string>
    <string name="dialog_utils_lib_default_title">提示</string>
    <string name="dialog_utils_lib_data_processing">数据处理中…</string>
    </resources>
    • mipmap下资源定义,注意:此2张图片为密码输入时显示/隐藏按钮的图片,png格式
    dialog_utils_lib_password_hide 隐藏图片命名
    dialog_utils_lib_password_show 显示图片命名

    代码下载:mjsoftking-dialog-utils-app-master.zip

    收起阅读 »

    30秒上手的HTTP请求库

    RxHttp主要优势1. 30秒即可上手,学习成本极低2. 史上最优雅的支持 Kotlin 协程3. 史上最优雅的处理多个BaseUrl及动态BaseUrl4. 史上最优雅的对错误统一处理,且不打破Lambda表达式5. 史上最优雅的文件上传/下载/断点下载/...
    继续阅读 »

    RxHttp


    主要优势

    1. 30秒即可上手,学习成本极低

    2. 史上最优雅的支持 Kotlin 协程

    3. 史上最优雅的处理多个BaseUrl及动态BaseUrl

    4. 史上最优雅的对错误统一处理,且不打破Lambda表达式

    5. 史上最优雅的文件上传/下载/断点下载/进度监听,已适配Android 10

    6. 支持Gson、Xml、ProtoBuf、FastJson等第三方数据解析工具

    7. 支持Get、Post、Put、Delete等任意请求方式,可自定义请求方式

    8. 支持在Activity/Fragment/View/ViewModel/任意类中,自动关闭请求

    9. 支持全局加解密、添加公共参数及头部、网络缓存,均支持对某个请求单独设置

    请求三部曲

    上手教程

    30秒上手教程:30秒上手新一代Http请求神器RxHttp

    协程文档:RxHttp ,比Retrofit 更优雅的协程体验

    掘金详细文档:RxHttp 让你眼前一亮的Http请求框架

    wiki详细文档:https://github.com/liujingxing/rxhttp/wiki (此文档会持续更新)

    自动关闭请求用到的RxLife类,详情请查看RxLife库

    更新日志      遇到问题,点击这里,99%的问题都能自己解决

    上手准备

    Maven依赖点击这里

    1、RxHttp目前已适配OkHttp 3.12.0 - 4.9.1版本(4.3.0版本除外), 如你想要兼容21以下,请依赖OkHttp 3.12.x,该版本最低要求 API 9

    2、asXxx方法内部是通过RxJava实现的,而RxHttp 2.2.0版本起,内部已剔除RxJava,如需使用,请自行依赖RxJava并告知RxHttp依赖的Rxjava版本

    必须

    jitpack添加到项目的build.gradle文件中,如下:

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

    注:RxHttp 2.6.0版本起,已全面从JCenter迁移至jitpack

    //使用kapt依赖rxhttp-compiler时必须
    apply plugin: 'kotlin-kapt'

    android {
    //必须,java 8或更高
    compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
    }
    }

    dependencies {
    implementation 'com.github.liujingxing.rxhttp:rxhttp:2.6.1'
    implementation 'com.squareup.okhttp3:okhttp:4.9.1' //rxhttp v2.2.2版本起,需要手动依赖okhttp
    kapt 'com.github.liujingxing.rxhttp:rxhttp-compiler:2.6.1' //生成RxHttp类,纯Java项目,请使用annotationProcessor代替kapt
    }

    可选

    android {
    defaultConfig {
    javaCompileOptions {
    annotationProcessorOptions {
    arguments = [
    //使用asXxx方法时必须,告知RxHttp你依赖的rxjava版本,可传入rxjava2、rxjava3
    rxhttp_rxjava: 'rxjava3',
    rxhttp_package: 'rxhttp' //非必须,指定RxHttp类包名
    ]
    }
    }
    }
    }
    dependencies {
    implementation 'com.github.liujingxing.rxlife:rxlife-coroutine:2.1.0' //管理协程生命周期,页面销毁,关闭请求

    //rxjava2 (RxJava2/Rxjava3二选一,使用asXxx方法时必须)
    implementation 'io.reactivex.rxjava2:rxjava:2.2.8'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
    implementation 'com.github.liujingxing.rxlife:rxlife-rxjava2:2.1.0' //管理RxJava2生命周期,页面销毁,关闭请求

    //rxjava3
    implementation 'io.reactivex.rxjava3:rxjava:3.0.6'
    implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
    implementation 'com.github.liujingxing.rxlife:rxlife-rxjava3:2.1.0' //管理RxJava3生命周期,页面销毁,关闭请求

    //非必须,根据自己需求选择 RxHttp默认内置了GsonConverter
    implementation 'com.github.liujingxing.rxhttp:converter-fastjson:2.6.1'
    implementation 'com.github.liujingxing.rxhttp:converter-jackson:2.6.1'
    implementation 'com.github.liujingxing.rxhttp:converter-moshi:2.6.1'
    implementation 'com.github.liujingxing.rxhttp:converter-protobuf:2.6.1'
    implementation 'com.github.liujingxing.rxhttp:converter-simplexml:2.6.1'
    }

    最后,rebuild一下(此步骤是必须的) ,就会自动生成RxHttp类

    混淆

    RxHttp v2.2.8版本起,无需添加任何混淆规则(内部自带混淆规则),v2.2.8以下版本,请查看混淆规则,并添加到自己项目中

    代码下载:327744707-okhttp-RxHttp-master.zip







    收起阅读 »

    Android微信工具包,你想要的这里都有~

    wxlibrary aar文件使用说明APP 使用示例项目,libs下含有以编译最新的aar资源。wxlibrary arr资源项目,需要引入的资源包项目。aar文件生成,在工具栏直接Gradle - (项目名) - wxlibrary - Tasks - b...
    继续阅读 »

    wxlibrary aar文件使用说明

    一、项目介绍

    1. APP 使用示例项目,libs下含有以编译最新的aar资源。
    2. wxlibrary arr资源项目,需要引入的资源包项目。
    3. aar文件生成,在工具栏直接Gradle - (项目名) - wxlibrary - Tasks - build - assemble,直到编译完成
    4. aar文件位置,打开项目所在文件夹,找到 wxlibrary\build\outputs\aar 下。

    二、工程引入工具包

    下载项目,可以在APP项目的libs文件下找到*.aar文件(已编译为最新版),选择其中一个引入自己的工程

    引入微信工具包及微信SDK

    dependencies {
    //引入wxlibrary.aar资源
    implementation files('libs/wxlibrary-release.aar')
    //引入wxlibrary.aar的依赖资源,以下2个
    implementation 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:6.6.5'
    //eventbus,引入后你的项目将支持EventBus,EventBus是一种用于Android的事件发布-订阅总线,替代广播的传值方式,使用方法可以度娘查询。
    implementation 'org.greenrobot:eventbus:3.1.1'
    ...
    }

    三、工具包初始准备工作

    • 工程继承WxApplication 或者 application 的 onCreate 下使用,获取 APPkey 和AppSecret需要使用mete-data方式获取。 isCheckSignature() 与 isNowRegister() 默认即可
        WxApiUtil.getInstance().init(getApplicationContext(), true, true);
    • APPkeyAppSecret,需要使用mete-data方式进行赋值

    方式一,manifest下覆盖mete-data资源

     
    ...>



    android:name="WX_LIBRARY_WX_APP_KEY"
    android:value="123456s"
    tools:replace="android:value"/>


    android:name="WX_LIBRARY_WX_APP_SECRET"
    android:value="567890a"
    tools:replace="android:value"/>



    方式二,manifest下不覆盖mete-data资源,在gradle(app)下赋值

    android {
    ...
    defaultConfig {
    ...

    //todo 微信appKey和appSecret赋值的方法二,2个参数都需要赋值,secret不需要时赋值为空字符串即可
    manifestPlaceholders = [
    WX_LIBRARY_WX_APP_KEY: '',
    WX_LIBRARY_WX_APP_SECRET: ''
    ]
    }
    ...
    }

    四、登录、分享和支付的使用,链式写法一句搞定

    1. 登录使用

    // 注意以下注册回调事件不注册则不会触发
    WxLoginUtil.newInstance()
    .setSucceed((code) -> {
    // 登录过程回调成功 code为微信返回的code
    // 如果需要在app获取openID,则在此处使用code向微信服务器请求获取openID。
    // 使用WxApiGlobal.getInstance().getAppKey()和WxApiGlobal.getInstance().getAppSecret()获取微信的必要参数,使用前请确保已填写正确参数
    return;
    })
    .setNoInstalled((() -> {
    // 微信客户端未安装
    return;
    }))
    .setUserCancel(() -> {
    // 用户取消
    return;
    })
    .setFail((errorCode, errStr) -> {
    // 其他类型错误, errorCode为微信返回的错误码
    return;
    })
    //发起登录请求
    .logIn();
    2. 分享使用,注意由于微信分享变更,分享时只要唤起微信客户端,无论是否真正分享,都会返回成功

    // 注意以下注册回调事件不注册则不会触发
    WxShareUtil.newInstance()
    .setSucceed(() -> {
    // 分享过程回调成功
    })
    .setNoInstalled((() -> {
    // 微信客户端未安装
    }))
    .setUserCancel(() -> {
    // 用户取消,由于微信调整,用户取消状态不会触发
    })
    .setFail((errorCode, errStr) -> {
    // 其他类型错误, errorCode为微信返回的错误码
    })
    //发起分享请求
    .shareTextMessage("内容", "标题", "描述", ShareType.WXSceneTimeline);
    3. 支付使用

    // req.appId = json.getString("appid");
    // req.partnerId = json.getString("partnerid");
    // req.prepayId = json.getString("prepayid");
    // req.nonceStr = json.getString("noncestr");
    // req.timeStamp = json.getString("timestamp");
    // req.packageValue = json.getString("package");
    // req.sign = json.getString("sign");
    // 此json文本需要包含以上所需字段,或者使用实体方式,不列举
    // 注意以下注册回调事件不注册则不会触发
    WxPayUtil.newInstance()
    .setSucceed(() -> {
    // sdk支付成功,向微信服务器查询下具体结果吧
    })
    .setNoInstalled((() -> {
    // 微信客户端未安装
    }))
    .setUserCancel(() -> {
    // 用户取消
    })
    .setFail((errorCode, errStr) -> {
    // 其他类型错误, errorCode为微信返回的错误码
    })
    //发起分享请求
    .payWeChat("json文本");

    五、测试说明

    由于微信需要在后台配置签名信息,而测试时不能修改一次打包一次进行测试,所以配置项目的签名信息即可在debug模式下使用正式版签名信息。

    android {
    signingConfigs {
    release {
    storeFile file('key文件位置,可写相对位置。默认是相对于app的文件夹下')
    storePassword 'key文件密码'
    keyAlias = '打包别名'
    keyPassword '别名密码'
    }
    }
    ...
    buildTypes {
    debug {
    signingConfig signingConfigs.release
    }
    release {
    minifyEnabled false
    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    signingConfig signingConfigs.release
    }
    }

    }


    代码下载:mjsoftking-wxlibraryapp-master.zip

    收起阅读 »

    Android仿微信录制音视频的管理工具

    ecorderManager因为在项目中经常需要使用音视频录制,所以写了一个公共库RecorderManager,欢迎大家使用。最新0.4.0-beta.3版本: 1.升级依赖 2.移除EasyPermissions和废弃方法,使用新API registerF...
    继续阅读 »

    ecorderManager

    因为在项目中经常需要使用音视频录制,所以写了一个公共库RecorderManager,欢迎大家使用。

    最新0.4.0-beta.3版本: 1.升级依赖 2.移除EasyPermissions和废弃方法,使用新API registerForActivityResult,请采用Java1.8以上版本 3.重构框架,优化代码 4.库调用做部分调整,详见下方文档说明 5.欢迎大家测试反馈完善功能

    0.3.2版本:1.移除strings.xml中app_name 2.升级kotlin

    0.3.1版本更新:详情见文档 1.新增最小录制时间设置RecordVideoOption.setMinDuration(//最小录制时长(秒数,最小是1,会自动调整不大于最大录制时长)) 2.优化代码

    0.3-beta.2版本更新: 1.重构项目代码,kotlin改写部分功能 2.移除rxjava库,减少依赖 3.升级最新SDK 4.新增闪光灯功能,增加计时前提示文本设置 5.增加国际化支持,英文和中文 6.修复已知问题,优化代码 7.对外用户调用API改动较少,主要为内部调整,见下方文档,欢迎大家测试反馈完善功能

    0.2.29版本更新: 1.新增圆形进度按钮配置功能 2.新增指定前后置摄像头功能 3.优化代码,调整启动视频录制配置项

    0.2.28版本更新: 1.优化视频录制结果获取方式 2.优化代码

    0.2.27版本更新: 1.视频录制界面RecordVideoRequestOption新增RecorderOption和hideFlipCameraButton配置 2.优化代码

    0.2.26版本更新: 1.项目迁移至AndroidX, 引入Kotlin

    0.2.25版本更新: 1.优化权限自动申请,可自动调起视频录制界面 2.规范图片资源命名

    一.效果展示

    仿微信界面视频录制

    2.音频录制界面比较简单,就不放图了

    二.引用

    1.Add it in your root build.gradle at the end of repositories

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

    2.Add the dependency

    dependencies {
    implementation 'com.github.MingYueChunQiu:RecorderManager:0.3.2'
    }

    三.使用

    1.音频录制

    采用默认配置录制

    mRecorderManager.recordAudio(mFilePath);

    自定义配置参数录制

    mRecorderManager.recordAudio(new RecorderOption.Builder()
    .setAudioSource(MediaRecorder.AudioSource.MIC)
    .setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
    .setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
    .setAudioSamplingRate(44100)
    .setBitRate(96000)
    .setFilePath(path)
    .build());

    2.视频录制

    (1).可以直接使用RecordVideoActivity,实现了仿微信风格的录制界面

    从0.2.18开始改为类似

    RecorderManagerFactory.getRecordVideoRequest().startRecordVideo(MainActivity.this, 0);

    从0.4.0-beta版本开始,因为采用registerForActivityResult,所以直接传入结果回调

          RecorderManagerProvider.getRecordVideoRequester().startRecordVideo(MainActivity.this, new RMRecordVideoResultCallback() {
    @Override
    public void onResponseRecordVideoResult(@NonNull RecordVideoResultInfo info) {
    Log.e("MainActivity", "onActivityResult: " + info.getDuration() + " " + info.getFilePath());
    Toast.makeText(MainActivity.this, info.getDuration() + " " + info.getFilePath(), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onFailure(@NonNull RecorderManagerException e) {
    Log.e("MainActivity", "onActivityResult: " + e.getErrorCode() + " " + e.getMessage());
    }
    });

    从0.4.0-beta版本开始:RecorderManagerFactory重命名为RecorderManagerProvider RecorderManagerFactory中可以拿到IRecordVideoPageRequester,在IRecordVideoPageRequester接口中

    /**
    * 以默认配置打开录制视频界面
    *
    * @param activity Activity
    * @param requestCode 请求码
    */
    void startRecordVideo(@NonNull FragmentActivity activity, int requestCode);

    /**
    * 以默认配置打开录制视频界面
    *
    * @param fragment Fragment
    * @param requestCode 请求码
    */
    void startRecordVideo(@NonNull Fragment fragment, int requestCode);

    /**
    * 打开录制视频界面
    *
    * @param activity Activity
    * @param requestCode 请求码
    * @param option 视频录制请求配置信息类
    */
    void startRecordVideo(@NonNull FragmentActivity activity, int requestCode, @Nullable RecordVideoRequestOption option);

    /**
    * 打开录制视频界面
    *
    * @param fragment Fragment
    * @param requestCode 请求码
    * @param option 视频录制请求配置信息类
    */
    void startRecordVideo(@NonNull Fragment fragment, int requestCode, @Nullable RecordVideoRequestOption option);

    从0.4.0-beta版本开始:

    public interface IRecordVideoPageRequester extends IRMRequester {

    /**
    * 以默认配置打开录制视频界面
    *
    * @param activity Activity
    * @param callback 视频录制结果回调
    */
    void startRecordVideo(@NonNull FragmentActivity activity, @NonNull RMRecordVideoResultCallback callback);

    /**
    * 以默认配置打开录制视频界面
    *
    * @param fragment Fragment
    * @param callback 视频录制结果回调
    */
    void startRecordVideo(@NonNull Fragment fragment, @NonNull RMRecordVideoResultCallback callback);

    /**
    * 打开录制视频界面
    *
    * @param activity Activity
    * @param option 视频录制请求配置信息类
    * @param callback 视频录制结果回调
    */
    void startRecordVideo(@NonNull FragmentActivity activity, @Nullable RecordVideoRequestOption option, @NonNull RMRecordVideoResultCallback callback);

    /**
    * 打开录制视频界面
    *
    * @param fragment Fragment
    * @param option 视频录制请求配置信息类
    * @param callback 视频录制结果回调
    */
    void startRecordVideo(@NonNull Fragment fragment, @Nullable RecordVideoRequestOption option, @NonNull RMRecordVideoResultCallback callback);
    }

    RecordVideoRequestOption可配置最大时长(秒)和文件保存路径

    public class RecordVideoRequestOption implements Parcelable {

    private String filePath;//文件保存路径
    private int maxDuration;//最大录制时间(秒数)
    private RecordVideoOption recordVideoOption;//录制视频配置信息类(里面配置的filePath和maxDuration会覆盖外面的)
    }

    RecordVideoActivity里已经配置好了默认参数,可以直接使用,然后在onActivityResult里拿到视频路径的返回值 返回值为RecordVideoResultInfo

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == Activity.RESULT_OK && requestCode == 0) {
    RecordVideoResultInfo info = data.getParcelableExtra(EXTRA_RECORD_VIDEO_RESULT_INFO);

    //从0.2.28版本开始可以使用下面这种方式,更安全更灵活,兼容性强
    RecordVideoResultInfo info = RecorderManagerFactory.getRecordVideoResult(data);
    //从0.3版本开始
    RecordVideoResultInfo info = RecorderManagerFactory.getRecordVideoResultParser().parseRecordVideoResult(data);

    if (info != null) {
    Log.e("MainActivity", "onActivityResult: " + " "
    + info.getDuration() + " " + info.getFilePath());
    }
    }
    }

    从0.4.0-beta.1版本开始: 由于采用Android新API registerForActivityResult,所以startActivityForResult等相关方法皆已废弃,相关回调将直接通过RMRecordVideoResultCallback传递

    interface RMRecordVideoResultCallback {

    fun onResponseRecordVideoResult(info: RecordVideoResultInfo)

    fun onFailure(e: RecorderManagerException)
    }

    通过下列IRecordVideoPageRequester相关方法,调用时同时传入响应结果回调
    void startRecordVideo(@NonNull FragmentActivity activity, @NonNull RMRecordVideoResultCallback callback);

    (2).如果想要界面一些控件的样式,可以继承RecordVideoActivity,里面提供了几个protected方法,可以拿到界面的一些控件

    /**
    * 获取计时控件
    *
    * @return 返回计时AppCompatTextView
    */
    protected AppCompatTextView getTimingView() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getTimingView();
    }

    /**
    * 获取圆形进度按钮
    *
    * @return 返回进度CircleProgressButton
    */
    protected CircleProgressButton getCircleProgressButton() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getCircleProgressButton();
    }

    /**
    * 获取翻转摄像头控件
    *
    * @return 返回翻转摄像头AppCompatImageView
    */
    public AppCompatImageView getFlipCameraView() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getFlipCameraView();
    }

    /**
    * 获取播放控件
    *
    * @return 返回播放AppCompatImageView
    */
    protected AppCompatImageView getPlayView() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getPlayView();
    }

    /**
    * 获取取消控件
    *
    * @return 返回取消AppCompatImageView
    */
    protected AppCompatImageView getCancelView() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getCancelView();
    }

    /**
    * 获取确认控件
    *
    * @return 返回确认AppCompatImageView
    */
    protected AppCompatImageView getConfirmView() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getConfirmView();
    }

    /**
    * 获取返回控件
    *
    * @return 返回返回AppCompatImageView
    */
    protected AppCompatImageView getBackView() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getBackView();
    }

    想要替换图标资源的话,提供下列名称图片

    rm_record_video_flip_camera.png
    rm_record_video_cancel.png
    rm_record_video_confirm.png
    rm_record_video_play.png
    rm_record_video_pull_down.png
    rm_record_video_flashlight_turn_off.png
    rm_record_video_flashlight_turn_on.png

    (3).同时提供了对应的RecordVideoFragment,实现与RecordVideoActivity同样的功能,实际RecordVideoActivity就是包裹了一个RecordVideoFragment

    1.创建RecordVideoFragment

    /**
    * 获取录制视频Fragment实例(使用默认配置项)
    *
    * @param filePath 存储文件路径
    * @return 返回RecordVideoFragment
    */
    public static RecordVideoFragment newInstance(@Nullable String filePath) {
    return newInstance(filePath, 30);
    }

    /**
    * 获取录制视频Fragment实例(使用默认配置项)
    *
    * @param filePath 存储文件路径
    * @param maxDuration 最大时长(秒数)
    * @return 返回RecordVideoFragment
    */
    public static RecordVideoFragment newInstance(@Nullable String filePath, int maxDuration) {
    return newInstance(new RecordVideoOption.Builder()
    .setRecorderOption(new RecorderOption.Builder().buildDefaultVideoBean(filePath))
    .setMaxDuration(maxDuration)
    .build());
    }

    /**
    * 获取录制视频Fragment实例
    *
    * @param option 录制配置信息对象
    * @return 返回RecordVideoFragment
    */
    public static RecordVideoFragment newInstance(@Nullable RecordVideoOption option) {
    RecordVideoFragment fragment = new RecordVideoFragment();
    Bundle args = new Bundle();
    args.putParcelable(BUNDLE_EXTRA_RECORD_VIDEO_OPTION, option == null ? new RecordVideoOption() : option);
    fragment.setArguments(args);
    return fragment;
    }

    2.然后添加RecordVideoFragment到自己想要的地方就可以了 3.可以设置OnRecordVideoListener,拿到各个事件的回调

    public class RecordVideoOption:

    private RecorderOption recorderOption;//录制配置信息
    private RecordVideoButtonOption recordVideoButtonOption;//录制视频按钮配置信息类
    private int minDuration;//最小录制时长(秒数,最小是1,会自动调整不大于最大录制时长),可以和timingHint配合使用
    private int maxDuration;//最大录制时间(秒数)
    private RecorderManagerConstants.CameraType cameraType;//摄像头类型
    private boolean hideFlipCameraButton;//隐藏返回翻转摄像头按钮
    private boolean hideFlashlightButton;//隐藏闪光灯按钮
    private String timingHint;//录制按钮上方提示语句(默认:0:%s),会在计时前显示
    private String errorToastMsg;//录制发生错误Toast(默认:录制时间小于1秒,请重试)

    原OnRecordVideoListener现已改为RMOnRecordVideoListener,并从RecordVideoOption中移除,主要用于用户自己activity或fragment实现此接口,用于承载RecordVideoFragment,获取相关步骤回调

    interface RMOnRecordVideoListener {

    /**
    * 当完成一次录制时回调
    *
    * @param filePath 视频文件路径
    * @param videoDuration 视频时长(毫秒)
    */
    fun onCompleteRecordVideo(filePath: String?, videoDuration: Int)

    /**
    * 当点击确认录制结果按钮时回调
    *
    * @param filePath 视频文件路径
    * @param videoDuration 视频时长(毫秒)
    */
    fun onClickConfirm(filePath: String?, videoDuration: Int)

    /**
    * 当点击取消按钮时回调
    *
    * @param filePath 视频文件路径
    * @param videoDuration 视频时长(毫秒)
    */
    fun onClickCancel(filePath: String?, videoDuration: Int)

    /**
    * 当点击返回按钮时回调
    */
    fun onClickBack()
    }

    4.RecordVideoButtonOption是圆形进度按钮配置类

    	private @ColorInt
    int idleCircleColor;//空闲状态内部圆形颜色
    private @ColorInt
    int pressedCircleColor;//按下状态内部圆形颜色
    private @ColorInt
    int releasedCircleColor;//释放状态内部圆形颜色
    private @ColorInt
    int idleRingColor;//空闲状态外部圆环颜色
    private @ColorInt
    int pressedRingColor;//按下状态外部圆环颜色
    private @ColorInt
    int releasedRingColor;//释放状态外部圆环颜色
    private int idleRingWidth;//空闲状态外部圆环宽度
    private int pressedRingWidth;//按下状态外部圆环宽度
    private int releasedRingWidth;//释放状态外部圆环宽度
    private int idleInnerPadding;//空闲状态外部圆环与内部圆形之间边距
    private int pressedInnerPadding;//按下状态外部圆环与内部圆形之间边距
    private int releasedInnerPadding;//释放状态外部圆环与内部圆形之间边距
    private boolean idleRingVisible;//空闲状态下外部圆环是否可见
    private boolean pressedRingVisible;//按下状态下外部圆环是否可见
    private boolean releasedRingVisible;//释放状态下外部圆环是否可见

    5.RecorderOption是具体的录制参数配置类

    	private int audioSource;//音频源
    private int videoSource;//视频源
    private int outputFormat;//输出格式
    private int audioEncoder;//音频编码格式
    private int videoEncoder;//视频编码格式
    private int audioSamplingRate;//音频采样频率(一般44100)
    private int bitRate;//视频编码比特率
    private int frameRate;//视频帧率
    private int videoWidth, videoHeight;//视频宽高
    private int maxDuration;//最大时长
    private long maxFileSize;//文件最大大小
    private String filePath;//文件存储路径
    private int orientationHint;//视频录制角度方向

    (4).如果想自定义自己的界面,可以直接使用RecorderManagerable类

    1.通过RecorderManagerFactory获取IRecorderManager 从0.4.0-beta版本开始:RecorderManagerFactory重命名为RecorderManagerProvider

    public class RecorderManagerFactory {

    private RecorderManagerFactory() {
    }

    /**
    * 创建录制管理类实例(使用默认录制类)
    *
    * @return 返回录制管理类实例
    */
    @NonNull
    public static IRecorderManager newInstance() {
    return newInstance(new RecorderHelper());
    }

    /**
    * 创建录制管理类实例(使用默认录制类)
    *
    * @param intercept 录制管理器拦截器
    * @return 返回录制管理类实例
    */
    @NonNull
    public static IRecorderManager newInstance(@NonNull IRecorderManagerInterceptor intercept) {
    return newInstance(new RecorderHelper(), intercept);
    }

    /**
    * 创建录制管理类实例
    *
    * @param helper 实际录制类
    * @return 返回录制管理类实例
    */
    @NonNull
    public static IRecorderManager newInstance(@NonNull IRecorderHelper helper) {
    return newInstance(helper, null);
    }

    /**
    * 创建录制管理类实例
    *
    * @param helper 实际录制类
    * @param intercept 录制管理器拦截器
    * @return 返回录制管理类实例
    */
    @NonNull
    public static IRecorderManager newInstance(@NonNull IRecorderHelper helper, @Nullable IRecorderManagerInterceptor intercept) {
    return new RecorderManager(helper, intercept);
    }

    @NonNull
    public static IRecordVideoRequest getRecordVideoRequest() {
    return new RecordVideoPageRequest();
    }

    //0.3之后版本通过解析器来进行处理数据
    @NonNull
    public static IRecordVideoResultParser getRecordVideoResultParser() {
    return new RecordVideoResultParser();
    }
    }

    它们返回的都是IRecorderManager 接口类型,RecorderManager 是默认的实现类,RecorderManager 内持有一个真正进行操作的RecorderHelper。

    public interface IRecorderManager extends IRecorderHelper {

    /**
    * 设置录制对象
    *
    * @param helper 录制对象实例
    */
    void setRecorderHelper(@NonNull IRecorderHelper helper);

    /**
    * 获取录制对象
    *
    * @return 返回录制对象实例
    */
    @NonNull
    IRecorderHelper getRecorderHelper();

    /**
    * 初始化相机对象
    *
    * @param holder Surface持有者
    * @return 返回初始化好的相机对象
    */
    @Nullable
    Camera initCamera(@NonNull SurfaceHolder holder);

    /**
    * 初始化相机对象
    *
    * @param cameraType 指定的摄像头类型
    * @param holder Surface持有者
    * @return 返回初始化好的相机对象
    */
    @Nullable
    Camera initCamera(@NonNull RecorderManagerConstants.CameraType cameraType, @NonNull SurfaceHolder holder);

    /**
    * 打开或关闭闪光灯
    *
    * @param turnOn true表示打开,false关闭
    */
    boolean switchFlashlight(boolean turnOn);

    /**
    * 翻转摄像头
    *
    * @param holder Surface持有者
    * @return 返回翻转并初始化好的相机对象
    */
    @Nullable
    Camera flipCamera(@NonNull SurfaceHolder holder);

    /**
    * 翻转到指定类型摄像头
    *
    * @param cameraType 摄像头类型
    * @param holder Surface持有者
    * @return 返回翻转并初始化好的相机对象
    */
    @Nullable
    Camera flipCamera(@NonNull RecorderManagerConstants.CameraType cameraType, @NonNull SurfaceHolder holder);

    /**
    * 获取当前摄像头类型
    *
    * @return 返回摄像头类型
    */
    @NonNull
    RecorderManagerConstants.CameraType getCameraType();

    /**
    * 释放相机资源
    */
    void releaseCamera();

    }

    RecorderManagerIntercept实现IRecorderManagerInterceptor接口,用户可以直接继承RecorderManagerIntercept,它里面所有方法都是空实现,可以自己改写需要的方法

    public interface or extends ICameraInterceptor {}

    IRecorderHelper是一个接口类型,由实现IRecorderHelper的子类来进行录制操作,默认提供的是RecorderHelper,RecorderHelper实现了IRecorderHelper。

    public interface IRecorderHelper {

    /**
    * 录制音频
    *
    * @param path 文件存储路径
    * @return 返回是否成功开启录制,成功返回true,否则返回false
    */
    boolean recordAudio(@NonNull String path);

    /**
    * 录制音频
    *
    * @param option 存储录制信息的对象
    * @return 返回是否成功开启录制,成功返回true,否则返回false
    */
    boolean recordAudio(@NonNull RecorderOption option);

    /**
    * 录制视频
    *
    * @param camera 相机
    * @param surface 表面视图
    * @param path 文件存储路径
    * @return 返回是否成功开启录制,成功返回true,否则返回false
    */
    boolean recordVideo(@Nullable Camera camera, @Nullable Surface surface, @Nullable String path);

    /**
    * 录制视频
    *
    * @param camera 相机
    * @param surface 表面视图
    * @param option 存储录制信息的对象
    * @return 返回是否成功开启视频录制,成功返回true,否则返回false
    */
    boolean recordVideo(@Nullable Camera camera, @Nullable Surface surface, @Nullable RecorderOption option);

    /**
    * 释放资源
    */
    void release();

    /**
    * 获取录制器
    *
    * @return 返回实例对象
    */
    @NonNull
    MediaRecorder getMediaRecorder();

    /**
    * 获取配置信息对象
    *
    * @return 返回实例对象
    */
    @Nullable
    RecorderOption getRecorderOption();
    }

    2.拿到后创建相机对象

    		if (mCamera == null) {
    mCamera = mManager.initCamera(mCameraType, svVideoRef.get().getHolder());
    mCameraType = mManager.getCameraType();
    }

    3.录制

    isRecording = mManager.recordVideo(mCamera, svVideoRef.get().getHolder().getSurface(), mOption.getRecorderOption());

    4.释放

    	    mManager.release();
    mManager = null;
    mCamera = null;

    代码下载:MingYueChunQiu-RecorderManager-master.zip
    收起阅读 »

    kotlin编写的 Android 开源播放器, 开箱即用

    介绍功能特性1、通过 dependence 引入MXVideo2、页面集成3、开始播放MXPlaySource 可选参数说明:4、监听播放进度5、全屏返回 + 释放资源功能相关

    MXVideo

    介绍

    基于饺子播放器、kotlin编写的 Android 开源播放器, 开箱即用,欢迎提 issue 和 pull request 最新版本:

    功能特性

    • 任意播放器内核(包含开源IJK、谷歌Exo、阿里云等等)
    • 单例播放,只能同时播放一个节目
    • 0代码集成全屏功能
    • 可以调节音量、屏幕亮度
    • 可以注册播放状态监听回调
    • 播放器高度可以根据视频高度自动调节
    • 播放器支持设置宽高比,设置宽高比后,高度固定。
    • 自动保存与恢复播放进度(可关闭)
    • 支持循环播放、全屏时竖屏模式、可关闭快进快退功能、可关闭全屏功能、可关闭非WiFi环境下流量提醒

    1、通过 dependence 引入MXVideo

        dependencies {
    implementation 'com.gitee.zhangmengxiong:MXVideo:x.x.x'
    }

    2、页面集成

            
    android:id="@+id/mxVideoStd"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

    3、开始播放
    // 设置播放占位图
    Glide.with(this).load("http://www.xxx.com/xxx.png").into(mxVideoStd.getPosterImageView())

    // 默认从上一次进度播放
    mxVideoStd.setSource(MXPlaySource(Uri.parse("https://aaa.bbb.com/xxx.mp4"), "标题1"))
    mxVideoStd.startPlay()

    // 从头开始播放
    mxVideoStd.setSource(MXPlaySource(Uri.parse("https://aaa.bbb.com/xxx.mp4"), "标题1"), seekTo = 0)
    mxVideoStd.startPlay()

    // 从第10秒开始播放
    mxVideoStd.setSource(MXPlaySource(Uri.parse("https://aaa.bbb.com/xxx.mp4"), "标题1"), seekTo = 10)
    mxVideoStd.startPlay()


    MXPlaySource 可选参数说明:

    参数说明默认值
    title标题""
    headerMap网络请求头部null
    changeOrientationWhenFullScreen全屏时是否需要变更Activity方向,如果 = null,会自动根据视频宽高来判断null
    isLooping是否循环播放false
    enableSaveProgress是否存储、读取播放进度true
    isLiveSource是否直播源,当时直播时,不显示进度,无法快进快退暂停false

    4、监听播放进度

    mxVideoStd.addOnVideoListener(object : MXVideoListener() {
    // 播放状态变更
    override fun onStateChange(state: MXState) {
    }

    // 播放时间变更
    override fun onPlayTicket(position: Int, duration: Int) {
    }
    })

    5、全屏返回 + 释放资源

    这里MXVideo默认持有当前播放的MXVideoStd,可以使用静态方法操作退出全屏、释放资源等功能。

    也可以直接使用viewId:mxVideoStd.isFullScreen(),mxVideoStd.isFullScreen(),mxVideoStd.release() 等方法。

        override fun onBackPressed() {
    if (MXVideo.isFullScreen()) {
    MXVideo.gotoNormalScreen()
    return
    }
    super.onBackPressed()
    }

    override fun onDestroy() {
    MXVideo.releaseAll()
    super.onDestroy()
    }

    功能相关

    • 切换播放器内核
    // 默认MediaPlayer播放器,库默认内置
    com.mx.video.player.MXSystemPlayer

    // 谷歌的Exo播放器
    com.mx.mxvideo_demo.player.MXExoPlayer

    // IJK播放器
    com.mx.mxvideo_demo.player.MXIJKPlayer

    // 设置播放源是可以设置内核,默认 = MXSystemPlayer
    mxVideoStd.setSource(MXPlaySource(Uri.parse("xxx"), "xxx"), player = MXSystemPlayer::class.java)
    • 视频渲染旋转角度
    // 默认旋转角度 = MXOrientation.DEGREE_0
    mxVideoStd.setOrientation(MXOrientation.DEGREE_90)
    • 视频填充规则
    // 强制填充宽高 MXScale.FILL_PARENT
    // 根据视频大小,自适应宽高 MXScale.CENTER_CROP

    // 默认填充规则 = MXScale.CENTER_CROP
    mxVideoStd.setScaleType(MXScale.CENTER_CROP)
    • MXVideoStd 控件宽高约束

    在页面xml中添加,layout_width一般设置match_parent,高度wrap_content

        
    android:id="@+id/mxVideoStd"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

    可以设置任意宽高比,如果设置宽高比,则控件高度需要设置android:layout_height="wrap_content",否则不生效。

    当取消约束、MXVideo高度自适应、填充规则=MXScale.CENTER_CROP时,控件高度会自动根据视频宽高自动填充高度

    // MXVideoStd控件设置宽高比= 16:9
    mxVideoStd.setDimensionRatio(16.0 / 9.0)

    // MXVideoStd控件设置宽高比= 4:3
    mxVideoStd.setDimensionRatio(4.0 / 3.0)

    // 取消约束
    mxVideoStd.setDimensionRatio(0.0)
    • 进度跳转
    // 进度单位:秒  可以在启动播放后、错误或播完之前调用
    mxVideoStd.seekTo(55)
    • 设置不能快进快退
    // 播放前设置 默认=true
    mxVideoStd.getConfig().canSeekByUser = false
    • 设置不能全屏
    // 播放前设置 默认=true
    mxVideoStd.getConfig().canFullScreen = false
    • 设置不显示控件右上角时间
    // 播放前设置 默认=true
    mxVideoStd.getConfig().canShowSystemTime = false
    • 设置不显示控件右上角电量图
    // 播放前设置 默认=true
    mxVideoStd.getConfig().canShowBatteryImg = false
    • 设置关闭WiFi环境播放前提醒
    // 播放前设置 默认=true
    mxVideoStd.getConfig().showTipIfNotWifi = false
    • 设置播放完成后自动退出全屏
    // 播放前设置 默认=true
    mxVideoStd.getConfig().gotoNormalScreenWhenComplete = true
    • 设置播放错误后自动退出全屏
    // 播放前设置 默认=true
    mxVideoStd.getConfig().gotoNormalScreenWhenError = true
    • 设置屏幕方向根据重力感应自动进入全屏、小屏模式
    // 播放前设置 默认=false
    mxVideoStd.getConfig().autoRotateBySensor = true


    代码下载:zhangmengxiong-MXVideo-master.zip

    一行代码完成http请求,bitmap异步加载,数据库增删查改!

    ##WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.##Welike带来了哪些特征?WelikeAndroid...
    继续阅读 »

    ##WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,
    使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.

    ##Welike带来了哪些特征?

    WelikeAndroid目前包含五个大模块:

    • 异常安全隔离模块(实验阶段):当任何线程抛出任何异常,我们的异常隔离机制都会让UI线程继续运行下去.
    • Http模块: 一行代码完成POST、GET请求和Download,支持上传, 高度优化Disk的缓存加载机制,
      自由设置缓存大小、缓存时间(也支持永久缓存和不缓存).
    • Bitmap模块: 一行代码完成异步显示图片,无需考虑OOM问题,支持加载前对图片做自定义处理.
    • Database模块: 支持NotNull,Table,ID,Ignore等注解,Bean无需Getter和Setter,一键式部署数据库.
    • ui操纵模块: 我们为Activity基类做了完善的封装,继承基类可以让代码更加优雅.
    • :请不要认为功能相似,框架就不是原创,源码摆在眼前,何不看一看?

    使用WelikeAndroid需要以下权限:

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.INTERNET" />

    ##下文将教你如何圆润的使用WelikeAndroid:
    ###通过WelikeContext在任意处取得上下文:

    • WelikeContext.getApplication(); 就可以取得当前App的上下文
    • WelikeToast.toast("你好!"); 简单一步弹出Toast.

    ##WelikeGuard(异常安全隔离机制用法):

    • 第一步,开启异常隔离机制:
    WelikeGuard.enableGuard();
    • 第二步,注册一个全局异常监听器:

    WelikeGuard.registerUnCaughtHandler(new Thread.UncaughtExceptionHandler() {
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {

    WelikeGuard.newThreadToast("出现异常了: " + ex.getMessage() );

    }
    });
    • 你也可以自定义异常:

    /**
    *
    * 自定义的异常,当异常被抛出后,会自动回调onCatchThrowable函数.
    */
    @Catch(process = "onCatchThrowable")
    public class CustomException extends IllegalAccessError {

    public static void onCatchThrowable(Thread t){
    WeLog.e(t.getName() + " 抛出了一个异常...");
    }
    }
    • 另外,继承自UncaughtThrowable的异常我们不会对其进行拦截.

    使用Welike做屏幕适配:

    Welike的ViewPorter类提供了屏幕适配的Fluent-API,我们可以通过一组流畅的API轻松做好屏幕适配.

            ViewPorter.from(button).ofScreen().divWidth(2).commit();//宽度变为屏幕的二分之一
    ViewPorter.from(button).of(viewGroup).divHeight(2).commit();//高度变为viewGroup的二分之一
    ViewPorter.from(button).div(2).commit();//宽度和高度变为屏幕的四分之一
    ViewPorter.from(button).of(this).fillWidth().fillHeight().commit();//宽度和高度铺满Activity
    ViewPorter.from(button).sameAs(imageView).commit();//button的宽度和高度和imageView一样

    WelikeHttp入门:

    首先来看看框架的调试信息,是不是一目了然. DEBUG DEBUG2

    • 第一步,取得WelikeHttp默认实例.
    WelikeHttp welikeHttp = WelikeHttp.getDefault();
    • 第二步,发送一个Get请求.
    HttpParams params = new HttpParams();
    params.putParams("app","qr.get",
    "data","Test");//一次性放入两对 参数 和 值

    //发送Get请求
    HttpRequest request = welikeHttp.get("http://api.k780.com:88", params, new HttpResultCallback() {
    @Override
    public void onSuccess(String content) {
    super.onSuccess(content);
    WelikeToast.toast("返回的JSON为:" + content);
    }

    @Override
    public void onFailure(HttpResponse response) {
    super.onFailure(response);
    WelikeToast.toast("JSON请求发送失败.");
    }

    @Override
    public void onCancel(HttpRequest request) {
    super.onCancel(request);
    WelikeToast.toast("请求被取消.");
    }
    });

    //取消请求,会回调onCancel()
    request.cancel();

    当然,我们为满足需求提供了多种扩展的Callback,目前我们提供以下Callback供您选择:

    • HttpCallback(响应为byte[]数组)
    • FileUploadCallback(仅在上传文件时使用)
    • HttpBitmapCallback(建议使用Bitmap模块)
    • HttpResultCallback(响应为String)
    • DownloadCallback(仅在download时使用)

    如需自定义Http模块的配置(如缓存时间),请查看HttpConfig.

    WelikeBitmap入门:

    • 第一步,取得默认的WelikeBitmap实例:

    //取得默认的WelikeBitmap实例
    WelikeBitmap welikeBitmap = WelikeBitmap.getDefault();
    • 第二步,异步加载一张图片:
    BitmapRequest request = welikeBitmap.loadBitmap(imageView,
    "http://img0.imgtn.bdimg.com/it/u=937075122,1381619862&fm=21&gp=0.jpg",
    android.R.drawable.btn_star,//加载中显示的图片
    android.R.drawable.ic_delete,//加载失败时显示的图片
    new BitmapCallback() {

    @Override
    public Bitmap onProcessBitmap(byte[] data) {
    //如果需要在加载时处理图片,可以在这里处理,
    //如果不需要处理,就返回null或者不复写这个方法.
    return null;
    }

    @Override
    public void onPreStart(String url) {
    super.onPreStart(url);
    //加载前回调
    WeLog.d("===========> onPreStart()");
    }

    @Override
    public void onCancel(String url) {
    super.onCancel(url);
    //请求取消时回调
    WeLog.d("===========> onCancel()");
    }

    @Override
    public void onLoadSuccess(String url, Bitmap bitmap) {
    super.onLoadSuccess(url, bitmap);
    //图片加载成功后回调
    WeLog.d("===========> onLoadSuccess()");
    }

    @Override
    public void onRequestHttp(HttpRequest request) {
    super.onRequestHttp(request);
    //图片需要请求http时回调
    WeLog.d("===========> onRequestHttp()");
    }

    @Override
    public void onLoadFailed(HttpResponse response, String url) {
    super.onLoadFailed(response, url);
    //请求失败时回调
    WeLog.d("===========> onLoadFailed()");
    }
    });
    • 如果需要自定义Config,请看BitmapConfig这个类.

    ##WelikeDAO入门:

    • 首先写一个Bean.

    /*表名,可有可无,默认为类名.*/
    @Table(name="USER",afterTableCreate="afterTableCreate")
    public class User{
    @ID
    public int id;//id可有可无,根据自己是否需要来加.

    /*这个注解表示name字段不能为null*/
    @NotNull
    public String name;

    public static void afterTableCreate(WelikeDao dao){
    //在当前的表被创建时回调,可以在这里做一些表的初始化工作
    }
    }
    • 然后将它写入到数据库
    WelikeDao db = WelikeDao.instance("Welike.db");
    User user = new User();
    user.name = "Lody";
    db.save(user);
    • 从数据库取出Bean

    User savedUser = db.findBeanByID(1);
    • SQL复杂条件查询
    List<User> users = db.findBeans().where("name = Lody").or("id = 1").find();
    • 更新指定ID的Bean
    User wantoUpdateUser = new User();
    wantoUpdateUser.name = "NiHao";
    db.updateDbByID(1,wantoUpdateUser);
    • 删除指ID定的Bean
    db.deleteBeanByID(1);
    • 更多实例请看DEMO和API文档.

    ##十秒钟学会WelikeActivity

    • 我们将Activity的生命周期划分如下:

    =>@initData(所有标有InitData注解的方法都最早在子线程被调用)
    =>initRootView(bundle)
    =>@JoinView(将标有此注解的View自动findViewByID和setOnClickListener)
    =>onDataLoaded(数据加载完成时回调)
    =>点击事件会回调onWidgetClick(View Widget)

    ###关于@JoinView的细节:

    • 有以下三种写法:
    @JoinView(name = "welike_btn")
    Button welikeBtn;
    @JoinView(id = R.id.welike_btn)
    Button welikeBtn;
    @JoinView(name = "welike_btn",click = false)
    Button welikeBtn;
    • clicktrue时会自动调用view的setOnClickListener方法,并在onWidgetClick回调.
    • 当需要绑定的是一个Button的时候, click属性默认为true,其它的View则默认为false.
    收起阅读 »

    一个简洁而优雅的Android原生UI框架,解放你的双手!

    一个简洁而又优雅的Android原生UI框架,解放你的双手!还不赶紧点击使用说明文档,体验一下吧!涵盖绝大部分的UI组件:TextView、Button、EditText、ImageView、Spinner、Picker、Dialog、PopupWindow、...
    继续阅读 »

    一个简洁而又优雅的Android原生UI框架,解放你的双手!还不赶紧点击使用说明文档,体验一下吧!

    涵盖绝大部分的UI组件:TextView、Button、EditText、ImageView、Spinner、Picker、Dialog、PopupWindow、ProgressBar、LoadingView、StateLayout、FlowLayout、Switch、Actionbar、TabBar、Banner、GuideView、BadgeView、MarqueeView、WebView、SearchView等一系列的组件和丰富多彩的样式主题。

    在提issue前,请先阅读【提问的智慧】,并严格按照issue模板进行填写,节约大家的时间。

    在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!

    在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!

    在使用前,请一定要仔细阅读使用说明文档,重要的事情说三遍!!!

    X系列库快速集成

    为了方便大家快速集成X系列框架库,我提供了一个空壳模版供大家参考使用: https://github.com/xuexiangjys/TemplateAppProject

    除此之外,我还特别制作了几期视频教程供大家学习参考.


    特征

    • 简洁优雅,尽可能少得引用资源文件的数量,项目库整体大小不足1M(打包后大约644k)。

    • 组件丰富,提供了绝大多数我们在开发者常用的功能组件。

    • 使用简单,为方便快速开发,提高开发效率,对api进行了优化,提供一键式接入。

    • 样式统一,框架提供了一系列统一的样式,使UI整体看上去美观和谐。

    • 兼容性高,框架还提供了3种不同尺寸设备的样式(4.5英寸、7英寸和10英寸),并且最低兼容到Android 17, 让UI兼容性更强。

    • 扩展性强,各组件提供了丰富的属性和样式API,可以通过设置不同的样式属性,构建不同风格的UI。


    如何使用

    在决定使用XUI前,你必须明确的一点是,此框架给出的是一整套UI的整体解决方案,如果你只是想使用其中的几个控件,那大可不必引入如此庞大的一个UI库,Github上会有更好的组件库。如果你是想拥有一套可以定制的、统一的UI整体解决方案的话,那么你就继续往下看吧!

    添加Gradle依赖

    1.先在项目根目录的 build.gradle 的 repositories 添加:

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

    2.然后在dependencies添加:

    dependencies {
    ...
    //androidx项目
    implementation 'com.github.xuexiangjys:XUI:1.1.7'

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    }

    【注意】如果你的项目目前还未使用androidx,请使用如下配置:

    dependencies {
    ...
    //support项目
    implementation 'com.github.xuexiangjys:XUI:1.0.9-support'

    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support:recyclerview-v7:28.0.0'
    implementation 'com.android.support:design:28.0.0'
    implementation 'com.github.bumptech.glide:glide:4.8.0'
    }

    初始化XUI设置

    1.调整应用的基础主题(必须)

    必须设置应用的基础主题,否则组件将无法正常使用!必须保证所有用到XUI组件的窗口的主题都为XUITheme的子类,这非常重要!!!

    基础主题类型:

    • 大平板(10英寸, 240dpi, 1920*1200):XUITheme.Tablet.Big

    • 小平板(7英寸, 320dpi, 1920*1200):XUITheme.Tablet.Small

    • 手机(4.5英寸, 320dpi, 720*1280):XUITheme.Phone

    <style name="AppTheme" parent="XUITheme.Phone">
    
 <!-- 自定义自己的主题样式 -->

    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
    
</style>

    当然也可以在Activity刚开始时调用如下代码动态设置主题

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    XUI.initTheme(this);
    super.onCreate(savedInstanceState);
    ...
    }

    2.调整字体库(对字体无要求的可省略)

    (1)设置你需要修改的字体库路径(assets下)

    //设置默认字体为华文行楷,这里写你的字体库
    XUI.getInstance().initFontStyle("fonts/hwxk.ttf");

    (2)在项目的基础Activity中加入如下代码注入字体.

    注意:1.1.4版本之后使用如下设置注入

    @Override
    protected void attachBaseContext(Context newBase) {
    //注入字体
    super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase));
    }

    注意:1.1.3版本及之前的版本使用如下设置注入

    @Override
    protected void attachBaseContext(Context newBase) {
    //注入字体
    super.attachBaseContext(CalligraphyContextWrapper.wrap(newBase));
    }

    混淆配置

    -keep class com.xuexiang.xui.widget.edittext.materialedittext.** { *; }


    代码下载:XUI.zip

    收起阅读 »

    Android支付组件

    接入指南:1、导入libSdk 依赖工程2、配置 AndroidManifest文件(配置内容,请看下文,此处支持 两种方式来配置 第三方支付 参数【①可以在AndroidManifest 对应的meta-data 配置;②支持在代码中配置;选其一即可】)2....
    继续阅读 »



    接入指南:

    1、导入libSdk 依赖工程

    2、配置 AndroidManifest文件(配置内容,请看下文,此处支持 两种方式来配置 第三方支付 参数【①可以在AndroidManifest 对应的meta-data 配置;②支持在代码中配置;选其一即可】)

    • 2.1 拷贝assets/data.bin 文件到 项目中

    3、项目中实际使用支付:具体使用看下文 ---> 调起支付 。


    请配置正确的参数,否则支付宝和微信 会出现无法调起的情况。

    //配置 AndroidManifest

        <activity
    android:name="net.lbh.pay.PaymentActivity"
    android:launchMode="singleTop"
    android:theme="@android:style/Theme.Translucent.NoTitleBar" />

    <activity-alias
    android:name=".wxapi.WXPayEntryActivity"
    android:exported="true"
    android:targetActivity="net.lbh.pay.PaymentActivity" />
    <!-- 微信支付 end -->


    <!-- 支付宝 begin -->
    <activity
    android:name="com.alipay.sdk.app.H5PayActivity"
    android:configChanges="orientation|keyboardHidden|navigation"
    android:exported="false"
    android:screenOrientation="behind"
    android:windowSoftInputMode="adjustResize|stateHidden" />
    <!-- 支付宝 end -->


    <!-- 银联支付 begin -->

    <activity
    android:name="com.unionpay.uppay.PayActivity"
    android:configChanges="orientation|keyboardHidden"
    android:excludeFromRecents="true"
    android:screenOrientation="portrait"
    android:windowSoftInputMode="adjustResize" />

    <activity
    android:name="com.unionpay.UPPayWapActivity"
    android:configChanges="orientation|keyboardHidden"
    android:screenOrientation="portrait"
    android:windowSoftInputMode="adjustResize" />

    <!-- 银联支付 end -->


    <!-- 微信 广播 start -->
    <receiver android:name="net.lbh.pay.wxpay.AppRegister" >
    <intent-filter>
    <action android:name="com.tencent.mm.plugin.openapi.Intent.ACTION_REFRESH_WXAPP" />
    </intent-filter>
    </receiver>
    <!-- 微信 广播 end -->


    <!-- 微信支付 参数 appid, 需要替换成你自己的 -->
    <meta-data
    android:name="WXPAY_APP_ID"
    android:value="替换成自己的 app id" >
    </meta-data>
    <meta-data
    android:name="WXPAY_MCH_ID"
    android:value="替换成自己的 MCH_ID" >
    </meta-data>
    <meta-data
    android:name="WXPAY_API_KEY"
    android:value="替换成自己的 api key" >
    </meta-data>
    <!-- 微信支付 参数 end 需要替换成你自己的 -->


    <!-- 支付宝 参数 appid, 需要替换成你自己的 --> //如果是 超过10位数字,要在前边加 ,Eg: \0223987667567887653
    <meta-data
    android:name="ALIPAY_PARTNER_ID"
    android:value="替换成自己的 partenr id" >
    </meta-data>
    <meta-data
    android:name="ALIPAY_SELLER_ID"
    android:value="替换成自己的 seller id" >
    </meta-data>

    <meta-data
    android:name="ALIPAY_PRIVATE_KEY"
    android:value="替换成自己的 private key" >
    </meta-data>

    <meta-data
    android:name="ALIPAY_PUBLIC_KEY"
    android:value="替换成自己的 public key" >
    </meta-data>
    <!-- 支付宝 参数 end 需要替换成你自己的 -->

    // 初始化支付组件

    	PayAgent payAgent = PayAgent.getInstance();
    payAgent.setDebug(true);

    // 代码初始化 参数, 此处针对场景,所有参数有 自己app server保管的时候,动态的支付配置下发参数
    payAgent.initAliPayKeys(partnerId, sellerId, privateKey, publicKey);
    payAgent.initWxPayKeys(appId, mchId, appKey)
    // 初始化 银联支付 所需的 验签 参数
    //payAgent.initUpPayKeys(PublicKeyPMModulus, publicExponent, PublicKeyProductModulus);
    // 代码动态初始化为 可选

    payAgent.initPay(this);

    // 调起支付

        PayAgent.getInstance().onPay(payType, this, payInfo,
    new OnPayListener() {

    @Override
    public void onStartPay() {

    progressDialog.setTitle("加载中。。。");
    progressDialog.show();
    }

    @Override
    public void onPaySuccess() {

    Toast.makeText(MainActivity.this,"支付成功!", 1).show();

    if (null != progressDialog) {
    progressDialog.dismiss();
    }

    }

    @Override
    public void onPayFail(String code, String msg) {
    Toast.makeText(MainActivity.this,
    "code:" + code + "msg:" + msg, 1).show();
    Log.e(getClass().getName(), "code:" + code + "msg:" + msg);

    if (null != progressDialog) {
    progressDialog.dismiss();
    }
    }
    });

    支付参数说明:

    PayType: 支付的支付方式,目前支持:

    • 1、PayAgent.PayType.WECHATPAY(微信支付);
    • 2、PayAgent.PayType.ALIPAY(支付宝);
    • 3、PayAgent.PayType.UPPAY(银联)。

    Activity: 调起支付的 Activity

    PayInfo:

    /** 商品名称*/
    private String subject;

    /** 商品详细信息 商品的标题/交易标题/订单标题/订单关键字等。该参数最长为128个汉字*/
    private String body;

    /** 商品价格*/
    private String price;

    /** 商品订单号*/
    private String orderNo;

    /** 支付通知地址*/
    private String notifyUrl;

    OnPayListener: 支付监听器:

    • onStartPay() 开始支付,可以在此做 支付前准备提示
    • onPaySuccess(); 支付成功
    • onPayFail(String code, String msg); 支付失败,code和msg 均为第三方原样返回

    配置第三方参数说明:

    • 1、支付宝:

    注意:

    • 1、支付宝支付,调用支付宝时, 所有参数为必须向
    • 2、微信支付,orderNo 为必须项
    • 3、银联支付时,orderNo 为必须项 -4、关于支付后,通知回调,只有支付宝是 在客户端手动设置,其余都是在 后台配置。

    注意事项:

    • 1、当测试时,可以使用Debug模式,开启方式为: PayAgent payAgent = PayAgent.getInstance(); payAgent.setDebug(true);

    • 2、调试模式(非正式环境,目前只有 银联): PayAgent payAgent = PayAgent.getInstance(); payAgent.setOnlieMode(false);

    版本說明:

    • 1、银联支付:3.3.2
    • 2、支付宝:
    • 3、微信:

    更新日志:

    • 2016.04.15更新:

    • 1、2016.4.14 银联更新sdk,更新银联支付控件为3.3.3

    • 2、去除银联客户端验签;添加银联需要权限(nfc等)

    • 1、更新银联支付控件为3.3.2

    • 2、添加调试模式(非正式环境模式、主要正对银联支付)

    payAgent.setOnlieMode(false);

    • 3、添加银联 验证签名,初始化签名参数
    • 4、修改Demo ,测试 Demo能正常运行。

    其他说明:



    收起阅读 »