Compare commits

..

8 Commits

3889 changed files with 84427 additions and 255183 deletions

15
.gitignore vendored
View File

@ -1,11 +1,8 @@
.idea/
/.idea
.idea/misc.xml
*.iml
.gradle/
local.properties
# sign.properties
.gradle
/local.properties
.DS_Store
captures/
build/
release-app/
scripts/apk-channel/
app/src/test/java/com/gh/gamecenter
/build
/captures

4
.gitmodules vendored
View File

@ -1,4 +0,0 @@
[submodule "libraries/LGLibrary"]
path = libraries/LGLibrary
url = git@gitlab.ghzs.com:android/common-library.git
branch = master

1
.idea/.name generated Normal file
View File

@ -0,0 +1 @@
GH-ASSISTv1.45

22
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>

3
.idea/copyright/profiles_settings.xml generated Normal file
View File

@ -0,0 +1,3 @@
<component name="CopyrightManager">
<settings default="" />
</component>

6
.idea/encodings.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

24
.idea/gradle.xml generated Normal file
View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleHome" value="$APPLICATION_HOME_DIR$/gradle/gradle-2.10" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="myModules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

46
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<entry_points version="2.0" />
</component>
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectLevelVcsManager" settingsEditedManually="false">
<OptionsSetting value="true" id="Add" />
<OptionsSetting value="true" id="Remove" />
<OptionsSetting value="true" id="Checkout" />
<OptionsSetting value="true" id="Update" />
<OptionsSetting value="true" id="Status" />
<OptionsSetting value="true" id="Edit" />
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.7" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

9
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/GH-ASSISTv1.45.iml" filepath="$PROJECT_DIR$/GH-ASSISTv1.45.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
</modules>
</component>
</project>

12
.idea/runConfigurations.xml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -1,89 +0,0 @@
# 版本升级备忘
### Ver 2.5
* 此处写本次更新所做的业务和代码修改
### Ver 2.6
* xx
### Ver 3.0
* 升级账号系统(登录流程/用户信息相关/用户账号相关操作(评论,礼包...))
* 新增收藏功能(文章/工具箱)
* 删除用户相关的所有本地数据库
* 重做总开服表
* 重做首页插件化模块
* 礼包重复领取机制改变(可重复领取的礼包,领取后立刻显示再领一个/再淘一个)
* 游戏下载平台面板修改(加快弹出速度,不再读取本地平台图片)
* 接入bugly(tinker)
### VER 3.1
### VER 3.2
### VER 3.3
### VER 3.4
### VER 3.5
### Ver 3.6
* 首页游戏增加预览骨架,游戏ITEM样式微调和开服标签
* 首页问答增加关注页面
* 重构游戏更新管理(游戏更新/插件化/已安装的游戏),具体细节参考PackageRepository & PackageViewModel
* 重构APP更新管理(已VersionVode为更新基准,小版本更新改为光环后台控制)
- 删除TINKER_VERISON_NAME
- tinker打包方式变更(以小版本作为Base包,防止与数据后台小版本更新发生冲突)
* 社区增加版主功能(版主可以对存在的相关内容进行修改/隐藏操作,内容包括问题/回答/回答评论)
* 社区互动引导优化(问答推荐增加`推荐关注`,回答详情增加一些交互动效)
### Ver 3.6.1
* 可以后台控制关闭资讯功能
* 版块、分类、专题详情、游戏详情、礼包详情增加预览骨架
* 下载按钮状态可以通过接口屏蔽相应的包
### Ver 3.6.2
* 资讯/问答入口和插件功能线上控制(不可逆)
* 首页不显示已安装的游戏
* 插件求版本功能增加内部跳转
* 下载面板增加公告和版本说明功能
* 接入腾讯`广点通`(广告)
### ver 3.6.3
* 社区搜索修改
- 增加 `文章/用户` 模块
- 增加 `搜索置顶` 功能
* 回答详情/社区文章详情修改
- 支持文案样式(加粗/斜体/删除线)和段落样式(引用/标题)
- 支持关闭评论功能
- 回答详情新增上下切换回答
* 社区编辑框(回答/文章)修改
- 支持批量插入图片(使用知乎Matisse实现)
- 新增插入特殊样式,文案样式(加粗/斜体/删除线)和段落样式(引用/标题)
* 编辑框部分 JS/CSS 使用远程文件
### ver 3.6.4
* 增加浏览记录(回答/文章/资讯/游戏)
* 回答/社区文章 增加反对功能
* 社区编辑框增加插入文章/回答/游戏
- 低版本兼容方案: 插入的样式默认隐藏,只有在3.6.4及以上才会显示
* 游戏详情评分模块增加`小编评论`区域以及样式修改
* 游戏评分增加回复功能
### var 3.6.5
* 以补丁方式向外推出并没有增加需求只是单纯的修BUG
### var 3.6.6
* 游戏详情:
- 支持修改评分
- 评分列表增加排序/过滤功能
- 增加弹出系统
* 社区相关:
- 选择社区页面重做
- 首页社区推荐增加推荐入口
- 首页社区问题模块改版,名称改为全部,去除问题分类(统一为问题列表),增加社区文章列表
* 游戏搜索默认页面改版
* 权限系统更改,不授权也可以进去App,申请权限细分到功能(用到某个功能时才需要强制授予权限)
* 增加隐私系统
* 增加游戏预约功能
* 增加标签详情模块
* 进入今日头条广告SDK
* 图片上传压缩机制优化
- 支持从后台修改上传配置(本该在早些版本实现,由于代码问题无法引用后台配置)
- 对压缩失败是进行catch(由于后台对宽高配置放宽,很容易发生OOM),失败后直接上传原图(这步可能会出现问题)
* 游戏相关UI修改

View File

@ -1,69 +0,0 @@
# 光环助手Android客户端
### APK打包配置
* 使用[ApkChannelPackage](https://github.com/ltlovezh/ApkChannelPackage)的方案
* 打包命令,视情况使用:
> 打包Tinker基准包`./scripts/tinker_release_base.sh`
> 以Tinker基准包打渠道包`./scripts/tinker_release_channel.sh`
> 以Tinker基准包打补丁包`./scripts/tinker_release_patch.sh`
### 混淆配置
* 配置文件Android默认配置+proguard-rules.txt等
* 参考libraries下每个项目独立的配置文件`proguard-project.txt`
### apk大小优化
* 限制resConfig资源集
* 开启ShrinkResources
* 开启混淆使用minifyEnabled(仅在release开启
* pngquant对png压缩、png/jpg->webp(未尝试)
### 第三方appkey等配置
* 修改`gradle.properties`文件将各种key填入其中实现统一管理
* 通过gradle文件内的resValue/buildConfigField/manifestPlaceHolder方式实现编译期间修改具体情况请参考``./build.gradle``和``./app/build.gradle``配置
### 拉取代码步骤
1. 拉取主项目代码: `git clone -b dev git@gitlab.ghzhushou.com:halo/assistant-android.git`
2. 初始化公用类库: `bash ./scripts/submodules_init.sh`
### submodule管理方式(只拉取master)
* 提交代码需要cd到submodule文件夹去做修改
* 更新远端代码,`bash ./scripts/submodules_update.sh`
### TODO
* GSON 序列化用统一的一个, GsonUtil fromJson
* CleanApkAdapter 转化字符串size工具函数 比如SpeedUtils
* getString 解决 字符串hardcode问题
* ~~Adapter 里面clicklistener 用接口传参将点击操作委托给controller~~
* ~~Adapter ViewHolder的功能部分重写到ViewHolder类本身~~
* ~~activity 统一入口未完成(外部入口相关)去除多余activity使用统一toolbar~~
* ~~release / debug compile不同的类库不需要再做什么开关~~
* ~~Toolbar分离有图形按钮/没有图形按钮~~
### TODO Since 3.1
- 解决 Utils 工具类引发的内存泄漏问题
- 把原有 EventBus 的消息 Type 统一到一个文件内
- 将实现细节从 View(Fragment、Activity) 剥离并以 MVVM 结构改造
- ~~将 ListViewModel 所对应的 ListRepository 合并到 ListViewModel 中~~
- 依照光环助手界面功能以大模块 - 小模块的方式去修改包结构,包内文件建议以包名摘要作为前缀
- ~~使用 RxJava 的 Debounce 和 Map 操作优化搜索触发机制 参考资料:[1](https://proandroiddev.com/building-an-autocompleting-edittext-using-rxjava-f69c5c3f5a40),[2](https://medium.com/@kurtisnusbaum/rxandroid-basics-part-2-6e877af352)~~
- ~~把 ListViewModel 的数据结构类型转换方式换为抽象方法,让继承的类实现,避免出现无响应的问题~~
- ~~rxjava2 如果接口返回为空 会发生异常:java.lang.NullPointerException: Null is not a valid element (答案编辑) 解决方法->com.gh.gamecenter.retrofit.Response~~
- constraintLayout 1.1.2 导致布局出现异常(问题编辑标签选择弹窗)
- 搞清楚 GameManager 的用途,看能不能去掉
- 重构一下 MainActivity

View File

@ -1,367 +1,127 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' // kotlin
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
// apkChannelPackage
apply plugin: 'channel'
//apply from: 'tinker-support.gradle'
android {
androidExtensions {
experimental = true
}
dataBinding {
enabled = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
dexOptions {
// jumboMode = true
javaMaxHeapSize "4g"
}
compileSdkVersion 21
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "com.gh.gamecenter"
minSdkVersion 14
targetSdkVersion 21
versionCode 17
versionName "2.2"
multiDexEnabled true
javaCompileOptions {
annotationProcessorOptions {
arguments = [eventBusIndex: 'com.gh.EventBusIndex']
}
}
ndk {
abiFilters "armeabi-v7a"
}
renderscriptTargetApi 18
renderscriptSupportModeEnabled true
// 由于app只针对中文用户所以仅保留zh资源其他删掉
resConfigs "zh"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
applicationId rootProject.ext.applicationId
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt', 'proguard-fresco.txt'
/**
* All third-party appid/appkey
*/
buildConfigField "String", "WECHAT_APPID", "\"${WECHAT_APPID}\""
buildConfigField "String", "WECHAT_SECRET", "\"${WECHAT_SECRET}\""
buildConfigField "String", "TENCENT_APPID", "\"${TENCENT_APPID}\""
buildConfigField "String", "WEIBO_APPKEY", "\"${WEIBO_APPKEY}\""
buildConfigField "String", "MTA_APPKEY", "\"${MTA_APPKEY}\""
buildConfigField "String", "TD_APPID", "\"${TD_APPID}\""
buildConfigField "String", "LETO_APPID", "\"${LETO_APPID}\""
buildConfigField "String", "TTAD_APPID", "\"${TTAD_APPID}\""
buildConfigField "String", "DOUYIN_CLIENTKEY", "\"${DOUYIN_CLIENTKEY}\""
buildConfigField "String", "DOUYIN_CLIENTSECRET", "\"${DOUYIN_CLIENTSECRET}\""
buildConfigField "String", "MIPUSH_APPID", "\"${MIPUSH_APPID}\""
buildConfigField "String", "MIPUSH_APPKEY", "\"${MIPUSH_APPKEY}\""
buildConfigField "String", "MEIZUPUSH_APPID", "\"${MEIZUPUSH_APPID}\""
buildConfigField "String", "MEIZUPUSH_APPKEY", "\"${MEIZUPUSH_APPKEY}\""
resValue "string", "huawei_push_appid", "appid=${HUAWEI_PUSH_APPID}"
/**
* Build Time 供区分 jenkins 打包时间用
*/
buildConfigField "long", "BUILD_TIME", "0"
// 默认的渠道
// manifestPlaceholders = [CHANNEL_VALUE: "GH_TEST"]
}
// gradle 2.2以上默认同时启用v1和v2优先用于Android N
/**
* 签名设置
*/
signingConfigs {
debug {
v1SigningEnabled true
v2SigningEnabled true
}
release {
v1SigningEnabled true
v2SigningEnabled true
storeFile file("gh.keystore")
keyAlias "gh.keystore"
keyPassword "20150318"
storePassword "20150318"
}
}
buildTypes {
debug {
debuggable true
minifyEnabled false
zipAlignEnabled false
signingConfig signingConfigs.debug
buildConfigField "String", "EXPOSURE_REPO", "\"test\""
buildConfigField "String", "EXPOSURE_VERSION", "\"E4\""
multiDexKeepProguard file("tinker_multidexkeep.pro")
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
debuggable false
minifyEnabled true
zipAlignEnabled true
shrinkResources true
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
buildConfigField "String", "EXPOSURE_REPO", "\"exposure\""
buildConfigField "String", "EXPOSURE_VERSION", "\"E4\""
multiDexKeepProguard file("tinker_multidexkeep.pro")
}
}
flavorDimensions ("env")
/**
* 多渠道打包
*/
productFlavors {
publish {
dimension "env"
buildConfigField "String", "API_HOST", "\"${API_HOST}\""
buildConfigField "String", "SENSITIVE_API_HOST", "\"${SENSITIVE_API_HOST}\""
buildConfigField "String", "UMENG_APPKEY", "\"${UMENG_APPKEY}\""
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${UMENG_MESSAGE_SECRET}\""
buildConfigField "String", "BUGLY_APPID", "\"${BUGLY_APPID}\""
}
internal {
dimension "env"
versionNameSuffix "-debug"
buildConfigField "String", "API_HOST", "\"${DEV_API_HOST}\""
buildConfigField "String", "SENSITIVE_API_HOST", "\"${DEV_API_HOST}\""
buildConfigField "String", "UMENG_APPKEY", "\"${DEV_UMENG_APPKEY}\""
buildConfigField "String", "UMENG_MESSAGE_SECRET", "\"${DEV_UMENG_MESSAGE_SECRET}\""
buildConfigField "String", "BUGLY_APPID", "\"${DEV_BUGLY_APPID}\""
}
GH_100 {}
GH_101 {}
GH_102 {}
GH_103 {}
GH_104 {}
GH_106 {}
GH_107 {}
GH_108 {}
GH_109 {}
GH_110 {}
GH_111 {}
GH_113 {}
GH_114 {}
GH_115 {}
GH_116 {}
GH_117 {}
GH_118 {}
GH_119 {}
GH_120 {}
GH_121 {}
GH_123 {}
GH_127 {}
GH_200 {}
GH_201 {}
GH_202 {}
GH_203 {}
GH_204 {}
GH_205 {}
GH_222 {}
GH_307 {}
GH_TEST {}
}
productFlavors.all { flavor ->
flavor.manifestPlaceholders = [CHANNEL_VALUE: name]//命令 gradlew assembleRelease
}
}
// apkChannelPackage
channel {
//多渠道包的输出目录默认为new File(project.buildDir,"channel")
baseOutputDir = new File(project.buildDir, "channel")
//多渠道包的命名规则,默认为:${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}
apkNameFormat = '${appName}-${versionName}-${versionCode}-${flavorName}-${buildType}'
}
rebuildChannel {
// baseDebugApk = 已有Debug APK
// baseReleaseApk = 已有Release APK
// //默认为new File(project.buildDir, "rebuildChannel/debug")
// debugOutputDir = Debug渠道包输出目录
// //默认为new File(project.buildDir, "rebuildChannel/release")
// releaseOutputDir = Release渠道包输出目录
}
repositories {
flatDir {
dirs 'libs', 'libs/aars'
//butterknife
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.neenbedankt.android-apt'
dependencies {
implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs')
testImplementation 'junit:junit:4.12'
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakcanary}"
debugImplementation "com.facebook.stetho:stetho:${stetho}"
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stetho}"
debugImplementation "com.squareup.okhttp3:logging-interceptor:${okHttp}"
debugImplementation "com.gu.android:toolargetool:${toolargetool}"
implementation "androidx.core:core:${core}"
implementation "androidx.fragment:fragment:${fragment}"
implementation "androidx.multidex:multidex:${multiDex}"
implementation "androidx.appcompat:appcompat:${appCompat}"
implementation "androidx.cardview:cardview:${cardView}"
implementation "androidx.annotation:annotation:${annotation}"
implementation "androidx.constraintlayout:constraintlayout:${constraintLayout}"
implementation "androidx.recyclerview:recyclerview:${recyclerView}"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifeCycle"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifeCycle"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifeCycle"
implementation "androidx.room:room-runtime:${room}"
implementation "androidx.room:room-rxjava2:${room}"
implementation "androidx.core:core-ktx:${ktx}"
implementation "androidx.viewpager2:viewpager2:${viewpager2}"
kapt "androidx.room:room-compiler:${room}"
implementation "com.google.android.material:material:${material}"
implementation "com.kyleduo.switchbutton:library:${switchButton}"
implementation "com.facebook.fresco:fresco:${fresco}"
implementation "com.facebook.fresco:animated-gif:${fresco}"
implementation "com.facebook.fresco:animated-drawable:${fresco}"
implementation "com.squareup.okhttp3:okhttp:${okHttp}"
implementation "com.leon.channel:helper:${apkChannelPackage}"
implementation "com.squareup.retrofit2:retrofit:${retrofit}"
implementation "com.squareup.retrofit2:converter-gson:${retrofit}" // include gson 2.7
implementation "com.squareup.retrofit2:adapter-rxjava2:${retrofit}"
implementation "com.j256.ormlite:ormlite-android:${ormlite}"
implementation "com.j256.ormlite:ormlite-core:${ormlite}"
implementation "com.jakewharton:butterknife:${butterKnife}"
kapt "com.jakewharton:butterknife-compiler:${butterKnife}"
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
implementation "org.greenrobot:eventbus:${eventbus}"
kapt "org.greenrobot:eventbus-annotation-processor:${eventbusApt}"
implementation "io.reactivex.rxjava2:rxjava:${rxJava2}"
implementation "io.reactivex.rxjava2:rxandroid:${rxAndroid2}"
implementation "com.jakewharton.rxbinding2:rxbinding:${rxBinding2}"
implementation "com.google.zxing:core:${zxing}"
implementation "com.google.zxing:android-core:${zxing}"
implementation "com.daimajia.swipelayout:library:${swipeLayout}"
implementation "com.sina.weibo.sdk:core:${weiboSDK}"
// bugly with tinker support
// implementation "com.tencent.bugly:crashreport_upgrade:${buglyTinkerSupport}"
implementation "com.google.android:flexbox:${flexbox}"
implementation "pub.devrel:easypermissions:${easypermissions}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.contrarywind:Android-PickerView:${pickerView}"
implementation "com.scwang.smartrefresh:SmartRefreshLayout:${smartRefreshLayout}"
implementation "net.cachapa.expandablelayout:expandablelayout:${expandableLayout}"
// 用于比较 versionName 是大于小于或等于
implementation "com.g00fy2:versioncompare:${versioncompare}"
implementation "top.zibin:Luban:${luban}"
implementation "com.squareup.picasso:picasso:${picasso}"
// for video streaming
implementation("com.shuyu:gsyVideoPlayer-java:$gsyVideo", {
exclude module: "gsyvideoplayer-androidvideocache"
exclude group: "tv.danmaku.ijk.media"
})
implementation "com.shuyu:GSYVideoPlayer-exo2:$gsyVideo"
implementation "android.arch.work:work-runtime:${workManager}"
implementation "com.llew.huawei:verifier:${verifier}"
implementation "com.github.tbruyelle:rxpermissions:${rxPermissions}"
implementation "com.ethanhua:skeleton:${skeleton}"
implementation "io.supercharge:shimmerlayout:${shimmerlayout}"
implementation "com.tencent.mm.opensdk:wechat-sdk-android-without-mta:${mta}"
implementation "com.walkud.rom.checker:RomChecker:${romChecker}"
debugImplementation "com.github.nichbar.chucker:library:$chucker"
releaseImplementation "com.github.nichbar.chucker:library-no-op:$chucker"
// implementation "com.bytedance.applog:RangersAppLog-Lite-cn:$bytedanceApplog"
// implementation "com.bytedance.ies.ugc.aweme:opensdk-china-external:$bytedanceAweme"
// implementation "com.bytedance.ies.ugc.aweme:opensdk-common:$bytedanceAweme"
implementation "com.aliyun.dpa:oss-android-sdk:${oss}"
implementation "com.airbnb.android:lottie:$lottie"
implementation "net.lingala.zip4j:zip4j:${zip4j}"
implementation("com.github.piasy:BigImageViewer:$bigImageViewer", {
exclude group: 'com.squareup.okhttp3'
exclude group: 'androidx.swiperefreshlayout'
exclude group: 'com.github.bumptech.glide'
})
implementation "com.github.PhilJay:MPAndroidChart:${chart}"
implementation project(':libraries:LGLibrary')
// implementation project(':libraries:MTA')
implementation project(':libraries:QQShare')
// implementation project(':libraries:TalkingData')
// implementation project(':libraries:UmengPush')
// implementation project(':libraries:WechatShare')
// implementation project(':libraries:im')
implementation project(':libraries:Matisse')
}
File propFile = file('sign.properties')
if (propFile.exists()) {
Properties props = new Properties()
props.load(new FileInputStream(propFile))
if (props.containsKey('keyAlias') && props.containsKey('keyPassword') &&
props.containsKey('storeFile') && props.containsKey('storePassword')) {
android.signingConfigs {
// debug 不要使用正式签名这样tinker才不会打补丁。
debug {
keyAlias props.get('keyAlias')
keyPassword props.get('keyPassword')
storeFile file(props.get('storeFile'))
storePassword props.get('storePassword')
}
release {
keyAlias props.get('keyAlias')
keyPassword props.get('keyPassword')
storeFile file(props.get('storeFile'))
storePassword props.get('storePassword')
}
}
} else {
android.buildTypes.release.signingConfig = null
}
} else {
android.buildTypes.release.signingConfig = null
}
// 用于测试读取 META-INF 里的文件
//task generateMetaJson {
// def resDir = new File(buildDir, 'generated/FILES_FOR_META_INF/')
// def destDir = new File(resDir, 'META-INF/')
// // Add resDir as a resource directory so that it is automatically included in the APK.
// android {
// sourceSets {
// main.resources {
// srcDir resDir
// }
// }
// }
//
// doLast {
// if (!destDir.exists()) destDir.mkdirs()
// copy {
// into destDir
// from new File('generated/FILES_FOR_META_INF/META-INF/halo_skip.json')
// }
// }
//}
//// Specify when put_files_in_META_INF should run
//project.afterEvaluate {
// tasks.findAll { task ->
// task.name.startsWith('merge') && task.name.endsWith('Resources')
// }.each { t -> t.dependsOn generateMetaJson }
//}
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:21.0.0'
compile 'com.android.support:cardview-v7:21.0.0'
// fresco图片框架
compile 'com.facebook.fresco:fresco:0.12.0'
compile 'com.facebook.fresco:animated-gif:0.12.0'
// Retrofit2所需要的包
compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
// okhttp
compile 'com.squareup.okhttp3:okhttp:3.2.0'
// ConverterFactory的Gson依赖包
compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
// ConverterFactory的String依赖包
compile 'com.squareup.retrofit2:converter-scalars:2.0.0-beta4'
// ConverterFactory的RxJava依赖包
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta4'
// gson
compile 'com.google.code.gson:gson:2.8.0'
// OrmLite数据库
compile 'com.j256.ormlite:ormlite-android:5.0'
compile 'com.j256.ormlite:ormlite-core:5.0'
// butterknife
compile 'com.jakewharton:butterknife:8.4.0'
apt 'com.jakewharton:butterknife-compiler:8.4.0'
// RxJava && RxAndroid
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.0'
// RxBinding
compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
// compile 'com.jakewharton.rxbinding:rxbinding-appcompat-v7:0.3.0'
// compile 'com.jakewharton.rxbinding:rxbinding-design:0.3.0'
}

BIN
app/libs/EventBus-2.4.0.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
app/libs/libammsdk.jar Normal file

Binary file not shown.

BIN
app/libs/mid-sdk-2.3.jar Normal file

Binary file not shown.

Binary file not shown.

BIN
app/libs/open_sdk_r5756.jar Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,21 +0,0 @@
# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.common.internal.DoNotStrip *;
}
# Keep native methods
-keepclassmembers class * {
native <methods>;
}
-dontwarn okio.**
-dontwarn com.squareup.okhttp.**
-dontwarn okhttp3.**
-dontwarn javax.annotation.**
-dontwarn com.android.volley.toolbox.**
-dontwarn com.facebook.infer.**

17
app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Android\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View File

@ -1,261 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Android\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
#--------- remove logs start ----------------
-assumenosideeffects class com.lightgame.config.CommonDebug {
private static String getLogTag(...);
private static String getMethodName();
public static void logMethodName(...);
public static void logParams(...);
public static void logFields(...);
public static void logMethodWithParams(...);
}
#-assumenosideeffects class com.lightgame.config.CommonDebug {*;}
#-dontoptimize
#--------- remove logs end ----------------
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod
-dontwarn InnerClasses
# OrmLite uses reflection
-keep class com.j256.**
-keepclassmembers class com.j256.** { *; }
-keep enum com.j256.**
-keepclassmembers enum com.j256.** { *; }
-keep interface com.j256.**
-keepclassmembers interface com.j256.** { *; }
-dontwarn com.j256.**
#okhttp3
-dontwarn com.squareup.okhttp3.**
-dontwarn okio.**
-keep class com.squareup.okhttp3.** { *;}
# stetho
-keep class com.facebook.stetho.** { *; }
-dontwarn com.facebook.stetho.**
# Retrofit 2.2
# Platform calls Class.forName on types which do not exist on Android to determine platform.
-dontnote retrofit2.Platform
# Platform used when running on Java 8 VMs. Will not be used at runtime.
-dontwarn retrofit2.Platform$Java8
# Retain generic type information for use by reflection by converters and adapters.
-keepattributes Signature
# Retain declared checked exceptions for use by a Proxy instance.
-keepattributes Exceptions
# Retrofit 2.X
## https://square.github.io/retrofit/ ##
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# rxjava
-keep class rx.schedulers.Schedulers {
public static <methods>;
}
-keep class rx.schedulers.ImmediateScheduler {
public <methods>;
}
-keep class rx.schedulers.TestScheduler {
public <methods>;
}
-keep class rx.schedulers.Schedulers {
public static ** test();
}
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
long producerIndex;
long consumerIndex;
}
-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
long producerNode;
long consumerNode;
}
-dontwarn rx.internal.util.**
## AutoScrollViewPager
-keep class cn.trinea.android.** { *; }
-keepclassmembers class cn.trinea.android.** { *; }
-dontwarn cn.trinea.android.**
## butterknife
# Retain generated class which implement Unbinder.
#-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
#
## Prevent obfuscation of types which use ButterKnife annotations since the simple name
## is used to reflectively look up the generated ViewBinding.
#-keep class butterknife.*
#-keepclasseswithmembernames class * { @butterknife.* <methods>; }
#-keepclasseswithmembernames class * { @butterknife.* <fields>; }
-dontwarn butterknife.internal.**
-keep class **$$ViewInjector { *; }
-keepnames class * { @butterknife.InjectView *;}
-dontwarn butterknife.Views$InjectViewProcessor
-dontwarn com.gc.materialdesign.views.**
# eventbus
-keepattributes *Annotation*
-keepclassmembers class ** {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
<init>(java.lang.Throwable);
}
# weiboSdk
-keep class com.sina.weibo.sdk.** { *; }
-dontwarn android.webkit.WebView
-dontwarn android.webkit.WebViewClient
# app models
-keep class com.gh.common.view.** {*;}
-keep class com.gh.gamecenter.db.info.** {*;}
-keep class com.gh.gamecenter.entity.** {*;}
-keep class com.gh.gamecenter.qa.entity.** {*;}
-keep class com.gh.gamecenter.retrofit.** {*;}
-keep class com.gh.gamecenter.eventbus.** {*;}
-keep class * extends rx.Subscriber
#---------------------------------webview------------------------------------
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String);
}
#----------------------------------------------------------------------------
##---------------Begin: proguard configuration for Gson ----------
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Gson specific classes
-keep class sun.misc.Unsafe { *; }
#-keep class com.google.gson.stream.** { *; }
# Prevent proguard from stripping interface information from TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
-keepclassmembers enum * { *; }
##---------------End: proguard configuration for Gson ----------
# ------ bugly ---------
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
# easypermission
-keepclassmembers class * {
@pub.devrel.easypermissions.AfterPermissionGranted <methods>;
}
# 重命名文件为SourceFile再配合mapping符号表可以拿到真实的类名
-renamesourcefileattribute SourceFile
# 保留源文件行号
-keepattributes SourceFile,LineNumberTable
-ignorewarnings
-keep @androidx.annotation.Keep class *
-keepclassmembers class ** {
@androidx.annotation.Keep *;
}
-keep class com.gh.loghub.** { *; }
### greenDAO 3
-keepclassmembers class * extends org.greenrobot.greendao.AbstractDao {
public static java.lang.String TABLENAME;
}
-keep class **$Properties
-keep class org.greenrobot.greendao.** { *; }
# If you do not use SQLCipher:
-dontwarn org.greenrobot.greendao.database.**
# If you do not use RxJava:
-dontwarn rx.**
-dontwarn org.greenrobot.greendao.rx.**
-dontwarn org.greenrobot.greendao.**
### fastJson
-dontwarn com.alibaba.fastjson.**
-keep class com.alibaba.fastjson.** { *; }
-keepattributes Signature
-keepattributes Annotation
### 广点通
-dontwarn com.qq.gdt.action.**
-keep class com.qq.gdt.action.** {*;}
### AndroidX
-keep class androidx.core.app.CoreComponentFactory { *; }
#阿里云上传
-keep class com.alibaba.sdk.android.oss.** { *; }
-dontwarn okio.**
-dontwarn org.apache.commons.codec.binary.**
#视频相关
-keep class com.shuyu.gsyvideoplayer.video.** { *; }
-dontwarn com.shuyu.gsyvideoplayer.video.**
-keep class com.shuyu.gsyvideoplayer.video.base.** { *; }
-dontwarn com.shuyu.gsyvideoplayer.video.base.**
-keep class com.shuyu.gsyvideoplayer.utils.** { *; }
-dontwarn com.shuyu.gsyvideoplayer.utils.**
-keep class tv.danmaku.ijk.** { *; }
-dontwarn tv.danmaku.ijk.**
#穿山甲
-keep class com.bytedance.sdk.openadsdk.** { *; }
-keep public interface com.bytedance.sdk.openadsdk.downloadnew.** {*;}
-keep class com.pgl.sys.ces.* {*;}
-keep class com.gyf.immersionbar.* {*;}
-dontwarn com.gyf.immersionbar.**
-keep class com.taobao.securityjni.**{*;}
-keep class com.taobao.wireless.security.**{*;}
-keep class com.ut.secbody.**{*;}
-keep class com.taobao.dp.**{*;}
-keep class com.alibaba.wireless.security.**{*;}
-keep class com.alibaba.sdk.android.**{*;}
-keep class com.ut.**{*;}
-keep class com.ta.**{*;}

View File

@ -1,4 +0,0 @@
storeFile=gh.keystore
storePassword=20150318
keyAlias=gh.keystore
keyPassword=20150318

View File

@ -0,0 +1,13 @@
package com.gh.gamecenter;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}

View File

@ -1,38 +0,0 @@
package com.gh.gamecenter;
import android.app.Application;
import com.facebook.stetho.Stetho;
import com.facebook.stetho.okhttp3.StethoInterceptor;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
/**
* @author CsHeng
* @Date 03/09/2017
* @Time 4:34 PM
*/
public class Injection {
public static boolean appInit(Application application) {
// init stetho
Stetho.initializeWithDefaults(application);
// 监控Bundle大小,预防溢出(需要调试的时候再开启吧!)
// TooLargeTool.startLogging(application);
return true;
}
public static OkHttpClient.Builder provideRetrofitBuilder() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
builder.addNetworkInterceptor(interceptor);
builder.addNetworkInterceptor(new StethoInterceptor());
return builder;
}
}

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.gh.gamecenter">
package="com.gh.gamecenter" >
<!-- 允许应用程序访问网络连接 -->
<uses-permission android:name="android.permission.INTERNET" />
@ -21,42 +20,28 @@
<uses-permission android:name="android.permission.GET_TASKS" />
<!-- 允许访问振动设备 -->
<uses-permission android:name="android.permission.VIBRATE" />
<!-- 允许应用程序通过WiFi或移动基站获取粗略的位置信息 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 允许应用程序通过GPS获取精确的位置信息 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- 允许应用程序改变Wi-Fi连接状态 -->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!-- 允许应用程序管理AccountManager中的账户列表 -->
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<!-- 允许应用程序访问GMail账户列表 -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<!-- 允许应用程序连接配对过的蓝牙设备 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<!-- 允许应用程序管理蓝牙,搜索和配对新的蓝牙设备 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- 允许应用程序打开系统窗口,显示其他应用程序 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 修改系统设置的权限 -->
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<!-- 小米推送需要的权限 -->
<uses-permission android:name="com.gh.gamecenter.permission.MIPUSH_RECEIVE" />
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<!-- bugly with tinker -->
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<!--可选,穿山甲提供“获取地理位置权限”和“不给予地理位置权限,开发者传入地理位置参数”两种方式上报用户位置,两种方式均可不选,添加位置权限或参数将帮助投放定位广告-->
<!--请注意:无论通过何种方式提供给穿山甲用户地理位置,均需向用户声明地理位置权限将应用于穿山甲广告投放,穿山甲不强制获取地理位置信息-->
<!--<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />-->
<!-- 如果有视频相关的广告且使用textureView播放请务必添加否则黑屏 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-sdk tools:overrideLibrary="com.shuyu.gsyvideoplayer,
com.shuyu.gsyvideoplayer.lib,
com.shuyu.gsyvideoplayer.armv7a,
com.shuyu.gsyvideoplayer.x86,
com.shuyu.gsy.base,
com.google.android.exoplayer2,
tv.danmaku.ijk.media.exo2,
pl.droidsonroids.gif,
com.donkingliang.consecutivescroller" />
<!-- 去掉 SDK 一些流氓权限 -->
<uses-permission
android:name="android.permission.READ_CONTACTS"
tools:node="remove" />
<permission
android:name="com.gh.gamecenter.permission.MIPUSH_RECEIVE"
android:protectionLevel="signature" />
<supports-screens
android:anyDensity="true"
@ -65,663 +50,260 @@
android:resizeable="true"
android:smallScreens="true" />
<!--android:largeHeap = "true"-->
<application
android:name="com.halo.assistant.HaloApp"
android:allowBackup="true"
android:icon="@mipmap/logo"
android:name="com.gh.base.AppController"
android:icon="@drawable/logo"
android:label="@string/app_name"
android:largeHeap="true"
android:resizeableActivity="true"
android:theme="@style/AppCompatTheme.APP"
tools:replace="android:allowBackup"
tools:targetApi="n">
android:theme="@style/AppThemeNormal" >
<!-- TalkingData -->
<meta-data
android:name="TD_APP_ID"
android:value="81DB144D555386A38A70B833537EC256" />
<meta-data
android:name="TD_CHANNEL_ID"
android:value="${CHANNEL_VALUE}"/>
<!--android:value="${CHANNEL_VALUE}"-->
<!-- MTA -->
<meta-data
android:name="TA_APPKEY"
android:value="APV567FTBS7J"/>
<meta-data
android:name="InstallChannel"
android:value="${CHANNEL_VALUE}"/>
<!--android:value="${CHANNEL_VALUE}"-->
<!--android:launchMode = "singleTask"-->
<activity
android:name="com.gh.gamecenter.SplashScreenActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.Launcher">
android:theme="@style/AppTheme_Guide"
android:uiOptions="splitActionBarWhenNarrow" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.gh.gamecenter.MainActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:launchMode="singleTask"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/AppCompatTheme.APP"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
android:windowSoftInputMode="stateAlwaysHidden|adjustResize"/>
<activity
android:name="com.gh.gamecenter.DownloadManagerActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait" />
<!--android:theme = "@android:style/Theme.Black.NoTitleBar.Fullscreen" 退出时屏幕抖动 -->
<activity
android:name="com.gh.gamecenter.ImageViewerActivity"
android:theme="@style/Theme.Transparent" />
android:name="com.gh.gamecenter.ViewImageActivity"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
<activity
android:name="com.gh.gamecenter.SearchActivity"
android:configChanges="keyboardHidden"
android:screenOrientation="portrait"
android:theme="@style/AppCompatTheme.APP" />
android:windowSoftInputMode="stateVisible" />
<activity
android:name="com.gh.gamecenter.amway.search.AmwaySearchActivity"
android:configChanges="keyboardHidden"
android:screenOrientation="portrait"
android:theme="@style/AppCompatTheme.APP" />
<activity
android:name="com.gh.gamecenter.ShellActivity"
android:name="com.gh.gamecenter.GameDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.gamedetail.history.HistoryApkListActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.NewsDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.SettingActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.SuggestionActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateVisible" />
<activity
android:name="com.gh.gamecenter.ConcernActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.subject.SubjectActivity"
android:name="com.gh.gamecenter.SubjectActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.PluginActivity"
android:screenOrientation="portrait"/>
<activity
android:name="com.gh.gamecenter.NewsSearchActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
android:screenOrientation="portrait"/>
<activity
android:name="com.gh.gamecenter.GameNewsActivity"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"/>
<activity
android:name="com.gh.gamecenter.CropImageActivity"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"/>
<activity
android:name="com.gh.gamecenter.WebActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.FullScreenWebActivity"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"/>
<activity
android:name="com.gh.gamecenter.ShareCardPicActivity"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"/>
<activity
android:name="com.gh.gamecenter.ShareCardActivity"
android:screenOrientation="portrait" />
android:screenOrientation="portrait"/>
<activity
android:name="com.gh.gamecenter.MessageDetailActivity"
android:screenOrientation="portrait"/>
<activity android:name="com.gh.gamecenter.SkipActivity"
android:theme="@android:style/Theme.Translucent">
<intent-filter>
<data android:scheme="ghzhushou"/>
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
</intent-filter>
</activity>
<activity
android:name="com.mob.tools.MobUIShell"
android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="portrait"
android:theme="@style/TransparentStatusBarAndNavigationBar" />
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:windowSoftInputMode="stateHidden|adjustResize" >
<intent-filter>
<data android:scheme="tencent100371282" />
<activity
android:name="com.gh.gamecenter.LibaoActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="com.gh.gamecenter.LibaoDetailActivity"
android:screenOrientation="portrait" />
android:name="com.gh.gamecenter.wxapi.WXEntryActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="true"
android:launchMode="singleTop"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="sdksample"/>
</intent-filter>
</activity>
<!-- QQ 分享 -->
<activity
android:name="com.gh.gamecenter.ShareGhActivity"
android:screenOrientation="portrait" />
android:name="com.tencent.tauth.AuthActivity"
android:launchMode="singleTask"
android:noHistory="true" >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tencent1104659243" />
</intent-filter>
</activity>
<activity
android:name="com.gh.gamecenter.CleanApkActivity"
android:screenOrientation="portrait" />
android:name="com.tencent.connect.common.AssistActivity"
android:configChanges="orientation|keyboardHidden"
android:screenOrientation="behind"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<!--微博分享-->
<activity
android:name="com.gh.gamecenter.SelectUserIconActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.AboutActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.security.SecurityActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.security.BindPhoneActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.CommentDetailActivity"
android:screenOrientation="portrait"
android:theme="@style/TransparentStatusBarAndNavigationBar" />
<activity
android:name="com.gh.gamecenter.mygame.MyGameActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.GameDetailActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.SuggestionActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.VoteActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
<activity
android:name="com.gh.gamecenter.ToolBoxActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
android:name="com.sina.weibo.sdk.component.WeiboSdkBrowser"
android:configChanges="keyboardHidden|orientation"
android:windowSoftInputMode="adjustResize"
android:exported="false" >
</activity>
<activity
android:name="com.gh.gamecenter.WeiBoShareActivity"
android:configChanges="keyboardHidden|orientation"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".category.CategoryDirectoryActivity"
android:screenOrientation="portrait" />
<activity
android:name=".category.CategoryListActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.LoginActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name="com.gh.gamecenter.UserInfoActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.UserRegionActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.CollectionActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.MessageActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.UserInfoEditActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name="com.gh.gamecenter.qa.search.AskSearchActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.answer.detail.AnswerDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.questions.detail.QuestionsDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.answer.fold.AnswerFoldActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.answer.edit.AnswerEditActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.ConcernInfoActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.InfoActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.MessageKeFuActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.select.CommunitiesSelectActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.subject.CommunitySubjectActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.MessageInviteActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.MessageVoteActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.questions.invite.QuestionsInviteActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.myqa.MyAskActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.column.order.AskTabOrderActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.questions.edit.QuestionEditActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.servers.add.AddKaiFuActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name="com.gh.gamecenter.servers.patch.PatchKaifuActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.BlockActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.amway.AmwayActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.column.detail.AskColumnDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.NetworkDiagnosisActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.personalhome.fans.FansActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.personalhome.followers.FollowersActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.personalhome.UserHomeActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.article.edit.ArticleEditActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.article.MyArticleActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.article.draft.ArticleDraftActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.article.detail.ArticleDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.article.detail.comment.ArticleDetailCommentActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.draft.CommunityDraftWrapperActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.gamedetail.rating.edit.RatingEditActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateVisible" />
<activity
android:name="com.gh.gamecenter.qa.questions.edit.manager.HistoryDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.questions.edit.manager.HistoryActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.editor.InsertAnswerWrapperActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.editor.GameActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.editor.InsertArticleWrapperActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.gamedetail.rating.RatingReplyActivity"
android:screenOrientation="portrait"
android:theme="@style/TransparentStatusBarAndNavigationBar" />
<activity
android:name="com.gh.gamecenter.history.HistoryActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.personalhome.rating.RatingActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.gamedetail.rating.logs.CommentLogsActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.tag.TagsActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.article.SimpleArticleListActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.video.videomanager.VideoManagerActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.video.upload.view.UploadVideoActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden"/>
<activity
android:name="com.gh.gamecenter.video.game.GameVideoActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.editor.VideoActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.mygame.PlayedGameActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.servers.GameServersActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.game.columncollection.detail.ColumnCollectionDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.game.upload.GameSubmissionActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.halo.assistant.fragment.user.UserPortraitCropImageActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.HelpAndFeedbackActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden" />
<activity
android:name="com.gh.gamecenter.help.HelpDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.qa.comment.CommentActivity"
android:screenOrientation="portrait"
android:theme="@style/Theme.Transparent"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name="com.gh.gamecenter.video.detail.VideoDetailActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:screenOrientation="portrait"
android:theme="@style/TransparentStatusBarAndNavigationBar" />
<activity
android:name=".gamedetail.myrating.MyRatingActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.gamedetail.fuli.kaifu.ServersCalendarActivity"
android:screenOrientation="portrait" />
<activity
android:name="com.gh.gamecenter.QaActivity"
android:screenOrientation="portrait" />
<activity
android:name=".qa.answer.draft.AnswerDraftActivity"
android:screenOrientation="portrait" />
<activity
android:name=".gamedetail.rating.RatingFoldActivity"
android:screenOrientation="portrait" />
<activity
android:name=".video.data.VideoDataActivity"
android:screenOrientation="portrait" />
<activity
android:name=".video.poster.PosterEditActivity"
android:screenOrientation="portrait" />
<activity
android:name=".video.poster.PosterClipActivity"
android:screenOrientation="portrait" />
<activity
android:name=".forum.select.ForumSelectActivity"
android:screenOrientation="portrait" />
<activity
android:name=".forum.follow.ForumMyFollowActivity"
android:screenOrientation="portrait" />
<activity
android:name=".forum.detail.ForumDetailActivity"
android:screenOrientation="portrait" />
<activity
android:name=".forum.moderator.ModeratorListActivity"
android:screenOrientation="portrait" />
<activity
android:name=".video.label.VideoLabelActivity"
android:screenOrientation="portrait" />
<activity
android:name=".personalhome.border.AvatarBorderActivity"
android:screenOrientation="portrait" />
<activity
android:name=".personalhome.background.PersonalityBackgroundActivity"
android:screenOrientation="portrait" />
<activity
android:name=".personalhome.background.BackgroundPreviewActivity"
android:screenOrientation="portrait" />
<activity
android:name=".personalhome.background.BackgroundClipActivity"
android:screenOrientation="portrait"
android:theme="@style/TransparentStatusBarAndNavigationBar"/>
<activity
android:name=".personalhome.excellentcomments.ExcellentCommentsActivity"
android:screenOrientation="portrait" />
<!-- &lt;!&ndash; 使用小米/华为推送弹窗功能提高推送成功率&ndash;&gt;-->
<!-- <activity-->
<!-- android:name="com.gh.gamecenter.PushProxyActivity"-->
<!-- android:exported="true"-->
<!-- android:launchMode="singleTask"-->
<!-- android:theme="@android:style/Theme.Translucent" />-->
<activity
android:name="com.gh.gamecenter.SkipActivity"
android:theme="@style/Theme.AppCompat.Light.Fullscreen.Transparent">
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<data android:scheme="ghzhushou" />
<action android:name="com.sina.weibo.sdk.action.ACTION_SDK_REQ_ACTIVITY" />
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
<activity
android:name="${applicationId}.wxapi.WXEntryActivity"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@android:style/Theme.Translucent.NoTitleBar"></activity>
<!-- <activity-->
<!-- android:name="${applicationId}.douyinapi.DouYinEntryActivity"-->
<!-- android:launchMode="singleTask"-->
<!-- android:taskAffinity="${applicationId}"-->
<!-- android:exported="true" />-->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<receiver android:name="com.gh.gamecenter.receiver.InstallAndUninstallReceiver" >
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<receiver
android:name="com.gh.gamecenter.receiver.NotificationReceiver"
android:exported="false" >
<intent-filter>
<action android:name="com.gh.gamecenter.NOTIFICATION" />
</intent-filter>
</receiver>
<receiver
android:name="com.gh.gamecenter.receiver.DownloadReceiver"
android:exported="false">
android:exported="false" >
<intent-filter>
<action android:name="com.gh.gamecenter.DOWNLOAD" />
</intent-filter>
</receiver>
<receiver
android:name="com.gh.gamecenter.receiver.InstallReceiver"
android:exported="false">
android:exported="false" >
<intent-filter>
<action android:name="com.gh.gamecenter.INSTALL" />
</intent-filter>
</receiver>
<receiver android:name="com.gh.gamecenter.receiver.NetworkStateReceiver" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
</receiver>
<receiver
android:name="com.xiaomi.push.service.receivers.PingReceiver"
android:exported="false"
android:process=":pushservice" >
<intent-filter>
<action android:name="com.xiaomi.push.PING_TIMER" />
</intent-filter>
</receiver>
<receiver
android:name="com.gh.base.GHPushMessageReceiver"
android:exported="true" >
<intent-filter>
<action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE" />
</intent-filter>
<intent-filter>
<action android:name="com.xiaomi.mipush.MESSAGE_ARRIVED" />
</intent-filter>
<intent-filter>
<action android:name="com.xiaomi.mipush.ERROR" />
</intent-filter>
</receiver>
<receiver
android:name="com.xiaomi.push.service.receivers.NetworkStatusReceiver"
android:exported="true" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<receiver
android:name="com.gh.gamecenter.receiver.ActivitySkipReceiver"
android:exported="true">
android:exported="true" >
<intent-filter>
<action android:name="com.gh.gamecenter.ACTIVITYSKIP" />
</intent-filter>
</receiver>
<!-- <receiver android:name="com.gh.gamecenter.receiver.UmengMessageReceiver">-->
<!-- <intent-filter>-->
<!-- <action android:name="com.gh.gamecenter.UMENG" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<!-- &lt;!&ndash;魅族push应用定义消息receiver声明 &ndash;&gt;-->
<!-- <receiver android:name="com.gh.gamecenter.receiver.UmengMeizuPushReceiver">-->
<!-- <intent-filter>-->
<!-- &lt;!&ndash; 接收push消息 &ndash;&gt;-->
<!-- <action android:name="com.meizu.flyme.push.intent.MESSAGE" />-->
<!-- &lt;!&ndash; 接收register消息 &ndash;&gt;-->
<!-- <action android:name="com.meizu.flyme.push.intent.REGISTER.FEEDBACK" />-->
<!-- &lt;!&ndash; 接收unregister消息&ndash;&gt;-->
<!-- <action android:name="com.meizu.flyme.push.intent.UNREGISTER.FEEDBACK" />-->
<!-- &lt;!&ndash; 兼容低版本Flyme3推送服务配置 &ndash;&gt;-->
<!-- <action android:name="com.meizu.c2dm.intent.REGISTRATION" />-->
<!-- <action android:name="com.meizu.c2dm.intent.RECEIVE" />-->
<!-- <category android:name="${applicationId}" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->
<receiver
android:name="com.gh.common.im.ImReceiver"
android:enabled="true">
<intent-filter android:priority="2147483647">
<action android:name="com.gh.im" />
<action android:name="action_finish" />
</intent-filter>
</receiver>
<!-- <meta-data-->
<!-- android:name="com.huawei.hms.client.appid"-->
<!-- android:value="@string/huawei_push_appid" />-->
<!-- <service-->
<!-- android:name="com.gh.base.GHUmengNotificationService"-->
<!-- android:permission="android.permission.BIND_JOB_SERVICE" />-->
<!--<service android:name = "com.gh.gamecenter.statistics.AppStaticService" />-->
<!-- 梦工厂配置 开始 -->
<!--<meta-data
android:name="MGC_APPID"
android:value="1001276" />
<provider
android:name="com.leto.game.base.provider.LetoFileProvider"
android:authorities="${applicationId}.leto.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/leto_file_path"
tools:replace="android:resource" />
</provider>-->
<!-- 梦工厂配置 结束 -->
<!-- 穿山甲配置 开始 -->
<!--<provider
android:name="com.bytedance.sdk.openadsdk.TTFileProvider"
android:authorities="${applicationId}.TTFileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<provider
android:name="com.bytedance.sdk.openadsdk.multipro.TTMultiProvider"
android:authorities="${applicationId}.TTMultiProvider"
android:exported="false" />-->
<!-- 穿山甲配置 结束 -->
<service
android:name="com.gh.download.DownloadService" />
<service
android:name="com.xiaomi.push.service.XMPushService"
android:enabled="true"
android:process=":pushservice" />
<service
android:name="com.xiaomi.mipush.sdk.PushMessageHandler"
android:enabled="true"
android:exported="true" />
<service
android:name="com.xiaomi.mipush.sdk.MessageHandleService"
android:enabled="true" />
</application>
</manifest>

File diff suppressed because it is too large Load Diff

View File

@ -1,163 +0,0 @@
function requestContentFocus() {
$("#editor").focus();
}
function setupWhenContentEditable() {
var editor = $("#editor");
if (!editor[0].hasAttribute("contenteditable")) {
return;
}
// paste
editor.on("paste", function(e) {
e.preventDefault();
var text = (e.originalEvent || e).clipboardData.getData("text/plain");
text = text.replace(/\n/g, "<br>");
if ("" != text) {
document.execCommand("insertHTML", false, text);
} else {
window.onPasteListener.onPaste();
}
});
requestContentFocus();
}
function getStyle(dom, name) {
return window.getComputedStyle(dom)[name];
}
function customLinkgo(self) {
var datas = self.dataset.datas;
console.log(datas)
window.OnLinkClickListener.onClick(datas);
}
var typeClassList = [
"community_article-container",
"answer-container",
"game-container"
];
function removeDomByParent(curDom) {
if (curDom.parentElement) {
curDom.parentElement.removeChild(curDom);
}
}
window.addEventListener("load", function() {
var EditorDom = document.querySelector("#editor");
setupWhenContentEditable();
document.addEventListener("keydown", function(e) {
var event = e || window.event;
if (event.keyCode === 8) {
var s = document.getSelection();
var r = s.getRangeAt(0);
if (r.startOffset === r.endOffset && r.endOffset === 0) {
var preDOM = s.focusNode.previousElementSibling;
if (
preDOM &&
preDOM instanceof Element &&
preDOM.nodeName === "IMG" &&
getStyle(preDOM, "display") === "block"
) {
preDOM.parentElement.removeChild(preDOM);
}
}
var customDom = s.focusNode;
if (customDom) {
if (
r.startContainer.nodeName.toLowerCase() === "blockquote" &&
r.startOffset === 0
) {
RE.formatBlock();
e.preventDefault();
} else if (
customDom.nodeName === "#text" &&
customDom.previousElementSibling &&
typeClassList.indexOf(customDom.previousElementSibling.className) >
-1 &&
r.startOffset === 1
) {
var needDeleteDom = customDom.previousElementSibling;
needDeleteDom.insertAdjacentElement(
"afterend",
document.createElement("br")
);
} else if (
customDom instanceof Element &&
customDom.childNodes[s.focusOffset] &&
customDom.childNodes[s.focusOffset].previousElementSibling &&
typeClassList.indexOf(
customDom.childNodes[s.focusOffset].previousElementSibling.className
) > -1
) {
customDom =
customDom.childNodes[s.focusOffset].previousElementSibling;
customDom.parentElement.removeChild(customDom);
}
}
}
});
document.addEventListener("keyup", function(e) {
var event = e || window.event;
if (event.keyCode === 13) {
var s = document.getSelection();
var curDom = s.focusNode;
var preDom = curDom.previousElementSibling;
if (
curDom.nodeName.toLowerCase() === "blockquote" &&
preDom.nodeName.toLowerCase() === "blockquote"
) {
if (
preDom.childNodes.length > 1 ||
(preDom.childNodes.length === 1 &&
preDom.childNodes[0].tagName !== "BR")
) {
curDom.style.marginTop = 0;
preDom.style.marginBottom = 0;
} else if (
(curDom.childNodes.length === 0 ||
(curDom.childNodes.length === 1 &&
curDom.childNodes[0].tagName === "BR")) &&
(preDom.childNodes.length === 0 ||
(preDom.childNodes.length === 1 &&
preDom.childNodes[0].tagName === "BR"))
) {
removeDomByParent(curDom);
var startQuoteDom = preDom.previousElementSibling;
startQuoteDom && startQuoteDom.nodeName.toLowerCase() === "blockquote"
? (startQuoteDom.style.marginBottom = "10px")
: null;
var range = document.createRange();
range.selectNode(preDom);
s.removeAllRanges();
s.addRange(range);
RE.formatBlock();
}
}
}
});
document.addEventListener("selectionchange", function(e) {
var event = e || window.event;
var targetDom = event.target.activeElement;
if (targetDom.id === "editor" && targetDom.lastElementChild) {
if (typeClassList.indexOf(targetDom.lastElementChild.className) > -1) {
var brDom = document.createElement("br");
EditorDom.appendChild(brDom);
}
}
});
});

View File

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="stylesheet" type="text/css" href="normalize.css">
<link rel="stylesheet" type="text/css" href="style.css">
<link rel="stylesheet" type="text/css" href="video-js.min.css">
<!-- <link rel="stylesheet" href="https://static-web.ghzs.com/website-static/lib/video-js.min.css">--> <!--在web页面播放视频-->
<!--<link rel="stylesheet" type="text/css" href="https://resource.ghzs.com/css/halo_app.css">-->
</head>
<body style="overflow-x: hidden; word-break: break-all;">
<div id="editor" contenteditable="false"></div>
<script type="text/javascript" src="zepto.min.js"></script>
<script type="text/javascript" src="rich_editor.js"></script>
<script type="text/javascript" src="video.min.js"></script>
<!--<script src="https://static-web.ghzs.com/website-static/lib/video.min.js"></script>--> <!--在web页面播放视频-->
<!--<script type="text/javascript" src="content.js"></script>-->
<!--<script type="text/javascript" src="https://resource.ghzs.com/js/halo_app.js"></script>-->
</body>
</html>

View File

@ -1,40 +0,0 @@
emoji_kf_1.png,:smile:
emoji_kf_2.png,:smiley:
emoji_kf_3.png,:laughing:
emoji_kf_4.png,:blush:
emoji_kf_5.png,:heart_eyes:
emoji_kf_6.png,:smirk:
emoji_kf_7.png,:flushed:
emoji_kf_8.png,:kissing_heart:
emoji_kf_9.png,:grin:
emoji_kf_10.png,:wink:
emoji_kf_11.png,:stuck_out_tongue_winking_eye:
emoji_kf_12.png,:stuck_out_tongue_closed eyes:
emoji_kf_13.png,:worried:
emoji_kf_14.png,:sleeping:
emoji_kf_15.png,:expressionless:
emoji_kf_16.png,:sweat_smile:
emoji_kf_17.png,:joy:
emoji_kf_18.png,:cold_sweat:
emoji_kf_19.png,:sob:
emoji_kf_20.png,:angry:
emoji_kf_21.png,:mask:
emoji_kf_22.png,:scream:
emoji_kf_23.png,:sunglasses:
emoji_kf_24.png,:heart:
emoji_kf_25.png,:broken_heart:
emoji_kf_26.png,:star:
emoji_kf_27.png,:anger:
emoji_kf_28.png,:exclamation:
emoji_kf_29.png,:question:
emoji_kf_30.png,:zzz:
emoji_kf_31.png,:thumbsup:
emoji_kf_32.png,:thumbsdown:
emoji_kf_33.png,:ok_hand:
emoji_kf_34.png,:punch:
emoji_kf_35.png,:yeah:
emoji_kf_36.png,:clap:
emoji_kf_37.png,:muscle:
emoji_kf_38.png,:pray:
emoji_kf_39.png,:skull:
emoji_kf_40.png,:trollface:

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1 +0,0 @@
{"v":"5.5.9","fr":60,"ip":0,"op":90,"w":1080,"h":202,"nm":"click","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"椭圆形","sr":1,"ks":{"o":{"a":0,"k":20,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[204,1455,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"椭圆形","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":63,"s":[10]},{"t":70,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[204,1455,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":39,"s":[100,100,100]},{"t":49,"s":[110,110,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[36,36],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"圆环","refId":"comp_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.531],"y":[0]},"t":28,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":38,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.526],"y":[0]},"t":48,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.446],"y":[0]},"t":63,"s":[50]},{"t":82,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[125.951,79.658,0],"ix":2},"a":{"a":0,"k":[205.951,1458.658,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.601,0.601,0.333],"y":[0,0,0]},"t":28,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.528,0.528,0.333],"y":[0,0,0]},"t":38,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.526,0.526,0.333],"y":[0,0,0]},"t":48,"s":[120,120,100]},{"t":63,"s":[100,100,100]}],"ix":6}},"ao":0,"w":1080,"h":1920,"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"点击手","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.596],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.515],"y":[0]},"t":63,"s":[100]},{"t":83,"s":[0]}],"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.507],"y":[0]},"t":10,"s":[6]},{"t":30,"s":[2]}],"ix":10},"p":{"a":0,"k":[178.982,123.325,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.489,0.489,0.333],"y":[0,0,0]},"t":10,"s":[100,100,100]},{"t":30,"s":[90,90,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-5.33,-8.3],[3.89,0.27],[-4.4,-1.68],[-4.33,-0.67],[-4.08,9.32],[3.33,5.44],[3.39,4.6],[0.87,-3.7],[3.6,-0.86],[1.03,-0.21],[2.34,-0.53],[0.96,1.15],[4.22,5.48],[-1.18,-4.56]],"o":[[1.11,1.71],[-3.89,-0.27],[6.42,2.5],[4.33,0.66],[1.63,-5.32],[-3.34,-5.45],[-1.68,-2.1],[-0.71,3.14],[-3.43,0.95],[-0.57,0.08],[-3.86,1.12],[-3.23,-3.94],[-1.89,-2.28],[2.42,4.64]],"v":[[-5.387,9.698],[-10.717,8.498],[-12.327,15.628],[5.813,21.748],[23.313,11.778],[20.273,-1.202],[11.563,-13.962],[5.393,-12.362],[1.083,-13.722],[-2.087,-9.742],[-5.707,-11.752],[-8.777,-7.572],[-18.297,-20.542],[-23.827,-17.832]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"路径备份 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"v":"5.6.4","fr":25,"ip":0,"op":35,"w":1080,"h":214,"nm":"点赞","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"椭圆形 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.588],"y":[0]},"t":17,"s":[15]},{"t":20,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[468.04,73.68,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.659,0.659,0.333],"y":[0,0,0]},"t":10,"s":[50,50,100]},{"t":19,"s":[150,150,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":10,"op":1510,"st":10,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"椭圆形 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.588],"y":[0]},"t":27,"s":[15]},{"t":30,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[468.04,73.68,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.659,0.659,0.333],"y":[0,0,0]},"t":20,"s":[50,50,100]},{"t":28,"s":[140,140,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":1520,"st":20,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"路径备份 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.602],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.663],"y":[0]},"t":24,"s":[100]},{"t":29,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[531.02,129.675,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.514,0.514,0.333],"y":[0,0,0]},"t":5,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.533,0.533,0.333],"y":[0,0,0]},"t":10,"s":[90,90,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.586,0.586,0.333],"y":[0,0,0]},"t":15,"s":[95,95,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.499,0.499,0.333],"y":[0,0,0]},"t":19,"s":[90,90,100]},{"t":24,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-5.33,-8.3],[3.89,0.27],[-4.4,-1.68],[-4.33,-0.67],[-4.08,9.32],[3.33,5.44],[3.39,4.6],[0.87,-3.7],[3.6,-0.86],[1.03,-0.21],[2.34,-0.53],[0.96,1.15],[4.22,5.48],[-1.18,-4.56]],"o":[[1.11,1.71],[-3.89,-0.27],[6.42,2.5],[4.33,0.66],[1.63,-5.32],[-3.34,-5.45],[-1.68,-2.1],[-0.71,3.14],[-3.43,0.95],[-0.57,0.08],[-3.86,1.12],[-3.23,-3.94],[-1.89,-2.28],[2.42,4.64]],"v":[[-5.387,9.698],[-10.717,8.498],[-12.327,15.628],[5.813,21.748],[23.313,11.778],[20.273,-1.202],[11.563,-13.962],[5.393,-12.362],[1.083,-13.722],[-2.087,-9.742],[-5.707,-11.752],[-8.777,-7.572],[-18.297,-20.542],[-23.827,-17.832]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"路径备份 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1500,"st":0,"bm":0}],"markers":[]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"v":"5.5.9","fr":60,"ip":0,"op":120,"w":1080,"h":586,"nm":"上滑","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"手","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.642],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":6,"s":[100]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.558],"y":[0]},"t":60,"s":[100]},{"t":71,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.479,"y":0},"t":3,"s":[611,475,0],"to":[0,-62.75,0],"ti":[0,62.75,0]},{"t":40,"s":[611,98.5,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[90,90,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-7.78,-12.06],[5.68,0.39],[-6.42,-2.45],[-6.32,-0.97],[-5.95,13.55],[4.87,7.91],[4.94,6.69],[1.26,-5.37],[5.25,-1.25],[1.5,-0.3],[3.4,-0.77],[1.4,1.68],[6.15,7.98],[-1.73,-6.64]],"o":[[1.62,2.49],[-5.68,-0.39],[9.37,3.63],[6.31,0.98],[2.39,-7.74],[-4.87,-7.92],[-2.45,-3.05],[-1.05,4.57],[-4.99,1.39],[-0.83,0.13],[-5.63,1.63],[-4.71,-5.72],[-2.75,-3.31],[3.52,6.76]],"v":[[-7.86,14.26],[-15.64,12.52],[-17.99,22.89],[8.47,31.78],[33.98,17.29],[29.55,-1.59],[16.85,-20.15],[7.86,-17.83],[1.56,-19.8],[-3.05,-14.02],[-8.33,-16.94],[-12.44,-11.045],[-26.761,-30.19],[-34.75,-25.78]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"路径","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"矩形","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.715],"y":[0]},"t":57,"s":[100]},{"t":67,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[525,228.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.491,"y":0},"t":3,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[24.328,180.5],[-22,180.5],[-22,204.5],[24.328,204.5]],"c":true}]},{"t":40,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[24.328,-207.5],[-22,-207.5],[-22,204.5],[24.328,204.5]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"蒙版 1"}],"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[6,137],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"矩形路径 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.5,1,1,1,1,1,1,1,0,1,0.5,0.5,1,0],"ix":9}},"s":{"a":0,"k":[0,-68.5],"ix":5},"e":{"a":0,"k":[0,68.5],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"矩形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":33,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":3,"op":123,"st":3,"bm":0}],"markers":[]}

View File

@ -1 +0,0 @@
{"v":"5.6.9","fr":30,"ip":0,"op":20,"w":66,"h":66,"nm":"bottom bar tab/论坛/选中/E","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"白-修正","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[33,33,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":4,"s":[80,80,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":9,"s":[110,110,100]},{"t":13,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":0,"s":[{"i":[[0,-0.66],[0,0],[1.38,0],[0,1.38],[0,0],[-0.66,0],[0,0]],"o":[[0,0],[0,1.38],[-1.38,0],[0,0],[0,-0.66],[0,0],[0.66,0]],"v":[[2.5,-1.3],[2.5,0],[0,2.5],[-2.5,0],[-2.5,-1.3],[-1.3,-2.5],[1.3,-2.5]],"c":true}]},{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":4,"s":[{"i":[[0,-0.66],[0,0],[1.38,0],[0,1.38],[0,0],[-0.66,0],[0,0]],"o":[[0,0],[0,1.38],[-1.38,0],[0,0],[0,-0.66],[0,0],[0.66,0]],"v":[[2.5,-0.102],[2.5,0],[0,2.109],[-2.5,0],[-2.5,-0.102],[-1.3,-1.302],[1.3,-1.302]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":1,"y":0},"t":9,"s":[{"i":[[0,-0.66],[0,0],[1.38,0],[0,1.38],[0,0],[-0.66,0],[0,0]],"o":[[0,0],[0,1.38],[-1.38,0],[0,0],[0,-0.66],[0,0],[0.66,0]],"v":[[2.498,-1.845],[2.5,0],[0,2.734],[-2.5,0],[-2.502,-1.845],[-1.302,-3.045],[1.298,-3.045]],"c":true}]},{"t":13,"s":[{"i":[[0,-0.66],[0,0],[1.38,0],[0,1.38],[0,0],[-0.66,0],[0,0]],"o":[[0,0],[0,1.38],[-1.38,0],[0,0],[0,-0.66],[0,0],[0.66,0]],"v":[[2.5,-1.3],[2.5,0],[0,2.5],[-2.5,0],[-2.5,-1.3],[-1.3,-2.5],[1.3,-2.5]],"c":true}]}],"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"填充 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"矩形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"蓝","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[33,33.76,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.8,0.42],[-3.44,-0.79],[-0.09,-1.71],[0,0],[0,0],[1.98,-0.1],[0,0],[0,0],[0,0],[0.62,0.57],[0,0],[0,0],[0,0],[0,0],[0,0],[0.2,1.89],[0,0],[0,0],[0,0]],"o":[[3.44,-0.79],[1.74,0.41],[0,0],[0,0],[0,2],[0,0],[0,0],[0,0],[-0.69,0.52],[0,0],[0,0],[0,0],[0,0],[0,0],[-1.94,0],[0,0],[0,0],[0,0],[0,-1.8]],"v":[[-6.39,-8.971],[6.39,-8.971],[9.49,-5.471],[9.5,-5.271],[9.5,3.249],[5.95,6.989],[5.75,6.999],[3.25,6.999],[0.3,9.209],[-1.95,9.089],[-2.06,8.969],[-2.17,8.849],[-2.21,8.779],[-3.4,6.999],[-5.75,6.999],[-9.48,3.639],[-9.49,3.449],[-9.5,3.249],[-9.5,-5.271]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.266,0.638,1,0.5,0.242,0.595,1,1,0.217,0.552,1],"ix":9}},"s":{"a":0,"k":[-9.5,-9.561],"ix":5},"e":{"a":0,"k":[9.5,9.561],"ix":6},"t":1,"nm":"color","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"路径","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"预合成 1","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[33,33,0],"ix":2},"a":{"a":0,"k":[33,33,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":4,"s":[70,70,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":9,"s":[110,110,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":13,"s":[90,90,100]},{"t":16,"s":[100,100,100]}],"ix":6}},"ao":0,"w":66,"h":66,"ip":0,"op":20,"st":0,"bm":0}],"markers":[]}

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
{"v":"5.5.9","fr":30,"ip":0,"op":20,"w":66,"h":66,"nm":"tab_index","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"椭圆形备份","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.596],"y":[0]},"t":0,"s":[0]},{"t":6,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[33,40.493,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.54,0.54,0.333],"y":[0,0,0]},"t":4,"s":[80,80,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.555,0.555,0.333],"y":[0,0,0]},"t":9,"s":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.552,0.552,0.333],"y":[0,0,0]},"t":13,"s":[90,90,100]},{"t":16,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[5,5],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.635],"y":[0]},"t":0,"s":[0]},{"t":8,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆形备份","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":-0.5,"op":59.5,"st":-0.5,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"路径备份","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[33,32.993,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.508,0.508,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.488,0.488,0.333],"y":[0,0,0]},"t":4,"s":[80,80,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.502,0.502,0.333],"y":[0,0,0]},"t":9,"s":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.534,0.534,0.333],"y":[0,0,0]},"t":13,"s":[95,95,100]},{"t":16,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.24,-1.21],[-1.93,-1.89],[-0.24,-0.57],[0,-0.06],[0,-2.63],[1.76,0],[0,0],[0,1.72],[0,2.63],[-0.24,0.61],[-0.03,0.02],[-1.93,1.89]],"o":[[1.92,1.89],[0.02,0.02],[0.24,0.57],[0,2.63],[0,1.72],[0,0],[-1.76,0],[0,-2.62],[0,-0.07],[0.25,-0.61],[1.92,-1.89],[1.24,-1.21]],"v":[[2.26,-9.09],[8.03,-3.42],[8.76,-2.38],[9,-1.02],[9,6.88],[5.82,10],[-5.82,10],[-9,6.88],[-9,-0.99],[-8.71,-2.38],[-8.01,-3.43],[-2.23,-9.09]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.266,0.638,1,0.5,0.242,0.595,1,1,0.217,0.552,1],"ix":9}},"s":{"a":0,"k":[-4.902,-4.663],"ix":5},"e":{"a":0,"k":[8.159,8.646],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"路径备份","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]}

View File

@ -1 +0,0 @@
{"v":"5.5.9","fr":30,"ip":0,"op":20,"w":66,"h":66,"nm":"tab_video","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"形状图层 1","parent":2,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.771],"y":[0]},"t":0,"s":[0]},{"t":5,"s":[100]}],"ix":11},"r":{"a":0,"k":90,"ix":10},"p":{"a":0,"k":[-3.742,6.835,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[30.937,31.042,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"sr","sy":2,"d":1,"pt":{"a":0,"k":3,"ix":3},"p":{"a":0,"k":[0,0],"ix":4},"r":{"a":0,"k":0,"ix":5},"or":{"a":0,"k":29.286,"ix":7},"os":{"a":0,"k":75,"ix":9},"ix":1,"nm":"多边星形路径 1","mn":"ADBE Vector Shape - Star","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":13,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-20.75,-13.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[102.743,88.578],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"多边星形 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.657],"y":[0]},"t":0,"s":[0]},{"t":8,"s":[100]}],"ix":2},"o":{"a":0,"k":-115,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":60,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"路径 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[33,33.004,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.508,0.508,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.488,0.488,0.333],"y":[0,0,0]},"t":4,"s":[80,80,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.502,0.502,0.333],"y":[0,0,0]},"t":9,"s":[110,110,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.534,0.534,0.333],"y":[0,0,0]},"t":13,"s":[95,95,100]},{"t":16,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[2.16,0.38],[3.22,-0.55],[0.38,-2.16],[-0.55,-3.22],[-2.16,-0.38],[-1.63,0],[-1.61,0.27],[-0.38,2.16],[0.55,3.22]],"o":[[-0.38,-2.16],[-3.22,-0.55],[-2.16,0.38],[-0.55,3.22],[0.38,2.16],[1.61,0.27],[1.63,0],[2.16,-0.38],[0.55,-3.22],[0,0]],"v":[[9.09,-4.86],[4.86,-9.09],[-4.86,-9.09],[-9.09,-4.86],[-9.09,4.86],[-4.86,9.09],[0,9.5],[4.86,9.09],[9.09,4.86],[9.09,-4.86]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.266,0.638,1,0.5,0.242,0.595,1,1,0.217,0.552,1],"ix":9}},"s":{"a":0,"k":[-5.174,-4.43],"ix":5},"e":{"a":0,"k":[8.612,8.214],"ix":6},"t":1,"nm":"Gradient Fill 3","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"路径","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60,"st":0,"bm":0}],"markers":[]}

View File

@ -1,5 +0,0 @@
# This is a simple Microlog configuration file
microlog.level=DEBUG
microlog.appender=LogCatAppender;FileAppender
microlog.formatter=PatternFormatter
microlog.formatter.PatternFormatter.pattern=%c [%P] %m %T

View File

@ -1,413 +0,0 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

View File

@ -1,132 +0,0 @@
[
{
"login": {
"title": "开启消息通知",
"content": "新游上线、互动回复,重要推送不错过",
"image": "bg_notification_login_style_1",
"styleNo": "样式A",
"scenes": "场景1"
},
"question": {
"title": "开启消息通知",
"content": "及时查看大神回答",
"image": "bg_notification_question_style_1",
"styleNo": "样式A",
"scenes": "场景2"
},
"answer": {
"title": "开启消息通知",
"content": "及时查看点赞与评论",
"image": "bg_notification_answer_style_1",
"styleNo": "样式A",
"scenes": "场景3"
},
"article": {
"title": "开启消息通知",
"content": "及时查看点赞与评论",
"image": "bg_notification_article_style_1",
"styleNo": "样式A",
"scenes": "场景4"
},
"video": {
"title": "开启消息通知",
"content": "实时获取审核与推荐进度",
"image": "bg_notification_video_style_1",
"styleNo": "样式A",
"scenes": "场景5"
},
"rating": {
"title": "开启消息通知",
"content": "成功上墙立即知道",
"image": "bg_notification_rating_style_1",
"styleNo": "样式A",
"scenes": "场景6"
},
"gift": {
"title": "开启消息通知",
"content": "新上礼包不再错过",
"image": "bg_notification_gift_style_1",
"styleNo": "样式A",
"scenes": "场景7"
},
"reserveGame": {
"title": "开启消息通知",
"content": "新游上线即时体验",
"image": "bg_notification_reserve_game_style_1",
"styleNo": "样式A",
"scenes": "场景8"
},
"feedback": {
"title": "开启消息通知",
"content": "及时查看客服回复",
"image": "bg_notification_feedback_style_1",
"styleNo": "样式A",
"scenes": "场景9"
}
},
{
"login": {
"title": "咦!是新的小伙伴耶!",
"content": "打开<font color=\"#1383EB\">通知开关</font>,游戏、礼包、抽奖活动不错过",
"image": "bg_notification_login_style_2",
"styleNo": "样式B",
"scenes": "场景1"
},
"question": {
"title": "发布成功!答案马上来!",
"content": "为了第一时间通知您,需要打开<font color=\"#1383EB\">通知开关</font>",
"image": "bg_notification_question_style_2",
"styleNo": "样式B",
"scenes": "场景2"
},
"answer": {
"title": "精彩的回答!大佬牛啤!",
"content": "打开<font color=\"#1383EB\">通知开关</font>,可以第一时间收获赞美和感谢哟!",
"image": "bg_notification_answer_style_2",
"styleNo": "样式B",
"scenes": "场景3"
},
"article": {
"title": "发布成功!不愧是你!",
"content": "打开<font color=\"#1383EB\">通知开关</font>,可以第一时间收获赞美和互动哟!",
"image": "bg_notification_article_style_2",
"styleNo": "样式B",
"scenes": "场景4"
},
"video": {
"title": "“百万”播放预定!",
"content": "<font color=\"#1383EB\">打开通知!</font>第一时间知道审核结果和互动信息哟!",
"image": "bg_notification_video_style_2",
"styleNo": "样式B",
"scenes": "场景5"
},
"rating": {
"title": "这游戏超好玩,我说的!",
"content": "想知道有多少人吃下安利?<font color=\"#1383EB\">打开通知</font>,马上知道!",
"image": "bg_notification_rating_style_2",
"styleNo": "样式B",
"scenes": "场景6"
},
"gift": {
"title": "获得道具:神奇的游戏礼包!",
"content": "<font color=\"#1383EB\">打开通知!</font>礼包上线,马上知道!",
"image": "bg_notification_gift_style_2",
"styleNo": "样式B",
"scenes": "场景7"
},
"reserveGame": {
"title": "玩最新的游戏,做游戏圈最靓的仔",
"content": "<font color=\"#1383EB\">打开通知!</font>游戏上线,更快知道!",
"image": "bg_notification_reserve_game_style_2",
"styleNo": "样式B",
"scenes": "场景8"
},
"feedback": {
"title": "真是重要的反馈!",
"content": "感恩有你,光环更精彩!<font color=\"#1383EB\">打开通知</font>,客服回复,马上知道!",
"image": "bg_notification_feedback_style_2",
"styleNo": "样式B",
"scenes": "场景9"
}
}
]

File diff suppressed because it is too large Load Diff

View File

@ -1,149 +1,30 @@
9u=\u4e5d\u6e38
4399=\u0034\u0033\u0039\u0039
yyb=\u5e94\u7528\u5b9d
xm=\u5c0f\u7c73
dl=\u5f53\u4e50
91=\u0039\u0031
yw=\u76ca\u73a9
gf-w=\u5b98\u65b9\u5916
az=\u5b89\u667a
oppo=\u006f\u0070\u0070\u006f
wdj=\u8c4c\u8c46\u835a
360=\u0033\u0036\u0030
vivo=\u0076\u0069\u0076\u006f
pps=\u0050\u0050\u0053
hw=\u534e\u4e3a
37=\u0033\u0037\u73a9
baidu=\u767e\u5ea6
ow=\u5076\u73a9
gf=\u5b98\u65b9
lenovo=\u8054\u60f3
pptv=\u0050\u0050\u0054\u0056
jf=\u673a\u950b
mumayi=\u6728\u8682\u8681
8868=\u0038\u0038\u0036\u0038
19196=\u0031\u0039\u0031\u0039\u0036
07073=\u0030\u0037\u0030\u0037\u0033
gp=\u679c\u76d8
mzw=\u62c7\u6307\u73a9
af=\u5b89\u950b
lb=\u730e\u5b9d
ayx=\u963f\u6e38\u620f
tt=\u0054\u0054
xiongmao=\u718a\u732b\u73a9
aq=\u5b89\u8da3
ls=\u4e50\u89c6
jl=\u91d1\u7acb
lehh=\u4e50\u55e8\u55e8
pyw=\u670b\u53cb\u73a9
azsc=\u5b89\u5353\u5e02\u573a
hyx=\u548c\u6e38\u620f
aiyouxi=\u7231\u6e38\u620f
woyouxi=\u6c83\u6e38\u620f
lg=\u4e50\u8d2d\u0028\u70b9\u70b9\u0029
gf-h=\u0028\u65e7\u0029\u5b98\u65b9\u4e13\u670d
gf-z=\u0028\u65e7\u0029\u5b98\u65b9\u6df7\u670d
gfzf=\u5b98\u65b9\u4e13\u670d
gfhf=\u5b98\u65b9\u6df7\u670d
owzf=\u5076\u73a9\u4e13\u670d
gf-n=\u5b98\u65b9\u5185
my=\u9b54\u9047
jinshan=\u91d1\u5c71
duoku=\u591a\u9177
yk=\u4f18\u9177
xl=\u65b0\u6d6a
sougou=\u641c\u72d7
dy=\u6597\u9c7c
dw=\u591a\u73a9
tf=\u53f0\u670d
xunlei=\u8fc5\u96f7
ky=\u5feb\u7528
lenvono=\u0028\u65e7\u0029\u8054\u60f3
jifeng=\u0028\u65e7\u0029\u673a\u950b
anzhi=\u0028\u65e7\u0029\u5b89\u667a
37wan=\u0028\u65e7\u0029\u0033\u0037\u73a9
ewan=\u0028\u65e7\u0029\u76ca\u73a9
mi=\u0028\u65e7\u0029\u5c0f\u7c73
dangle=\u0028\u65e7\u0029\u5f53\u4e50
ouwan=\u0028\u65e7\u0029\u5076\u73a9
ttyy=\u0028\u65e7\u0029\u0054\u0054
huawei=\u0028\u65e7\u0029\u534e\u4e3a
shuizhu=\u0028\u65e7\u0029\u6c34\u716e
43997=\u0034\u0033\u0039\u0039\u0037
19196zf=\u0031\u0039\u0031\u0039\u0036\u4e13\u670d
51508=\u0035\u0031\u0035\u0030\u0038
zzb=\u81f3\u5c0a\u5b9d
wkd=\u73a9\u5ba2\u7248
yq=\u4f18\u8da3
tiantian=\u5929\u5929
ddw=\u70b9\u70b9\u73a9
by=\u7206\u6e38
as=\u7231\u4e0a
flb=\u5c0f\u7b28\u6e38\u620f
cf=\u695a\u98ce
itools=\u0049\u0054\u004f\u004f\u004c\u0053
ayw=\u7231\u7ea6\u73a9
cc=\u866b\u866b
kpzs=\u9760\u8c31\u52a9\u624b
xtt=\u65b0\u0074\u0074
yt=\u6e38\u9014\u7248
9665=\u0039\u0036\u0036\u0035
lehhkf=\u4e50\u55e8\u55e8\u006b\u0066
lehhol=\u4e50\u55e8\u55e8\u006f\u006c
afly=\u5b89\u950b\u006c\u0079
360hf=\u0033\u0036\u0030\u6df7\u670d\u7248
qql=\u9f50\u9f50\u4e50
yehuo=\u91ce\u706b\u7248
jizhi=\u6781\u81f4\u7248
shengxun=\u76db\u8baf\u6e38\u620f
9upt=\u4e5d\u6e38\u666e\u901a\u7248
9ujs=\u4e5d\u6e38\u52a0\u901f\u7248
xmpt=\u5c0f\u7c73\u666e\u901a\u7248
xmjs=\u5c0f\u7c73\u52a0\u901f\u7248
gfpt=\u5b98\u65b9\u666e\u901a\u7248
gfjs=\u5b98\u65b9\u52a0\u901f\u7248
wkd1=\u73a9\u5ba2\u65b0\u7248
5288=\u0035\u0032\u0038\u0038
7guo=\u4e03\u679c
zhanyou=\u5c55\u6e38\u7248
babie=\u5df4\u522b\u65f6\u4ee3\u7248
yueyou=\u7ea6\u6e38\u7248
kuniu=\u9177\u725b\u7248
chukong=\u89e6\u63a7\u7248
changxiang=\u7545\u60f3\u7248
zhuohua=\u707c\u534e\u7248
duokemeng=\u54c6\u53ef\u68a6\u7248
xuanyun=\u7384\u4e91\u7248
hanqu=\u701a\u8da3\u7248
wapu=\u86d9\u6251\u7248
shengli=\u80dc\u5229\u6e38\u620f\u7248
heitao=\u9ed1\u6843\u4e92\u52a8\u7248
xumei=\u65ed\u6885\u6e38\u620f\u7248
weixun=\u5fae\u8baf\u7248
tianxiang=\u5929\u8c61\u4e92\u52a8\u7248
taiqi=\u6cf0\u5947\u7248
chujian=\u521d\u89c1\u7248
gaiya=\u76d6\u5a05\u7248
wanmei=\u5b8c\u7f8e\u4e16\u754c\u7248
zhuoyue=\u5353\u8d8a\u7248
meifeng=\u7f8e\u5cf0\u7248
xuanji=\u7384\u673a\u7248
changyou=\u7545\u6e38\u7248
syg=\u624b\u6e38\u72d7
youzu=\u6e38\u65cf\u7248
bili=\u0062\u0069\u006c\u0069\u0062\u0069\u006c\u0069\u7248
ly=\u4e50\u6e38
gfwy=\u7f51\u9875\u7248
miqi=\u7c73\u5947\u73a9
ayw=\u6e38\u620f\u0066\u0061\u006e
ys=\u591c\u795e\u7248
aofei=\u5965\u98de
mgw=\u8611\u83c7\u73a9
longc=\u9f99\u57ce\u7248
16y=\u0031\u0036\u6e38
xq=\u5c0f\u4e03
yuwan=\u9c7c\u4e38\u7248
jgp=\u679c\u76d8\u65e7
lequ=\u6dd8\u8da3
jianguo=\u575a\u679c\u7248
yanmeng=\u5ef6\u68a6\u7248
9u=\u4E5D\u6E38\u7248
360=360\u7248
baidu=\u767E\u5EA6\u7248
dangle=\u5F53\u4E50\u7248
ouwan=\u5076\u73A9\u7248
gf=\u5B98\u65B9\u7248
mi=\u5C0F\u7C73\u7248
oppo=OPPO\u7248
91=91\u7248
wdj=\u8C4C\u8C46\u835A\u7248
vivo=VIVO\u7248
pps=PPS\u7248
37wan=37\u73A9\u7248
anzhi=\u5B89\u667A\u7248
ewan=\u76CA\u73A9\u7248
huawei=\u534E\u4E3A\u7248
gf-h=\u5B98\u65B9-\u4E13\u670D\u7248
gf-z=\u5B98\u65B9-\u6DF7\u670D\u7248
shuizhu=\u6C34\u716E\u7248
jifeng=\u673A\u950B\u7248
azsc=\u5B89\u5353\u5E02\u573A\u7248
lenvono=\u8054\u60F3\u7248
jinshan=\u91D1\u5C71\u7248
mumayi=\u6728\u8682\u8681\u7248
gf-n=\u5B98\u7F51-\u5185\u7248
gf-w=\u5B98\u7F51-\u5916\u7248
duoku=\u591A\u9177\u7248
pptv=PPTV\u7248
\u5B98\u65B9\u7248=\u5B98\u65B9\u7248
\u5B98\u65B9=\u5B98\u65B9\u7248

View File

@ -1,758 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>隐私政策</title>
<style>
* {
margin: 0;
padding: 0;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
.page {
padding: 0 10px;
}
.date p {
margin-bottom: 6px;
}
p {
font-family: "SourceHanSansSC-regular" !important;
color: #101010;
font-size: 14px;
font-weight: normal;
margin-bottom: 6px;
word-break: break-all;
}
b {
font-weight: 700;
font-size: 14px;
}
.points {
margin: 14px 0;
}
.points p {
margin-bottom: 6px;
}
.introduce p {
margin-bottom: 6px;
}
.content p b {
margin: 6px 0;
display: block;
}
.link-text {
color: rgb(19, 131, 235);
cursor: pointer;
}
.link-text a {
color: rgb(19, 131, 235);
cursor: pointer;
text-decoration: none;
}
.left-indent {
margin-left: 20px;
}
.page-title {
font-weight: bold;
font-size: 16px;
text-align: center;
margin: 20px 0 10px 0;
}
.red-style {
color: red;
}
.bold-font {
font-weight: bold;
}
span.bold {
font-weight: bold;
}
.link-text {
color: #005ad0;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="page">
<div class="page-title">欢迎您使用光环助手!</div>
<div class="introduce">
<p>
为了向您提供游戏预约、论坛互动交流等相关服务,受制于手机系统限制,我们会申请您的设备信息权限;
</p>
<p>为了让您正常使用游戏下载和论坛功能,我们会申请您的储存权限;</p>
<p>以下为完整《隐私权限政策》</p>
<p>
光环助手(简称“我们”)深知个人信息对您的重要性,我们将依据《中华人民共和国网络安全法》、《信息安全技术
个人信息安全规范》GB/T
35273-2017以及其他相关法律法规和技术规范收集和使用您的个人信息以帮助我们向您提供更优质的产品和/或服务,
保护您的个人信息及隐私安全。我们制定本“隐私指引”并特别提示:希望您在使用光环助手及相关服务前仔细阅读并理解本隐私政策,以便做出适当的选择。
</p>
<p>
下文将帮您详细了解我们如何收集、使用、存储、传输、共享、转让(如适用)与保护个人信息;帮您了解查询、访问、删除、更正、撤回授权个人信息的方式。其中,
<b>
有关您个人信息权益的条款重要内容我们已用加粗形式提示,请特别关注。
</b>
</p>
</div>
<div class="points">
<p><b>1.我们处理个人信息的法律依据</b></p>
<p><b>2.我们如何共享、转让、公开披露个人信息</b></p>
<p><b>3.我们如何收集和使用个人信息</b></p>
<p><b>4.我们如何存储个人信息</b></p>
<p><b>5.我们如何保护个人信息的安全</b></p>
<p><b>6.管理您的个人信息</b></p>
<p><b>7.未成年人使用条款</b></p>
<p><b>8.隐私政策的修订和通知</b></p>
<p><b>9.联系我们</b></p>
</div>
<div class="content">
<p><b>1.我们处理个人信息的法律依据</b></p>
<p>
如果您是中华人民共和国大陆地区的用户,我们将依据《中华人民共和国网络安全法》、《信息安全技术
个人信息安全规范》GB/T
35273-2017以及其他相关法律法规收集和使用您的个人信息为您提供产品或服务。
</p>
<p>
我们通常只会在征得您同意的情况下收集您的个人信息。
在某些情况下,我们可能还会基于法律义务或者履行合同之必需向您收集个人信息,或者可能需要个人信息来保护您的重要利益或其他人的利益。
</p>
<p><b>2.我们如何共享、转让、公开披露个人信息</b></p>
<p class="title margintop"><b>2.1第三方SDK接入说明</b></p>
<p>
为保障光环助手App相关功能的实现与应用安全稳定的运行我们会接入由第三方提供的软件开发包SDK实现相关功能。
<br />
我们会对合作方获取有关信息的软件工具开发包SDK进行严格的安全检测并与授权合作伙伴约定严格的数据保护措施令其按照我们的委托目的、服务说明、本隐私权政策以及其他任何相关的保密和安全措施来处理个人信息。
<br />
<span class="red-style">
下方为整个光环助手
<span class="bold">所有版本</span>
内接入的所有信息收集类第三方SDK的权限说明因隐私政策会因光环助手版本迭代而新接入SDK或停止合作部分SDK方便照顾
<span class="bold">所有版本</span>
的用户查看自己SDK第三方权限说明。
<br />
我们对涉及用户信息使用的SDK相关情况进行了逐项列举具体如下
</span>
</p>
<p class="margintop red-style bold-font"><b>1数据统计类</b></p>
<p>1.头条推广</p>
<p>
SDK官网
<span class="link-text">
https://ad.oceanengine.com/openapi/index.html
</span>
</p>
<p>SDK包名com.bytedance</p>
<p>企业主体:北京有竹居网络技术有限公司</p>
<p>使用目的:用于广告流量统计相关服务</p>
<p>
收集信息类型设备品牌、型号、软件系统相关信息、安卓oaid、无线网SSID名称、WiFi路由器MAC地址、设备MAC地址、IMEI、地理位置
</p>
<p>
隐私政策链接:
<span class="link-text">
https://ad.oceanengine.com/openapi/register/protocol.html?rid=vo25p8sfqde
</span>
</p>
<p>2.talkingdata统计</p>
<p>
SDK官网
<span class="link-text">http://www.talkingdata.com/</span>
</p>
<p>SDK包名com.tendcloud</p>
<p>企业主体:北京腾云天下科技有限公司</p>
<p>使用目的:用于统计数据和效果分析,以便为用户提供更好的服务</p>
<p>收集信息类型:设备信息、网络信息、位置信息、应用信息</p>
<p>
隐私政策链接:
<span class="link-text">
http://www.talkingdata.com/privacy.jsp?languagetype=zh_cn
</span>
</p>
<p>3.腾讯MTA</p>
<p>
SDK官网
<span class="link-text">https://mta.qq.com/mta/</span>
</p>
<p>SDK包名com.tencent</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的:用于统计数据和效果分析</p>
<p>
收集信息类型Mac地址、唯一设备识别码IMEI、android
ID、IDFA、OPENUDID、GUID/SIM卡IMSI信息、地理位置信息
</p>
<p>
隐私政策链接:
<span class="link-text">
https://mta.qq.com/mta/ctr_index/protocol_v2/
</span>
</p>
<p>4.腾讯广点通</p>
<p>
SDK官网
<span class="link-text">https://developers.e.qq.com/</span>
</p>
<p>SDK包名com.tencent</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的:用于广告流量统计相关服务</p>
<p>
收集信息类型:
个人常用设备信息IMEI、AndroidID、位置信息IP地址、软件版本号
</p>
<p>
隐私政策链接:
<span class="link-text">https://e.qq.com/optout.html</span>
</p>
<p class="margintop red-style bold-font"><b>2社交登录类</b></p>
<p>5.微信登录分享</p>
<p>
SDK官网
<span class="link-text">https://open.weixin.qq.com/</span>
</p>
<p>SDK包名com.tencent.mm.opensdk</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的:用于支持微信登录、分享</p>
<p>
收集信息类型个人常用设备信息MAC地址、IMEI、AndroidID、硬件型号、操作系统类型、软件信息软件版本号、浏览器类型、IP地址、服务日志信息、通讯日志信息
</p>
<p>
隐私政策链接:
<span class="link-text">https://privacy.tencent.com/</span>
</p>
<p>6.QQ登录分享</p>
<p>
SDK官网
<span class="link-text">https://connect.qq.com/</span>
</p>
<p>SDK包名com.tentcent</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的用于支持QQ登录、分享</p>
<p>
收集信息类型个人常用设备信息MAC地址、IMEI、AndroidID、IMSI、ICCID、序列号、设备型号、操作系统版本、软件信息软件版本号、浏览器类型、网络信息、IP地址、服务日志信息、通讯日志信息 
</p>
<p>
隐私政策链接:
<span class="link-text">
https://wiki.connect.qq.com/qq互联sdk隐私保护声明
</span>
</p>
<p>7.微博登录分享</p>
<p>
SDK官网
<span class="link-text">http://open.weibo.com/authentication</span>
</p>
<p>SDK包名com.sina.weibo.sdk</p>
<p>企业主体:北京微梦创科网络技术有限公司</p>
<p>使用目的:用于支持微博登录、分享</p>
<p>
收集信息类型个人常用设备信息MAC地址、IMEI、AndroidID、IMSI、ICCID、序列号、网络信息、应用列表硬件型号、操作系统类型、软件信息软件版本号、浏览器类型、IP地址、服务日志信息、通讯日志信息
</p>
<p>
隐私政策链接:
<span class="link-text">https://open.weibo.com/wiki/开发者协议</span>
</p>
<p>8.头条抖音登录</p>
<p>
SDK官网
<span class="link-text">https://open.douyin.com/platform</span>
</p>
<p>SDK包名com.bytedance.sdk</p>
<p>企业主体:北京字节跳动科技有限公司</p>
<p>使用目的:用于支持抖音登录</p>
<p>
收集信息类型个人常用设备信息MAC地址、IMEI、AndroidID、硬件型号、操作系统类型、软件信息软件版本号、浏览器类型、IP地址、服务日志信息、通讯日志信息
</p>
<p>
隐私政策链接:
<span class="link-text">
https://www.douyin.com/agreements/?id=6773901168964798477
</span>
</p>
<p class="margintop red-style bold-font"><b>3推送通知类</b></p>
<p>9.友盟推送</p>
<p>
SDK官网
<span class="link-text">https://www.umeng.com/push</span>
</p>
<p>SDK包名com.umeng</p>
<p>企业主体:北京友盟网络科技有限公司</p>
<p>使用目的:用于游戏相关信息的提醒通知</p>
<p>
收集信息类型Mac地址、唯一设备识别码IMEI、android
ID、IDFA、OPENUDID、GUID/SIM卡IMSI信息、地理位置信息
</p>
<p>
隐私政策链接:
<span class="link-text">
https://www.umeng.com/page/policy?spm=a213m0.14063960.0.0.7f626e72hx3nnv
</span>
</p>
<p class="margintop red-style bold-font"><b>4其他功能类</b></p>
<p>10.阿里云反爬虫</p>
<p>
SDK官网
<span class="link-text">https://www.aliyun.com/product/antibot</span>
</p>
<p>SDK包名com.alibaba.wireless</p>
<p>企业主体:阿里巴巴网络技术有限公司</p>
<p>使用目的为APP提供网络应用安全防护</p>
<p>
收集信息类型设备相关信息例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息、设备所在位置相关信息例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息
</p>
<p>
隐私政策链接:
<span class="link-text">
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
</span>
</p>
<p>11.腾讯bugly</p>
<p>
SDK官网
<span class="link-text">https://bugly.qq.com/v2/</span>
</p>
<p>SDK包名com.tencent.bugly</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的APP异常上报</p>
<p>
收集信息类型:设备及应用信息。如:设备名称、设备识别符、硬件型号、操作系统版本、应用程序版本
</p>
<p>
隐私政策链接:
<span class="link-text">https://bugly.qq.com/v2/contract</span>
</p>
<p>12.阿里云文件上传</p>
<p>
SDK官网
<span class="link-text">https://www.alibabacloud.com/zh</span>
</p>
<p>SDK包名com.alibaba.sdk.android</p>
<p>SDK包名com.alibaba.sdk.android</p>
<p>企业主体:阿里巴巴网络技术有限公司</p>
<p>使用目的:用于支持用户上传视频等相关内容</p>
<p>
收集信息类型设备相关信息例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息、设备所在位置相关信息例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息
</p>
<p>
隐私政策链接:
<span class="link-text">
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
</span>
</p>
<p>13.阿里云日志上传</p>
<p>
SDK官网
<span class="link-text">https://www.alibabacloud.com/zh</span>
</p>
<p>SDK包名com.aliyun.sls.android.sdk</p>
<p>企业主体:阿里巴巴网络技术有限公司</p>
<p>
使用目的:通过网络日志分析这些信息以便更及时响应您的帮助请求,以及用于改进服务
</p>
<p>
收集信息类型设备相关信息例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息、设备所在位置相关信息例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息
</p>
<p>
隐私政策链接:
<span class="link-text">
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
</span>
</p>
<p>14.容联七陌</p>
<p>
SDK官网
<span class="link-text">https://www.7moor.com/developer</span>
</p>
<p>SDK包名com.m7.imkfsdk</p>
<p>企业主体:北京七陌科技有限公司</p>
<p>使用目的:用于提供对应在线客服功能</p>
<p>
收集信息类型:设备相关信息(设备名称、设备型号、硬件序列号、操作系统和应用程序版本及类型、语言设置、分辨率、移动终端随机存储内存、摄像头/相册、通讯录权限等)
</p>
<p>
隐私政策链接:
<span class="link-text">
http://m.7moor.com/72/57/p5077783560e807/
</span>
</p>
<p><b>2.2 共享您的个人信息</b></p>
<p>
1我们不会与任何公司、组织和个人共享您的个人信息但以下情况除外
</p>
<p>
2事先获得您的明确授权或同意
获得您的明确同意后,我们会与其他方共享您的个人信息;
</p>
<p>
3在法定情形下的共享
根据适用的法律法规、法律程序、政府的强制命令或司法裁定而需共享您的个人信息;
</p>
<p>
4在法律要求或允许的范围内为了保护光环助手及其用户或社会公众的利益、财产或安全免遭损害而有必要提供您的个人信息给第三方
</p>
<p>
5与我们的关联公司共享
您的个人信息可能会在我们的关联公司之间共享。我们会对共享的个人信息进行匿名化处理,且这种共享受本指引所声明目的的约束。关联公司如要改变个人信息的处理目的,将再次征求您的授权同意。
</p>
<p><b>2.3转让</b></p>
<p>
1我们不会转让您的个人信息给任何其他第三方除非征得您的明确同意。
</p>
<p>
2随着我们业务的持续发展我们将有可能进行合并、收购、资产转让您的个人信息有可能因此而被转移。在发生前述变更时我们将按照法律法规及不低于本隐私政策所载明的安全标准要求继受方保护您的个人信息否则我们将要求继受方重新征得您的授权同意。
</p>
<p><b>2.4披露</b></p>
<p>
1我们不会公开披露您的信息除非遵循国家法律法规规定或者获得您的同意。我们公开披露您的个人信息会采用符合行业内标准的安全保护措施。
</p>
<p>
2基于法律、法律程序、诉讼或政府主管部门强制性要求的情况下我们可能会向有权机关披露您的个人信息。但我们保证在上述情况发生时我们会要求披露请求方必须出具与之相应的有效法律文件并对被披露的信息采取符合法律和业界标准的安全防护措施。
</p>
<p>
3对违规账号、欺诈行为进行处罚公告时我们会披露相关账号的信息。
</p>
<p><b>2.5依法豁免征得同意共享、转让、公开披露的个人信息</b></p>
<p>
请您理解,在下列情形中,根据法律法规及国家标准,我们共享、转让、公开披露您的个人信息无需征得您的授权同意:
</p>
<p>1与国家安全、国防安全直接相关的</p>
<p>2与公共安全、公共卫生、重大公共利益直接相关的</p>
<p>3与犯罪侦查、起诉、审判和判决执行等直接相关的</p>
<p>
4出于维护您或其他个人的生命、财产等重大合法权益但又很难得到本人同意的
</p>
<p>5您自行向社会公众公开的个人信息</p>
<p>
6从合法公开披露的信息中收集个人信息的如合法的新闻报道、政府信息公开等渠道。
</p>
<p><b>3.我们如何收集和使用个人信息</b></p>
<p>
我们会遵循正当、合法、必要的原则,出于本指引所述的以下目的,收集和使用您在使用服务过程中主动提供或因使用产品或服务而产生的个人信息。
</p>
<p>
我们收集和使用的您的个人信息类型包括两种:第一种:我们产品或服务的核心业务功能所必需的信息:此类信息为产品或服务正常运行的必备信息,您须授权我们收集。如您拒绝提供,您将无法正常使用我们的功能,以"仅浏览(游客身份)"
的状态体验;第二种:我们产品或服务的附加业务功能可能需要收集的信息:此信息为非核心业务功能所需的信息,您可以选择是否授权我们收集。如您拒绝提供,将导致附加业务功能无法实现或无法达到我们拟达到的效果,但不影响您对核心业务功能的正常使用。
</p>
<p>
如果我们要将您的个人信息用于本指引未载明的其它用途,或基于特定目的将收集而来的信息用于其他目的,我们将以合理的方式向您告知,并在使用前再次征得您的同意。
</p>
<p><b>3.1实现产品或服务的基本功能</b></p>
<p>
1手机管理和内容资源下载功能。为实现手机管理及手机内容资源下载的基本功能我们会通过手机系统的公用接口收集经过MD5算法加密的国际移动设备身份码IMEI和网络设备地址MAC以及手机型号、手机系统版本号、系统编号、系统ID号、屏幕分辨率、上网类型、手机中软件的名称、版本号、版本名、包名、软件使用时间和频率、软件崩溃信息、设备和软件相关的信息。这些信息是提供服务所必须收集的基础信息如您拒绝提供上述权限将可能导致您无法使用我们的服务。
</p>
<p>
2软件升级管理功能。为实现手机软件下载、安装、升级、卸载软件管理功能在您使用产品时我们会采集您手机中已安装软件的软件名称、版本号、版本名、软件包名信息并上传到我们的服务器进行软件版本比对。发现有更新的版本我们会提示您升级相应的软件。上述软件信息为实现此功能所必需不涉及您个人身份敏感信息。
</p>
<p>
3过滤无法使用的软件功能。为了过滤您手机无法使用的软件我们会收集您手机的手机型号、手机系统版本号、系统版本号、屏幕分辨率信息并依据这些信息排除您手机无法使用的软件以保证您在光环助手下载的软件都可安装使用。
</p>
<p><b>3.2关于获取手机设备信息的说明</b></p>
<p>
1为方便区分每个用户的个人信息等本软件需获取用户的手机设备信息用于游戏主动预约、论坛互动交流后进行推送等用户相关的行为
</p>
<p>
2为了保障软件与服务的安全运行我们会收集您的硬件型号、操作系统版本号、国际移动设备识别码、唯一设备标识符、网络设备硬件地址、IP
地址、WLAN接入点、蓝牙、基站、软件版本号、网络接入方式、类型、状态、网络质量数据、操作、使用、服务日志。
</p>
<p>
3为了预防恶意程序及安全运营所必需我们会收集安装的应用信息或正在运行的进程信息、应用程序的总体运行、使用情况与频率、应用崩溃情况、总体安装使用情况、性能数据、应用来源。
</p>
<p>
4我们可能使用您的账户信息、设备信息、服务日志信息以及我们关联公司、合作方在获得您授权或依法可以共享的信息用于判断账户安全、进行身份验证、检测及防范安全事件。
</p>
<p>5具体会发生获取手机设备信息场景如下说明</p>
<p class="left-indent">
1 首次启动光环助手
<b></b>
2 游戏列表/游戏详情/资讯文章详情/搜索结果页-预约功能
<b></b>
3 礼包中心/礼包详情-领取功能
<b></b>
4 评论详情-发送评论功能
<b></b>
5 回答/问题详情-我来回答功能
<b></b>
6 问答首页-提问功能
<b></b>
7 个人主页-发文章功能
<b></b>
8 帖子草稿/我的草稿-编辑功能
<b></b>
9 游戏投稿功能
<b></b>
10视频投稿-上传视频功能
<b></b>
11游戏详情-关注游戏功能
</p>
<p><b>3.3帮助您成为我们的在线用户</b></p>
<p>1注册账号/登录账号</p>
<p>
a.当您注册、登录我们相关服务时,您可以通过手机号创建账号,并且您可以完善相关的网络身份识别信息(头像、昵称、密码),收集这些信息是为了帮助您完成注册。您还可以根据自身需求选择填写性别、生日、地区及个人介绍来完善您的信息。
</p>
<p>
b.您也可以使用第三方账号登录并使用,您将授权我们获取您在第三方平台注册的公开信息(头像、昵称以及您授权的其他信息),用于与光环助手账号绑定,使您可以直接登录并使用本产品和相关服务。
</p>
<p>2认证用户</p>
<p>
a.在您使用身份认证的功能或服务时,根据相关法律法规,您可能需要提供您的真实身份信息(真实姓名、身份证号码、电话号码)以完成实名验证。
</p>
<p>
b.这些信息属于个人敏感信息,您可以拒绝提供,但您将可能无法获得相关服务,但不影响其他功能与服务的正常使用。
</p>
<p><b>3.4搜索</b></p>
<p>
1您使用“光环助手”的搜索服务时我们会收集您的搜索关键字信息、日志记录。
</p>
<p>
2为了提供高效的搜索服务部分前述信息会暂时存储在您的本地存储设备之中并可向您展示搜索结果内容、搜索历史记录。
</p>
<p><b>3.5预约游戏</b></p>
<p>
当您使用游戏预约、游戏开测提醒功能时,您可以根据需要是否填写手机号。如您拒绝提供,仅会使您无法接收该预约游戏的短信快速提醒功能,但并不影响您正常使用产品与服务的其他。
</p>
<p><b>3.6游戏时长统计</b></p>
<p>
您可以授权我们使用应用使用记录访问权限,我们会获取您使用某款游戏应用的使用时长,以便于提供游戏时长展示服务以及对应的大数据统计分析。
</p>
<p><b>3.7信息发布功能</b></p>
<p>
1注册成为光环用户后可在光环平台上发布提问、帖子、视频并对别人的提问作出回答或邀请其他用户回答您还可以对别人的回答、帖子和视频的评论作出回复、赞同、感谢。
</p>
<p>
2上述功能基于相册图片库/视频库)的图片/视频访问及上传的附加服务,我们会请求您授权相机、照片、麦克风权限,您可以使用该功能上传您的照片/图片/视频,以实现发布照片/图片/视频的功能、与其他用户进行照片/图片分享等功能。如您拒绝提供该权限和内容的,仅会使您无法使用该功能,但并不影响您正常使用产品与/或服务的其他功能。
</p>
<p>
3您发布内容、评论、提问或回答时我们将收集您发布的信息并展示您的昵称、头像、发布内容。
</p>
<p>
4用户因使用我们的产品或者服务而被我们收集的信息例如其他用户发布的信息中可能含有您的部分信息在评论、留言、发布图文、音视频中涉及到与您相关的信息
</p>
<p><b>3.8浏览、关注与收藏功能</b></p>
<p>1您可浏览的内容包括问答、评论、专栏、文章。</p>
<p>
2在浏览的过程中您还可以关注您感兴趣的用户、专栏、问题、收藏并收藏上述内容。
</p>
<p>
3为此
我们可能会收集您使用时的设备信息,如设备型号、唯一设备标识符、操作系统、分辨率、电信运营商等软硬件信息。
我们还可能收集您的浏览器类型,以此来为您提供信息展示的最优方案。
</p>
<p>
4此外在您使用浏览和收藏功能的过程中我们会自动收集您使用的详细情况并作为有关的
网络日志保存,包括但不限于您输入的搜索关键词信息和点击的链接。
</p>
<p>
5您浏览和发布的内容及评论信息您上传的图片信息、您的交易信息、您使用的语言、访问的日期和时间、及您请求的网页记录、操作系统、软件版本号、登录
IP 信息。
</p>
<p>
6在此过程中
我们会收集您的浏览记录,浏览记录包括您浏览的问答、主页、文章、专栏,
您可以自主删除浏览记录。
</p>
<p><b>3.9互动交流</b></p>
<p>
1您主动关注您感兴趣的账号、内容、视频并与之进行互动进行浏览、评论、收藏、点赞或分享内容时我们会收集您关注的账号并向您展示您关注账号发布内容。
</p>
<p>
2您使用推荐通讯录好友功能时我们会请求通讯录权限并将通讯录中的信息进行高强度加密算法处理后用于向您推荐通信录中的好友。通讯录信息属于个人敏感信息拒绝提供该信息仅会使您无法使用上述功能但不影响您正常使用“光环助手”及相关服务的其他功能。
</p>
<p><b>3.10收集、使用个人信息目的变更</b></p>
<p>
1请您了解随着我们业务的发展可能会对“光环助手”的功能和提供的服务有所调整变化。
</p>
<p>
2原则上当新功能或服务与我们当前提供的功能或服务相关时收集与使用的个人信息将与原处理目的具有直接或合理关联。
</p>
<p>
3在与原处理目的无直接或合理关联的场景下我们收集、使用您的个人信息会再次进行告知并征得您的同意。
</p>
<p><b>3.11依法豁免征得同意收集和使用的个人信息</b></p>
<p>
请您理解,在下列情形中,根据法律法规及相关国家标准,我们收集和使用您的个人信息无需征得您的授权同意:
</p>
<p>1与国家安全、国防安全直接相关的</p>
<p>2与公共安全、公共卫生、重大公共利益直接相关的</p>
<p>3与犯罪侦查、起诉、审判和判决执行等直接相关的</p>
<p>
4出于维护个人信息主体或其他个人的生命、财产等重大合法权益但又很难得到本人同意的
</p>
<p>5所收集的您的个人信息是您自行向社会公众公开的</p>
<p>
6从合法公开披露的信息中收集的您的个人信息的如合法的新闻报道、政府信息公开等渠道
</p>
<p>7根据您的要求签订或履行合同所必需的</p>
<p>
8用于维护软件及相关服务的安全稳定运行所必需的例如发现、处置软件及相关服务的故障
</p>
<p>9为合法的新闻报道所必需的</p>
<p>
10学术研究机构基于公共利益开展统计或学术研究所必要且对外提供学术研究或描述的结果时对结果中所包含的个人信息进行去标识化处理的。
</p>
<p>11法律法规规定的其他情形。</p>
<p>
特别提示您注意,如信息无法单独或结合其他信息识别到您的个人身份,其不属于法律意义上您的个人信息;当您的信息可以单独或结合其他信息识别到您的个人身份时或我们将无法与任何特定个人信息建立联系的数据与其他您的个人信息结合使用时,这些信息在结合使用期间,将作为您的个人信息按照本隐私政策处理与保护。
</p>
<p><b>4.我们如何存储个人信息</b></p>
<p><b>4.1 存储地点</b></p>
<p>
1我们依照法律法规的规定将在境内运营过程中收集和产生的您的个人信息存储于中华人民共和国境内。
</p>
<p>
2目前我们不会将上述信息传输至境外如果我们向境外传输我们将会遵循相关国家规定或者征求您的同意。
</p>
<p><b>4.2存储期限</b></p>
<p>
1我们仅在为提供“光环助手”及服务之目的所必需的期间内保留您的个人信息您发布的信息、评论、点赞及相关信息在您未撤回、删除或未注销账号期间我们会保留相关信息。
</p>
<p>
2超出必要期限后我们将对您的个人信息进行删除或匿名化处理但法律法规另有规定的除外。
</p>
<p><b>5.我们如何保护个人信息的安全</b></p>
<p>
1我们非常重视您个人信息的安全将努力采取合理的安全措施包括技术方面和管理方面来保护您的个人信息防止您提供的个人信息被不当使用或未经授权的情况下被访问、公开披露、使用、修改、损坏、丢失或泄漏。
</p>
<p>
2我们会使用不低于行业同行的加密技术、匿名化处理及相关合理可行的手段保护您的个人信息并使用安全保护机制防止您的个人信息遭到恶意攻击。
</p>
<p>
3我们会建立专门的安全部门、安全管理制度、数据安全流程保障您的个人信息安全。我们采取严格的数据使用和访问制度确保只有授权人员才可访问您的个人信息并适时对数据和技术进行安全审计。
</p>
<p>
4尽管已经采取了上述合理有效措施并已经遵守了相关法律规定要求的标准但请您理解由于技术的限制以及可能存在的各种恶意手段在互联网行业即便竭尽所能加强安全措施也不可能始终保证信息百分之百的安全我们将尽力确保您提供给我们的个人信息的安全性。
</p>
<p>
5您知悉并理解您接入我们的服务所用的系统和通讯网络有可能因我们可控范围外的因素而出现问题。因此我们强烈建议您采取积极措施保护个人信息的安全包括但不限于使用复杂密码、定期修改密码、不将自己的账号密码及相关个人信息透露给他人。
</p>
<p>
6我们会制定应急处理预案并在发生用户信息安全事件时立即启动应急预案努力阻止这些安全事件的影响和后果扩大。一旦发生用户信息安全事件泄露、丢失我们将按照法律法规的要求及时向您告知安全事件的基本情况和可能的影响、我们已经采取或将要采取的处置措施、您可自主防范和降低风险的建议、对您的补救措施。我们将及时将事件相关情况以推送通知、邮件、信函、短信及相关形式告知您难以逐一告知时我们会采取合理、有效的方式发布公告。同时我们还将按照相关监管部门要求上报用户信息安全事件的处置情况。
</p>
<p>
7您一旦离开“光环助手”及相关服务浏览或使用其他网站、服务及内容资源我们将没有能力和直接义务保护您在光环助手及相关服务之外的软件、网站提交的任何个人信息无论您登录、浏览或使用上述软件、网站是否基于“光环助手”的链接或引导。
</p>
<p><b>6.管理您的个人信息</b></p>
<p>
我们非常重视您对个人信息的管理,并尽全力保护您的隐私,对于您个人信息的查询、访问、修改、删除、撤回同意授权、注销账号、投诉举报以及设置隐私功能的相关权利,以使您有能力保障您的隐私和信息安全。
</p>
<p><b>6.1 访问、删除、更正您的个人信息</b></p>
<p>1访问个人账号信息</p>
<p>a. 您可以查询、访问您的头像、用户名、简介、性别、生日、地区</p>
<p>b.您可以在光环助手的“个人中心”中进行查询、访问。</p>
<p>2查询访问、更正、取消您关注账号、查询访问粉丝、访客信息</p>
<p>a.进入“关注”在关注列表中查询、访问、取消关注您关注的账号。</p>
<p>
3查询访问、更改、删除您的收藏、点赞、浏览记录、阅读历史记录、搜索历史历史记录
</p>
<p>
a.点击“我的”—点击“我的收藏”、
“我的点赞”、或“浏览历史”进入查询访问、删除;
</p>
<p>b.点击搜索栏—删除搜索“历史记录”</p>
<p>c.您可以通过点击“系统设置”—点击“清理缓存”。</p>
<p>4投诉举报</p>
<p>a.您可按照我们公示的制度进行投诉或举报。</p>
<p>
b.如果您认为您的个人信息权利可能受到侵害,或者发现侵害个人信息权利的线索(例如:认为我们收集您的个人信息违反法律规定或者双方约定),“我的”—“基础功能”—“用户反馈”,进入用户反馈界面与我们联系。
</p>
<p>c.我们核查后会及时反馈您的投诉与举报。</p>
<p>5访问隐私政策</p>
<p>
a.您可以在注册页面,或者在登录个人账号“设置”—“关于”查看本隐私政策的全部内容
</p>
<p>
b.请您了解,本隐私政策中所述的“光环助手”及相关服务可能会根据您所使用的手机型号、系统版本、软件应用程序版本、移动客户端等因素而有所不同。最终的产品和服务以您所使用的“光环助手”软件及相关服务为准。
</p>
<p>6停止运营向您告知</p>
<p>
a.如我们停止运营,我们将及时停止收集您个人信息的活动,将停止运营的通知以逐一送达或公告的形式通知您,并对所持有的您的个人信息进行删除或匿名化处理。
</p>
<p><b>6.2 注销您的个人账号</b></p>
<p>
如需要注销个人账户,可前往光环助手,我的光环> 设置 > 账号与安全
账号安全中心
注销账号,进行注销操作。请您注意,如果您选择注销光环助手账户,那么您的光环助手账号将不可被使用且相关账号信息将被删除,包括所发布的所有内容,包括:提问、回答、社区文章、评论、关注的人等均会被清空;您将无法再通过光环助手账号登录光环助手的服务(但不会影响您使用无需账号登录即可使用的服务和功能)
</p>
<p>
当您注销账户后除法律法规要求我们保存相关信息的情况外我们将停止为您提供相应的产品或服务并在60个工作日内删除或匿名化您的个人信息。
</p>
<p><b>7.未成年人条款</b></p>
<p>
a.若您是未满18周岁的未成年人在使用“光环助手”及相关服务前应在您的父母或其他监护人监护、指导下共同阅读并同意本隐私政策。
</p>
<p>
b.我们根据国家相关法律法规的规定保护未成年人的个人信息,只会在法律允许、父母或其他监护人明确同意或保护未成年人所必要的情况下收集、使用、储存、共享、转让或披露未成年人的个人信息;如果我们发现在未事先获得可证实的父母同意的情况下收集了未成年人的个人信息,则会设法尽快删除相关信息。
</p>
<p>
c.若您是未成年人的监护人,当您对您所监护的未成年人的个人信息有相关疑问时,请通过公司本隐私政策公示的联系方式与我们联系。
</p>
<p><b>8.隐私政策的修订和通知</b></p>
<p>
1为了给您提供更好的服务光环助手及相关服务将不时更新与变化我们会适时对本隐私政策进行修订这些修订构成本隐私政策的一部分并具有等同于本隐私政策的效力未经您明确同意我们不会削减您依据当前生效的本隐私政策所应享受的权利。
</p>
<p>
2本隐私政策更新后我们会在光环助手发出更新版本并在更新后的条款生效前通过公告或其他适当的方式提醒您更新的内容以便您及时了解本隐私政策的最新版本。
</p>
<p><b>9.联系我们</b></p>
<p>
如果您对我们的隐私政策及对您个人信息的处理有任何疑问、意见、建议、或投诉,请通过以下方式与我们联系
</p>
<p>广州加兔网络科技有限公司</p>
<p>注册地址广州市番禺区市桥街丹山村青云一街2号229房</p>
<p>在线客服QQ350473523</p>
<p>信息保护事务联系电话020-85526920</p>
<p>在一般情况下我们会在15个工作日内对您的请求予以答复</p>
</div>
</div>
</body>
</html>

View File

@ -1,641 +0,0 @@
/**
* Copyright (C) 2017 Wasabeef
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// alert("")
var RE = {};
RE.currentSelection = {
"startContainer": 0,
"startOffset": 0,
"endContainer": 0,
"endOffset": 0};
var isDebug = false;
try {
isDebug = window.NativeCallBack.isNativeBuildDebug()
} catch(error) {
}
// 引用远端的JS 和 CSS
var script = document.createElement("script")
document.body.appendChild(script)
if (isDebug) {
script.src = "https://resource.ghzs.com/js/halo_app_test.js" + "?timestamp=" + Math.round(new Date().getTime() / 1000)
} else {
script.src = "https://resource.ghzs.com/js/halo.js" + "?timestamp=" + Math.round(new Date().getTime() / 1000 / 1000)
}
var style = document.createElement("link")
style.rel = "stylesheet"
style.type = "text/css"
if (isDebug) {
style.href = "https://resource.ghzs.com/css/halo_app_test.css" + "?timestamp=" + Math.round(new Date().getTime() / 1000)
} else {
style.href = "https://resource.ghzs.com/css/halo.css" + "?timestamp=" + Math.round(new Date().getTime() / 1000 / 1000)
}
document.head.appendChild(style)
RE.editor = document.getElementById('editor');
document.addEventListener("selectionchange", function() { RE.backuprange(); });
// Initializations
RE.callback = function() {
window.location.href = "re-callback://" + encodeURIComponent(RE.getHtml());
}
RE.setHtml = function(contents) {
RE.editor.innerHTML = decodeURIComponent(contents.replace(/\+/g, '%20'));
}
// 后续初始化html代码,都用该方法
RE.setHtmlByVideoStatus = function(contents) {
RE.editor.innerHTML = decodeURIComponent(contents.replace(/\+/g, '%20'));
}
// Deprecated
RE.getHtml = function() {
return RE.editor.innerHTML;
}
RE.getText = function() {
return RE.editor.innerText;
}
RE.setBaseTextColor = function(color) {
RE.editor.style.color = color;
}
RE.setBaseFontSize = function(size) {
RE.editor.style.fontSize = size;
}
RE.setPadding = function(left, top, right, bottom) {
RE.editor.style.paddingLeft = left;
RE.editor.style.paddingTop = top;
RE.editor.style.paddingRight = right;
RE.editor.style.paddingBottom = bottom;
}
RE.setBackgroundColor = function(color) {
document.body.style.backgroundColor = color;
}
RE.setBackgroundImage = function(image) {
RE.editor.style.backgroundImage = image;
}
RE.setWidth = function(size) {
RE.editor.style.minWidth = size;
}
RE.setHeight = function(size) {
RE.editor.style.height = size;
}
RE.setTextAlign = function(align) {
RE.editor.style.textAlign = align;
}
RE.setVerticalAlign = function(align) {
RE.editor.style.verticalAlign = align;
}
RE.setPlaceholder = function(placeholder) {
RE.editor.setAttribute("placeholder", placeholder);
}
RE.setEditorFocus = function() {
RE.editor.focus();
}
RE.setInputEnabled = function(inputEnabled) {
RE.editor.contentEditable = String(inputEnabled);
}
RE.formatBlock = function() {
document.execCommand('formatBlock', false, 'p');
}
RE.undo = function() {
document.execCommand('undo', false, null);
}
RE.redo = function() {
document.execCommand('redo', false, null);
}
RE.setBold = function() {
document.execCommand('bold', false, null);
}
RE.setItalic = function() {
document.execCommand('italic', false, null);
}
RE.setSubscript = function() {
document.execCommand('subscript', false, null);
}
RE.setSuperscript = function() {
document.execCommand('superscript', false, null);
}
RE.setStrikeThrough = function() {
document.execCommand('strikeThrough', false, null);
}
RE.setUnderline = function() {
document.execCommand('underline', false, null);
}
RE.setBullets = function() {
document.execCommand('insertUnorderedList', false, null);
}
RE.setNumbers = function() {
document.execCommand('insertOrderedList', false, null);
}
RE.setTextColor = function(color) {
RE.restorerange();
document.execCommand("styleWithCSS", null, true);
document.execCommand('foreColor', false, color);
document.execCommand("styleWithCSS", null, false);
}
RE.setTextBackgroundColor = function(color) {
RE.restorerange();
document.execCommand("styleWithCSS", null, true);
document.execCommand('hiliteColor', false, color);
document.execCommand("styleWithCSS", null, false);
}
RE.setFontSize = function(fontSize){
document.execCommand("fontSize", false, fontSize);
}
RE.setHeading = function(heading) {
document.execCommand('formatBlock', false, '<h'+heading+'>');
RE.sendElementNameToNative()
}
RE.setIndent = function() {
document.execCommand('indent', false, null);
}
RE.setOutdent = function() {
document.execCommand('outdent', false, null);
}
RE.setJustifyLeft = function() {
document.execCommand('justifyLeft', false, null);
}
RE.setJustifyCenter = function() {
document.execCommand('justifyCenter', false, null);
}
RE.setJustifyRight = function() {
document.execCommand('justifyRight', false, null);
}
RE.setBlockquote = function() {
document.execCommand('formatBlock', false, '<blockquote>');
// var blockId = window.getSelection().focusNode.parentNode;
// $(blockId).addClass("haloBlock")
RE.sendElementNameToNative()
}
RE.insertImage = function(url) {
var html = "<div><img src =\"" + url + "\" style=\" max-width: 100%; display:block; margin:15px auto; height: auto;\"></div><br>"
RE.insertHTML(html);
}
// 替换成缩略图
RE.replaceTbImage = function(imgRuleFlag, gifRuleFlag) {
var imgs = document.getElementsByTagName("img");
var index = 0
for (var i = 0; i < imgs.length; i++) {
var img = imgs[i];
var imageClassName = img.className;
// console.log(imageClassName)
if (imageClassName == "image-link" || img.className == "poster") continue;
if(img.src.indexOf("?") > 0) continue;
// console.log(i)
var tbImg
if(img.src.indexOf(".gif") > 0) {
tbImg = img.src + gifRuleFlag
} else {
tbImg = img.src + imgRuleFlag
}
img.style.cssText = "max-width: 60%; display:block; margin:15px auto; height: auto;"
img.src = tbImg;
if (index == 0) {
var bigImg = document.createElement('img');
bigImg.src = "file:///android_asset/web_load_dfimg_icon.png";
bigImg.style.cssText = "max-width: 20%; margin:15px 0 0 0; height: auto;"
img.parentNode.insertBefore(bigImg, img.parentNode.childNodes[0]);
i++;
if(img.parentNode != null) {
img.parentNode.style.cssText += "text-align: left;"
}
if(img.parentNode != null && img.parentNode.parentNode != null) {
img.parentNode.parentNode.style.cssText += "text-align: left;"
}
}
index ++;
}
}
// 替换成默认图
RE.replaceAllDfImage = function(imgRuleFlag, gifRuleFlag) {
var imgs = document.getElementsByTagName("img");
for (var i = 0; i < imgs.length; i++) {
var img = imgs[i];
var imageClassName = img.className;
if (imageClassName == "image-link" || img.className == "poster") continue;
if(img.src.indexOf("web_load_dfimg_icon") > 0) {
img.parentNode.removeChild(img.parentNode.childNodes[0]);
i--;
} else {
if(img.src.indexOf(".gif") > 0) {
if(gifRuleFlag.indexOf(",default") > 0) {
img.style.cssText = "max-width: 100%; display:block; margin:8px auto; height: auto;"
img.src = img.src.split("?")[0] + gifRuleFlag
}
} else {
img.style.cssText = "max-width: 100%; display:block; margin:8px auto; height: auto;"
img.src = img.src.split("?")[0] + imgRuleFlag
}
}
}
}
// 去除显示大图
RE.hideShowBigPic = function() {
var imgs = document.getElementsByTagName("img");
var j = 0;
for (var i = 0; i < imgs.length; i++) {
var img = imgs[i];
var imageClassName = img.className;
if (imageClassName == "image-link" || img.className == "poster") continue;
if(img.src.indexOf(",thumbnail") > 0 && img.src.indexOf(".gif") == -1) {
j++;
}
}
// 去除显示大图
if (j == 0) {
var imgs = document.getElementsByTagName("img");
for (var i = 0; i < imgs.length; i++) {
var img = imgs[i];
var imageClassName = img.className;
if (imageClassName == "image-link" || img.className == "poster") continue;
if(img.src.indexOf("web_load_dfimg_icon") > 0) {
img.parentNode.removeChild(img.parentNode.childNodes[0]);
break;
}
}
}
}
RE.replaceDfImageByUrl = function(imgUrl, imgRuleFlag, gifRuleFlag) {
var imgs = document.getElementsByTagName("img");
for (var i = 0; i < imgs.length; i++) {
var img = imgs[i];
var imageClassName = img.className;
if (imageClassName == "image-link" || img.className == "poster") continue;
if (img.src.indexOf(imgUrl) != -1) {
img.style.cssText = "max-width: 100%; display:block; margin:8px auto; height: auto;"
if(img.src.indexOf(".gif") > 0) {
img.src = img.src.split("?")[0] + gifRuleFlag
} else {
img.src = img.src.split("?")[0] + imgRuleFlag
}
}
}
RE.hideShowBigPic();
}
RE.ImageClickListener = function() {
var imgs = document.getElementsByTagName("img");
for (var i = 0; i < imgs.length; i++) {
var img = imgs[i];
var imageClassName = img.className;
if (imageClassName == "image-link"|| img.className == "poster") continue;
window.imagelistener.imageArr(img.src);
img.onclick = function() {
window.imagelistener.imageClick(this.src);
}
}
}
RE.insertHTML = function(html) {
RE.restorerange();
document.execCommand('insertHTML', false, html);
}
RE.insertLink = function(url, title) {
RE.restorerange();
var sel = document.getSelection();
if (sel.toString().length == 0) {
document.execCommand("insertHTML",false,"<a href='"+url+"'>"+title+"</a>");
} else if (sel.rangeCount) {
var el = document.createElement("a");
el.setAttribute("href", url);
el.setAttribute("title", title);
var range = sel.getRangeAt(0).cloneRange();
range.surroundContents(el);
sel.removeAllRanges();
sel.addRange(range);
}
RE.callback();
}
RE.setTodo = function(text) {
var html = '<input type="checkbox" name="'+ text +'" value="'+ text +'"/> &nbsp;';
document.execCommand('insertHTML', false, html);
}
RE.prepareInsert = function() {
RE.backuprange();
}
RE.backuprange = function(){
var selection = window.getSelection();
if (selection.rangeCount > 0) {
var range = selection.getRangeAt(0);
RE.currentSelection = {
"startContainer": range.startContainer,
"startOffset": range.startOffset,
"endContainer": range.endContainer,
"endOffset": range.endOffset};
}
}
RE.restorerange = function(){
try {
var selection = window.getSelection();
selection.removeAllRanges();
var range = document.createRange();
range.setStart(RE.currentSelection.startContainer, RE.currentSelection.startOffset);
range.setEnd(RE.currentSelection.endContainer, RE.currentSelection.endOffset);
selection.addRange(range);
} catch(error) {
}
}
RE.enabledEditingItems = function(e) {
var items = [];
if (document.queryCommandState('bold')) {
items.push('bold');
}
if (document.queryCommandState('italic')) {
items.push('italic');
}
if (document.queryCommandState('subscript')) {
items.push('subscript');
}
if (document.queryCommandState('superscript')) {
items.push('superscript');
}
if (document.queryCommandState('strikeThrough')) {
items.push('strikeThrough');
}
if (document.queryCommandState('underline')) {
items.push('underline');
}
if (document.queryCommandState('insertOrderedList')) {
items.push('orderedList');
}
if (document.queryCommandState('insertUnorderedList')) {
items.push('unorderedList');
}
if (document.queryCommandState('justifyCenter')) {
items.push('justifyCenter');
}
if (document.queryCommandState('justifyFull')) {
items.push('justifyFull');
}
if (document.queryCommandState('justifyLeft')) {
items.push('justifyLeft');
}
if (document.queryCommandState('justifyRight')) {
items.push('justifyRight');
}
if (document.queryCommandState('insertHorizontalRule')) {
items.push('horizontalRule');
}
var formatBlock = document.queryCommandValue('formatBlock');
if (formatBlock.length > 0) {
items.push(formatBlock);
}
window.location.href = "re-state://" + encodeURI(items.join(','));
}
RE.focus = function() {
var range = document.createRange();
range.selectNodeContents(RE.editor);
range.collapse(false);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
RE.editor.focus();
}
RE.blurFocus = function() {
RE.editor.blur();
}
RE.removeFormat = function() {
document.execCommand('removeFormat', false, null);
}
RE.insertCustomStyleLink = function(data) {
var entity = JSON.parse(data)
var html = "<br/><div class='"+ entity.type +"-container'>\n" +
" <a class='"+ entity.type +"' href=\"javascript:void(0);\" contenteditable=\"false\" onclick=\"customLinkgo(this)\" data-datas='"+ data +"'>\n" +
" <div class='flex-container'>\n" +
" <div class='gh-internal-content img-left'>\n" +
" <img class = \"image-link\" src='"+ entity.icon +"' />\n" +
" </div>\n" +
" <div class='gh-internal-content content-right'>\n" +
" <p class='content-title'>"+ entity.title +"</p>\n" +
" <p class='contents'>"+ entity.brief +"</p>\n" +
" </div>\n" +
" </div>\n" +
" </a>\n" +
" </div><br/>"
var tags = "", gameHtml = ""
if (entity.tags != null) {
for (var i = 0; i < entity.tags.length; i++) {
tags += "<label>"+ entity.tags[i]+"</label>"
}
gameHtml = "<br/><div class='"+ entity.type +"-container'>\n" +
" <a class='"+ entity.type +"' href=\"javascript:void(0);\" contenteditable=\"false\" onclick=\"customLinkgo(this)\" data-datas='"+ data +"'>\n" +
" <div class='flex-container'>\n" +
" <div class='gh-internal-content img-left'>\n" +
" <img class='image-link' src='"+ entity.icon +"' />\n" +
" </div>\n" +
" <div class='gh-internal-content content-right'>\n" +
" <p class='content-title'>"+ entity.title +"</p>\n" +
" <p class='tags'>"+ tags +"</p>\n" +
" </div>\n" +
" </div>\n" +
" </a></div><br/>"
}
switch(entity.type) {
case "answer":
document.execCommand("insertHTML",false, html);
break
case "community_article":
document.execCommand("insertHTML",false, html);
break
case "game":
document.execCommand("insertHTML",false, gameHtml);
break
}
RE.callback();
}
RE.showLinkStyle = function() {
var answerElement = document.getElementsByClassName("answer-container");
for (var i=0;i<answerElement.length;i+=1){
answerElement[i].style.display = 'inline';
}
var articleElement = document.getElementsByClassName("community_article-container");
for (var i=0;i<articleElement.length;i+=1){
articleElement[i].style.display = 'inline';
}
var gameElement = document.getElementsByClassName("game-container");
for (var i=0;i<gameElement.length;i+=1){
gameElement[i].style.display = 'inline';
}
}
RE.hideLinkStyle = function() {
var answerElement = document.getElementsByClassName("answer-container");
for (var i=0;i<answerElement.length;i+=1){
answerElement[i].style.display = 'none';
}
var articleElement = document.getElementsByClassName("community_article-container");
for (var i=0;i<articleElement.length;i+=1){
articleElement[i].style.display = 'none';
}
var gameElement = document.getElementsByClassName("game-container");
for (var i=0;i<gameElement.length;i+=1){
gameElement[i].style.display = 'none';
}
}
// Event Listeners
RE.editor.addEventListener("input", RE.callback);
RE.editor.addEventListener("keyup", function(e) {
var KEY_LEFT = 37, KEY_RIGHT = 39;
if (e.which == KEY_LEFT || e.which == KEY_RIGHT) {
RE.enabledEditingItems(e);
}
RE.sendElementNameToNative()
});
RE.editor.addEventListener("click", function(e) {
RE.enabledEditingItems
RE.sendElementNameToNative()
var s = document.getSelection()
var isNeedRemoveR = RE.recursion(e.target)
if (isNeedRemoveR && s.rangeCount) {
s.removeAllRanges()
}
});
document.addEventListener("selectionchange", function(e) {
RE.sendElementNameToNative()
});
RE.recursion = function(dom) {
var parenDom = dom.parentElement
if (parenDom && parenDom instanceof Element &&
typeClassList.indexOf(parenDom.className) > -1) {
return parenDom
} else if(parenDom && parenDom instanceof Element &&
typeClassList.indexOf(parenDom.className) === -1 && parenDom.nodeName !== 'BODY') {
return RE.recursion(parenDom)
} else {
return null
}
}
// 返回组件标签 多个标签以"空格"划分
RE.sendElementNameToNative = function() {
if (window.getSelection) {
var selection = window.getSelection()
if (selection.rangeCount > 0) {
var range = selection.getRangeAt(0);
var container = range.startContainer;
var elements = " " + container.localName + " ";
var parentElement;
while(true) {
if(parentElement != null) {
parentElement = parentElement.parentElement
} else {
parentElement = container.parentElement
}
if (parentElement == null || parentElement.localName == null) {
break;
}
elements = elements + " " + parentElement.localName + " "
}
// console.log(elements)
window.OnCursorChangeListener.onElements(elements);
}
}
}
// android function to open link
function customLinkgo(self) {
var datas = self.dataset.datas
// console.log(datas)
window.OnLinkClickListener.onClick(datas)
}
// 在web页面播放视频
//RE.initArticleVideo = function(){
// initArticleVideo()
//}
function showNativeDialog(title, message, positive, negative, callback) {
var jsCallbackCode = "(" + function (v) {
window.onNativeDialogCallback(v);
delete window.onNativeDialogCallback;
}.toString() + ")";
window.onNativeDialogCallback = callback;
window.NativeCallBack.showDialog(title, message, positive, negative, jsCallbackCode);
}

View File

@ -1,43 +0,0 @@
/**
* Copyright (C) 2017 Wasabeef
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@charset "UTF-8";
html {
height: 100%;
}
body {
overflow: scroll;
display: table;
table-layout: fixed;
width: 100%;
min-height:100%;
}
#editor {
display: table-cell;
outline: 0px solid transparent;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
#editor[placeholder]:empty:not(:focus):before {
content: attr(placeholder);
opacity: .5;
}}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,493 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>光环助手软件许可及服务协议</title>
</head>
<style>
* {
margin: 0;
padding: 0;
font-size: 14px;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
}
body {
margin: 10px;
}
.top {
margin-left: 0;
margin-right: 0;
padding: 10px 0 10px 0;
}
.title {
font-weight: 700;
}
p {
font-size: 14px;
word-break: break-all;
}
.bold {
font-weight: 700;
}
.margintop {
margin-top: 10px;
}
.left-indent {
margin-left: 20px;
}
.red-style {
color: red;
}
.bold-font {
font-weight: bold;
}
span.bold {
font-weight: bold;
}
.link-text {
color: #005ad0;
text-decoration: underline;
}
</style>
<body>
<h3 class="top">光环助手软件许可及服务协议</h3>
<h5 class="title">首部及导言</h5>
<p>欢迎使用光环助手软件许可及服务</p>
<p>
各位用户在使用光环助手前,请您务必审慎阅读、并充分理解本协议中的各项条款,
<span class="bold">
特别是免除或者限制责任的条款,以及开通或使用某项服务的单独协议,并选择接受或不接受。
</span>
除非您已阅读并接受本协议所有条款,否则您无权下载、安装或使用本软件及相关服务。您的下载、安装、使用、登录等行为即视为您已阅读并同意上述协议的约束。
</p>
<p>如果您未满18周岁请在法定监护人的陪同下阅读本协议及其他上述协议。</p>
<h5 class="title margintop">一、权利声明</h5>
<p>
“光环助手”的一切知识产权,以及与“光环助手”相关的所有信息内容,包括但不限于:文字表述及其组合、图标、图饰、图像、图表、色彩、界面设计、版面框架、有关数据、附加程序、印刷材料或电子文档等均为光环助手所有,受著作权法和国际著作权条约以及其他知识产权法律法规的保护。
</p>
<h5 class="title margintop">二、软件使用规范</h5>
<p>
2.1
本软件是基于Android安卓系统手机、平板电脑(PAD)等设备开发的一款软件,提供注册登录、手机游戏管理、游戏推荐、文章阅读等功能
</p>
<p>2.2 软件的下载、安装和使用</p>
<p>
本软件为免费软件,用户可以非商业性、无限制数量地从光环授权的渠道下载、安装及使用本软件。
</p>
<p>
<span class="bold">
如果您从未经光环授权的第三方获取本软件或与本软件名称相同的安装程序,光环无法保证该软件能够正常使用,并对因此给您造成的损失不予负责。
</span>
</p>
<p>2.3 软件的复制、分发和传播</p>
<p>
本产品以学习、研究交流为目的。用户可以非商业性、无限制数量地复制、分发和传播本软件产品。但必须保证每一份复制、分发和传播都是完整和真实的,
包括所有有关本软件产品的软件、电子文档, 版权和商标,亦包括本协议。
</p>
<p>2.4 软件的更新</p>
<p>
为了改善用户体验、完善服务内容,光环将不断努力开发新的服务,并为您不时提供软件更新(这些更新可能会采取软件替换、修改、功能强化、版本升级等形式)。为了保证本软件及服务的安全性和功能的一致性,光环有权不经向您特别通知而对软件进行更新,或者对软件的部分功能效果进行改变或限制。本软件新版本发布后,旧版本的软件可能无法使用。光环不保证旧版本软件继续可用及相应的客户服务,请您随时核对并下载最新版本。
</p>
<h5 class="title margintop">三、用户使用须知</h5>
<p>3.1 您理解并同意:</p>
<p>
为了向您提供有效的服务,本软件会利用您移动通讯终端的处理器和带宽等资源。本软件使用过程中可能产生数据流量的费用,用户需自行向运营商了解相关资费信息,并自行承担相关费用.
</p>
<p>3.2 您理解并同意:</p>
<p>
由本软件进行收录、推荐并提供下载、升级服务的第三方软件,由第三方享有一切合法权利,光环并不能识别用户利用本软件下载、安装的第三方软件是否有合法来源。
<span class="bold">
因第三方软件引发的任何纠纷,由该第三方负责解决,光环不承担任何责任。
</span>
同时光环不对第三方软件或技术提供客服支持,若用户需要获取支持,请与该软件或技术提供商联系,若您为有关软件的权利人,不愿本软件为您的软件提供用户下载、安装、使用的服务,也可按本协议约定的联系方式联系我们,我们将会积极配合进行处理。
</p>
<p>3.3 您理解并同意:</p>
<p>
<span class="bold">
如果因您不正当使用本软件造成了不良影响,或因使用本软件造成的包括但不限于数据异常等问题,均由使用者自行承担,光环团队不对任意类型的使用结果承担责任;
</span>
</p>
<p>3.4 您理解并同意:</p>
<p>
本软件不含任何破坏用户移动通讯设备数据和获取用户隐私信息的恶意代码,不会泄露用户的个人信息和隐私;
</p>
<p>3.5 您理解并同意:</p>
<p>
<span class="bold">
对于包括但不限于互联网网络故障、计算机故障、手机故障或病毒、信息损坏或丢失、计算机系统问题,或其它任何基于不可抗力原因而产生的损失,光环团队不承担任何责任。
</span>
</p>
<p>3.6 您理解并同意:</p>
<p>光环发布、收录的文章均不代表光环立场。</p>
<p>3.7 您理解并同意:</p>
<p>
为实现软件包括但不限于集中展示、下载、安装、卸载等游戏管理功能以及文章优先推荐功能,本软件会检测用户手机中已安装游戏的包名、版本号、版本名、游戏名称信息。除征得用户明确同意和法律明确规定外,光环不会向第三方泄露任何的用户信息
</p>
<p>3.8 您理解并同意:</p>
<p>
用户应在遵守法律及本协议的前提下使用本软件。用户无权实施包括但不限于下列行为:
</p>
3.8.1 不得删除或者改变本软件上的所有权利管理电子信息
<br />
3.8.2 不得故意避开或者破坏著作权人为保护本软件著作权而采取的技术措施;
<br />
3.8.3 用户不得利用本软件误导、欺骗他人;
<br />
3.8.4
违反国家规定,对计算机信息系统功能进行删除、修改、增加、干扰,造成计算机信息系统不能正常运行;
<br />
3.8.5 未经允许,进入计算机信息网络或者使用计算机信息网络资源;
<br />
3.8.6 未经允许,对计算机信息网络功能进行删除、修改或者增加;
<br />
3.8.7
未经允许,对计算机信息网络中存储、处理或者传输的数据和应用程序进行删除、修改或者增加;
<br />
3.8.8 破坏本软件系统或网站的正常运行,故意传播计算机病毒等破坏性程序;
<br />
3.8.9 其他任何危害计算机信息网络安全的行为。
<br />
<p>3.9 您理解并同意:</p>
<p>
本软件经过详细的测试,但不能保证与所有的软硬件系统完全兼容,不能保证本软件完全没有错误。如果出现不兼容及软件错误的情况,用户可通过各反馈途径将情况告知光环团队,获得技术支持。如果无法解决兼容性问题,用户可以删除本软件。
</p>
<h5 class="title margintop">四、争议解决处理</h5>
<p>
本《协议》的解释、效力及纠纷的解决,适用于中华人民共和国法律。若用户和光环助手之间发生任何纠纷或争议,首先应友好协商解决,协商不成的,用户在此完全同意将纠纷或争议提交光环助手所在地法院管辖
</p>
<p class="title margintop"><b>五、第三方SDK接入说明</b></p>
<p>
为保障光环助手App相关功能的实现与应用安全稳定的运行我们会接入由第三方提供的软件开发包SDK实现相关功能。
<br />
我们会对合作方获取有关信息的软件工具开发包SDK进行严格的安全检测并与授权合作伙伴约定严格的数据保护措施令其按照我们的委托目的、服务说明、本隐私权政策以及其他任何相关的保密和安全措施来处理个人信息。
<br />
<span class="red-style">
下方为整个光环助手
<span class="bold">所有版本</span>
内接入的所有信息收集类第三方SDK的权限说明因隐私政策会因光环助手版本迭代而新接入SDK或停止合作部分SDK方便照顾
<span class="bold">所有版本</span>
的用户查看自己SDK第三方权限说明。
<br />
我们对涉及用户信息使用的SDK相关情况进行了逐项列举具体如下
</span>
</p>
<p class="margintop red-style bold-font"><b>1数据统计类</b></p>
<p>1.头条推广</p>
<p>
SDK官网
<span class="link-text">
https://ad.oceanengine.com/openapi/index.html
</span>
</p>
<p>SDK包名com.bytedance</p>
<p>企业主体:北京有竹居网络技术有限公司</p>
<p>使用目的:用于广告流量统计相关服务</p>
<p>
收集信息类型设备品牌、型号、软件系统相关信息、安卓oaid、无线网SSID名称、WiFi路由器MAC地址、设备MAC地址、IMEI、地理位置
</p>
<p>
隐私政策链接:
<span class="link-text">
https://ad.oceanengine.com/openapi/register/protocol.html?rid=vo25p8sfqde
</span>
</p>
<p>2.talkingdata统计</p>
<p>
SDK官网
<span class="link-text">http://www.talkingdata.com/</span>
</p>
<p>SDK包名com.tendcloud</p>
<p>企业主体:北京腾云天下科技有限公司</p>
<p>使用目的:用于统计数据和效果分析,以便为用户提供更好的服务</p>
<p>收集信息类型:设备信息、网络信息、位置信息、应用信息</p>
<p>
隐私政策链接:
<span class="link-text">
http://www.talkingdata.com/privacy.jsp?languagetype=zh_cn
</span>
</p>
<p>3.腾讯MTA</p>
<p>
SDK官网
<span class="link-text">https://mta.qq.com/mta/</span>
</p>
<p>SDK包名com.tencent</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的:用于统计数据和效果分析</p>
<p>
收集信息类型Mac地址、唯一设备识别码IMEI、android
ID、IDFA、OPENUDID、GUID/SIM卡IMSI信息、地理位置信息
</p>
<p>
隐私政策链接:
<span class="link-text">
https://mta.qq.com/mta/ctr_index/protocol_v2/
</span>
</p>
<p>4.腾讯广点通</p>
<p>
SDK官网
<span class="link-text">https://developers.e.qq.com/</span>
</p>
<p>SDK包名com.tencent</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的:用于广告流量统计相关服务</p>
<p>
收集信息类型:
个人常用设备信息IMEI、AndroidID、位置信息IP地址、软件版本号
</p>
<p>
隐私政策链接:
<span class="link-text">https://e.qq.com/optout.html</span>
</p>
<p class="margintop red-style bold-font"><b>2社交登录类</b></p>
<p>5.微信登录分享</p>
<p>
SDK官网
<span class="link-text">https://open.weixin.qq.com/</span>
</p>
<p>SDK包名com.tencent.mm.opensdk</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的:用于支持微信登录、分享</p>
<p>
收集信息类型个人常用设备信息MAC地址、IMEI、AndroidID、硬件型号、操作系统类型、软件信息软件版本号、浏览器类型、IP地址、服务日志信息、通讯日志信息
</p>
<p>
隐私政策链接:
<span class="link-text">https://privacy.tencent.com/</span>
</p>
<p>6.QQ登录分享</p>
<p>
SDK官网
<span class="link-text">https://connect.qq.com/</span>
</p>
<p>SDK包名com.tentcent</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的用于支持QQ登录、分享</p>
<p>
收集信息类型个人常用设备信息MAC地址、IMEI、AndroidID、IMSI、ICCID、序列号、设备型号、操作系统版本、软件信息软件版本号、浏览器类型、网络信息、IP地址、服务日志信息、通讯日志信息 
</p>
<p>
隐私政策链接:
<span class="link-text">
https://wiki.connect.qq.com/qq互联sdk隐私保护声明
</span>
</p>
<p>7.微博登录分享</p>
<p>
SDK官网
<span class="link-text">http://open.weibo.com/authentication</span>
</p>
<p>SDK包名com.sina.weibo.sdk</p>
<p>企业主体:北京微梦创科网络技术有限公司</p>
<p>使用目的:用于支持微博登录、分享</p>
<p>
收集信息类型个人常用设备信息MAC地址、IMEI、AndroidID、IMSI、ICCID、序列号、网络信息、应用列表硬件型号、操作系统类型、软件信息软件版本号、浏览器类型、IP地址、服务日志信息、通讯日志信息
</p>
<p>
隐私政策链接:
<span class="link-text">https://open.weibo.com/wiki/开发者协议</span>
</p>
<p>8.头条抖音登录</p>
<p>
SDK官网
<span class="link-text">https://open.douyin.com/platform</span>
</p>
<p>SDK包名com.bytedance.sdk</p>
<p>企业主体:北京字节跳动科技有限公司</p>
<p>使用目的:用于支持抖音登录</p>
<p>
收集信息类型个人常用设备信息MAC地址、IMEI、AndroidID、硬件型号、操作系统类型、软件信息软件版本号、浏览器类型、IP地址、服务日志信息、通讯日志信息
</p>
<p>
隐私政策链接:
<span class="link-text">
https://www.douyin.com/agreements/?id=6773901168964798477
</span>
</p>
<p class="margintop red-style bold-font"><b>3推送通知类</b></p>
<p>9.友盟推送</p>
<p>
SDK官网
<span class="link-text">https://www.umeng.com/push</span>
</p>
<p>SDK包名com.umeng</p>
<p>企业主体:北京友盟网络科技有限公司</p>
<p>使用目的:用于游戏相关信息的提醒通知</p>
<p>
收集信息类型Mac地址、唯一设备识别码IMEI、android
ID、IDFA、OPENUDID、GUID/SIM卡IMSI信息、地理位置信息
</p>
<p>
隐私政策链接:
<span class="link-text">
https://www.umeng.com/page/policy?spm=a213m0.14063960.0.0.7f626e72hx3nnv
</span>
</p>
<p class="margintop red-style bold-font"><b>4其他功能类</b></p>
<p>10.阿里云反爬虫</p>
<p>
SDK官网
<span class="link-text">https://www.aliyun.com/product/antibot</span>
</p>
<p>SDK包名com.alibaba.wireless</p>
<p>企业主体:阿里巴巴网络技术有限公司</p>
<p>使用目的为APP提供网络应用安全防护</p>
<p>
收集信息类型设备相关信息例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息、设备所在位置相关信息例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息
</p>
<p>
隐私政策链接:
<span class="link-text">
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
</span>
</p>
<p>11.腾讯bugly</p>
<p>
SDK官网
<span class="link-text">https://bugly.qq.com/v2/</span>
</p>
<p>SDK包名com.tencent.bugly</p>
<p>企业主体:深圳市腾讯计算机系统有限公司</p>
<p>使用目的APP异常上报</p>
<p>
收集信息类型:设备及应用信息。如:设备名称、设备识别符、硬件型号、操作系统版本、应用程序版本
</p>
<p>
隐私政策链接:
<span class="link-text">https://bugly.qq.com/v2/contract</span>
</p>
<p>12.阿里云文件上传</p>
<p>
SDK官网
<span class="link-text">https://www.alibabacloud.com/zh</span>
</p>
<p>SDK包名com.alibaba.sdk.android</p>
<p>SDK包名com.alibaba.sdk.android</p>
<p>企业主体:阿里巴巴网络技术有限公司</p>
<p>使用目的:用于支持用户上传视频等相关内容</p>
<p>
收集信息类型设备相关信息例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息、设备所在位置相关信息例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息
</p>
<p>
隐私政策链接:
<span class="link-text">
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
</span>
</p>
<p>13.阿里云日志上传</p>
<p>
SDK官网
<span class="link-text">https://www.alibabacloud.com/zh</span>
</p>
<p>SDK包名com.aliyun.sls.android.sdk</p>
<p>企业主体:阿里巴巴网络技术有限公司</p>
<p>
使用目的:通过网络日志分析这些信息以便更及时响应您的帮助请求,以及用于改进服务
</p>
<p>
收集信息类型设备相关信息例如设备型号、操作系统版本、设备设置、唯一设备标识符等软硬件特征信息、设备所在位置相关信息例如IP地址、GPS位置以及能够提供相关信息的Wi-Fi接入点、蓝牙和基站等传感器信息
</p>
<p>
隐私政策链接:
<span class="link-text">
http://terms.aliyun.com/legal-agreement/terms/suit_bu1_ali_cloud/suit_bu1_ali_cloud201902141711_54837.html?spm=a2c4g.11186623.J_9220772140.81.b7574832gmk0vr
</span>
</p>
<p>14.容联七陌</p>
<p>
SDK官网
<span class="link-text">https://www.7moor.com/developer</span>
</p>
<p>SDK包名com.m7.imkfsdk</p>
<p>企业主体:北京七陌科技有限公司</p>
<p>使用目的:用于提供对应在线客服功能</p>
<p>
收集信息类型:设备相关信息(设备名称、设备型号、硬件序列号、操作系统和应用程序版本及类型、语言设置、分辨率、移动终端随机存储内存、摄像头/相册、通讯录权限等)
</p>
<p>
隐私政策链接:
<span class="link-text">http://m.7moor.com/72/57/p5077783560e807/</span>
</p>
<h5 class="title margintop">六、关于获取手机设备信息的说明</h5>
<div>
1为方便区分每个用户的个人信息等本软件需获取用户的手机设备信息用于游戏主动预约、论坛互动交流后进行推送等用户相关的行为
<br />
2为了保障软件与服务的安全运行我们会收集您的硬件型号、操作系统版本号、国际移动设备识别码、唯一设备标识符、网络设备硬件地址、IP
地址、WLAN接入点、蓝牙、基站、软件版本号、网络接入方式、类型、状态、网络质量数据、操作、使用、服务日志。
<br />
3为了预防恶意程序及安全运营所必需我们会收集安装的应用信息或正在运行的进程信息、应用程序的总体运行、使用情况与频率、应用崩溃情况、总体安装使用情况、性能数据、应用来源。
<br />
4我们可能使用您的账户信息、设备信息、服务日志信息以及我们关联公司、合作方在获得您授权或依法可以共享的信息用于判断账户安全、进行身份验证、检测及防范安全事件。
<br />
5具体会发生获取手机设备信息场景如下说明
<br />
<p class="left-indent">
1) 首次启动光环助手
<br />
2) 游戏列表/游戏详情/资讯文章详情/搜索结果页-预约功能
<br />
3) 礼包中心/礼包详情-领取功能
<br />
4) 评论详情-发送评论功能
<br />
5) 回答/问题详情-我来回答功能
<br />
6) 问答首页-提问功能
<br />
7) 个人主页-发文章功能
<br />
8) 帖子草稿/我的草稿-编辑功能
<br />
9) 游戏投稿功能
<br />
10) 视频投稿-上传视频功能
<br />
11) 游戏详情-关注游戏功能
</p>
</div>
<h5 class="title margintop">七、其他</h5>
<p>
7.1
本协议所有条款的标题仅为阅读方便,本身并无实际涵义,不能作为本协议涵义解释的依据。
<br />
7.2
如果本协议中的任何条款无论因何种原因完全或部分无效或不具有执行力,或违反任何适用的法律,则该条款被视为删除,但本协议的其余条款仍应有效并且有约束力。
<br />
7.3
光环有权随时根据有关法律、法规的变化以及公司经营状况和经营策略的调整等修改本协议。修改后的协议会在软件设置内发布。
当发生有关争议时,以最新的协议文本为准。如果不同意改动的内容,用户可以自行删除本软件。如果用户继续使用本软件,则视为您接受本协议的变动。
<br />
<span class="bold">
7.4 光环在法律允许的最大范围内对本协议拥有解释权与修改权。
</span>
</p>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,699 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v7.widget;
import android.support.v4.util.Pools;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static android.support.v7.widget.RecyclerView.ViewHolder;
/**
* Helper class that can enqueue and process adapter update operations.
* <p>
* To support animations, RecyclerView presents an older version the Adapter to best represent
* previous state of the layout. Sometimes, this is not trivial when items are removed that were
* not laid out, in which case, RecyclerView has no way of providing that item's view for
* animations.
* <p>
* AdapterHelper creates an UpdateOp for each adapter data change then pre-processes them. During
* pre processing, AdapterHelper finds out which UpdateOps can be deferred to second layout pass
* and which cannot. For the UpdateOps that cannot be deferred, AdapterHelper will change them
* according to previously deferred operation and dispatch them before the first layout pass. It
* also takes care of updating deferred UpdateOps since order of operations is changed by this
* process.
* <p>
* Although operations may be forwarded to LayoutManager in different orders, resulting data set
* is guaranteed to be the consistent.
*/
class AdapterHelper implements OpReorderer.Callback {
final static int POSITION_TYPE_INVISIBLE = 0;
final static int POSITION_TYPE_NEW_OR_LAID_OUT = 1;
private static final boolean DEBUG = false;
private static final String TAG = "AHT";
private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
final ArrayList<UpdateOp> mPostponedList = new ArrayList<UpdateOp>();
final Callback mCallback;
Runnable mOnItemProcessedCallback;
final boolean mDisableRecycler;
final OpReorderer mOpReorderer;
AdapterHelper(Callback callback) {
this(callback, false);
}
AdapterHelper(Callback callback, boolean disableRecycler) {
mCallback = callback;
mDisableRecycler = disableRecycler;
mOpReorderer = new OpReorderer(this);
}
AdapterHelper addUpdateOp(UpdateOp... ops) {
Collections.addAll(mPendingUpdates, ops);
return this;
}
void reset() {
recycleUpdateOpsAndClearList(mPendingUpdates);
recycleUpdateOpsAndClearList(mPostponedList);
}
void preProcess() {
mOpReorderer.reorderOps(mPendingUpdates);
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.ADD:
applyAdd(op);
break;
case UpdateOp.REMOVE:
applyRemove(op);
break;
case UpdateOp.UPDATE:
applyUpdate(op);
break;
case UpdateOp.MOVE:
applyMove(op);
break;
}
if (mOnItemProcessedCallback != null) {
mOnItemProcessedCallback.run();
}
}
mPendingUpdates.clear();
}
void consumePostponedUpdates() {
final int count = mPostponedList.size();
for (int i = 0; i < count; i++) {
mCallback.onDispatchSecondPass(mPostponedList.get(i));
}
recycleUpdateOpsAndClearList(mPostponedList);
}
private void applyMove(UpdateOp op) {
// MOVE ops are pre-processed so at this point, we know that item is still in the adapter.
// otherwise, it would be converted into a REMOVE operation
postponeAndUpdateViewHolders(op);
}
private void applyRemove(UpdateOp op) {
int tmpStart = op.positionStart;
int tmpCount = 0;
int tmpEnd = op.positionStart + op.itemCount;
int type = -1;
for (int position = op.positionStart; position < tmpEnd; position++) {
boolean typeChanged = false;
ViewHolder vh = mCallback.findViewHolder(position);
if (vh != null || canFindInPreLayout(position)) {
// If a ViewHolder exists or this is a newly added item, we can defer this update
// to post layout stage.
// * For existing ViewHolders, we'll fake its existence in the pre-layout phase.
// * For items that are added and removed in the same process cycle, they won't
// have any effect in pre-layout since their add ops are already deferred to
// post-layout pass.
if (type == POSITION_TYPE_INVISIBLE) {
// Looks like we have other updates that we cannot merge with this one.
// Create an UpdateOp and dispatch it to LayoutManager.
UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
dispatchAndUpdateViewHolders(newOp);
typeChanged = true;
}
type = POSITION_TYPE_NEW_OR_LAID_OUT;
} else {
// This update cannot be recovered because we don't have a ViewHolder representing
// this position. Instead, post it to LayoutManager immediately
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
// Looks like we have other updates that we cannot merge with this one.
// Create UpdateOp op and dispatch it to LayoutManager.
UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
postponeAndUpdateViewHolders(newOp);
typeChanged = true;
}
type = POSITION_TYPE_INVISIBLE;
}
if (typeChanged) {
position -= tmpCount; // also equal to tmpStart
tmpEnd -= tmpCount;
tmpCount = 1;
} else {
tmpCount++;
}
}
if (tmpCount != op.itemCount) { // all 1 effect
recycleUpdateOp(op);
op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
}
if (type == POSITION_TYPE_INVISIBLE) {
dispatchAndUpdateViewHolders(op);
} else {
postponeAndUpdateViewHolders(op);
}
}
private void applyUpdate(UpdateOp op) {
int tmpStart = op.positionStart;
int tmpCount = 0;
int tmpEnd = op.positionStart + op.itemCount;
int type = -1;
for (int position = op.positionStart; position < tmpEnd; position++) {
ViewHolder vh = mCallback.findViewHolder(position);
if (vh != null || canFindInPreLayout(position)) { // deferred
if (type == POSITION_TYPE_INVISIBLE) {
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
dispatchAndUpdateViewHolders(newOp);
tmpCount = 0;
tmpStart = position;
}
type = POSITION_TYPE_NEW_OR_LAID_OUT;
} else { // applied
if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
postponeAndUpdateViewHolders(newOp);
tmpCount = 0;
tmpStart = position;
}
type = POSITION_TYPE_INVISIBLE;
}
tmpCount++;
}
if (tmpCount != op.itemCount) { // all 1 effect
recycleUpdateOp(op);
op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
}
if (type == POSITION_TYPE_INVISIBLE) {
dispatchAndUpdateViewHolders(op);
} else {
postponeAndUpdateViewHolders(op);
}
}
private void dispatchAndUpdateViewHolders(UpdateOp op) {
// tricky part.
// traverse all postpones and revert their changes on this op if necessary, apply updated
// dispatch to them since now they are after this op.
if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) {
throw new IllegalArgumentException("should not dispatch add or move for pre layout");
}
if (DEBUG) {
Log.d(TAG, "dispatch (pre)" + op);
Log.d(TAG, "postponed state before:");
for (UpdateOp updateOp : mPostponedList) {
Log.d(TAG, updateOp.toString());
}
Log.d(TAG, "----");
}
// handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial
// TODO Since move ops are pushed to end, we should not need this anymore
int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);
if (DEBUG) {
Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart);
}
int tmpCnt = 1;
int offsetPositionForPartial = op.positionStart;
final int positionMultiplier;
switch (op.cmd) {
case UpdateOp.UPDATE:
positionMultiplier = 1;
break;
case UpdateOp.REMOVE:
positionMultiplier = 0;
break;
default:
throw new IllegalArgumentException("op should be remove or update." + op);
}
for (int p = 1; p < op.itemCount; p++) {
final int pos = op.positionStart + (positionMultiplier * p);
int updatedPos = updatePositionWithPostponed(pos, op.cmd);
if (DEBUG) {
Log.d(TAG, "pos:" + pos + ",updatedPos:" + updatedPos);
}
boolean continuous = false;
switch (op.cmd) {
case UpdateOp.UPDATE:
continuous = updatedPos == tmpStart + 1;
break;
case UpdateOp.REMOVE:
continuous = updatedPos == tmpStart;
break;
}
if (continuous) {
tmpCnt++;
} else {
// need to dispatch this separately
UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
if (DEBUG) {
Log.d(TAG, "need to dispatch separately " + tmp);
}
dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
recycleUpdateOp(tmp);
if (op.cmd == UpdateOp.UPDATE) {
offsetPositionForPartial += tmpCnt;
}
tmpStart = updatedPos;// need to remove previously dispatched
tmpCnt = 1;
}
}
recycleUpdateOp(op);
if (tmpCnt > 0) {
UpdateOp tmp = obtainUpdateOp(op.cmd, tmpStart, tmpCnt);
if (DEBUG) {
Log.d(TAG, "dispatching:" + tmp);
}
dispatchFirstPassAndUpdateViewHolders(tmp, offsetPositionForPartial);
recycleUpdateOp(tmp);
}
if (DEBUG) {
Log.d(TAG, "post dispatch");
Log.d(TAG, "postponed state after:");
for (UpdateOp updateOp : mPostponedList) {
Log.d(TAG, updateOp.toString());
}
Log.d(TAG, "----");
}
}
void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) {
mCallback.onDispatchFirstPass(op);
switch (op.cmd) {
case UpdateOp.REMOVE:
mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount);
break;
case UpdateOp.UPDATE:
mCallback.markViewHoldersUpdated(offsetStart, op.itemCount);
break;
default:
throw new IllegalArgumentException("only remove and update ops can be dispatched"
+ " in first pass");
}
}
private int updatePositionWithPostponed(int pos, int cmd) {
final int count = mPostponedList.size();
for (int i = count - 1; i >= 0; i--) {
UpdateOp postponed = mPostponedList.get(i);
if (postponed.cmd == UpdateOp.MOVE) {
int start, end;
if (postponed.positionStart < postponed.itemCount) {
start = postponed.positionStart;
end = postponed.itemCount;
} else {
start = postponed.itemCount;
end = postponed.positionStart;
}
if (pos >= start && pos <= end) {
//i'm affected
if (start == postponed.positionStart) {
if (cmd == UpdateOp.ADD) {
postponed.itemCount++;
} else if (cmd == UpdateOp.REMOVE) {
postponed.itemCount--;
}
// op moved to left, move it right to revert
pos++;
} else {
if (cmd == UpdateOp.ADD) {
postponed.positionStart++;
} else if (cmd == UpdateOp.REMOVE) {
postponed.positionStart--;
}
// op was moved right, move left to revert
pos--;
}
} else if (pos < postponed.positionStart) {
// postponed MV is outside the dispatched OP. if it is before, offset
if (cmd == UpdateOp.ADD) {
postponed.positionStart++;
postponed.itemCount++;
} else if (cmd == UpdateOp.REMOVE) {
postponed.positionStart--;
postponed.itemCount--;
}
}
} else {
if (postponed.positionStart <= pos) {
if (postponed.cmd == UpdateOp.ADD) {
pos -= postponed.itemCount;
} else if (postponed.cmd == UpdateOp.REMOVE) {
pos += postponed.itemCount;
}
} else {
if (cmd == UpdateOp.ADD) {
postponed.positionStart++;
} else if (cmd == UpdateOp.REMOVE) {
postponed.positionStart--;
}
}
}
if (DEBUG) {
Log.d(TAG, "dispath (step" + i + ")");
Log.d(TAG, "postponed state:" + i + ", pos:" + pos);
for (UpdateOp updateOp : mPostponedList) {
Log.d(TAG, updateOp.toString());
}
Log.d(TAG, "----");
}
}
for (int i = mPostponedList.size() - 1; i >= 0; i--) {
UpdateOp op = mPostponedList.get(i);
if (op.cmd == UpdateOp.MOVE) {
if (op.itemCount == op.positionStart || op.itemCount < 0) {
mPostponedList.remove(i);
recycleUpdateOp(op);
}
} else if (op.itemCount <= 0) {
mPostponedList.remove(i);
recycleUpdateOp(op);
}
}
return pos;
}
private boolean canFindInPreLayout(int position) {
final int count = mPostponedList.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPostponedList.get(i);
if (op.cmd == UpdateOp.MOVE) {
if (findPositionOffset(op.itemCount, i + 1) == position) {
return true;
}
} else if (op.cmd == UpdateOp.ADD) {
// TODO optimize.
final int end = op.positionStart + op.itemCount;
for (int pos = op.positionStart; pos < end; pos++) {
if (findPositionOffset(pos, i + 1) == position) {
return true;
}
}
}
}
return false;
}
private void applyAdd(UpdateOp op) {
postponeAndUpdateViewHolders(op);
}
private void postponeAndUpdateViewHolders(UpdateOp op) {
if (DEBUG) {
Log.d(TAG, "postponing " + op);
}
// Utils.log("add UpdateOp to PostponedList");
mPostponedList.add(op);
// Utils.log("op" + op.positionStart + "=" + op.itemCount);
switch (op.cmd) {
case UpdateOp.ADD:
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
break;
case UpdateOp.MOVE:
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
break;
case UpdateOp.REMOVE:
mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart,
op.itemCount);
break;
case UpdateOp.UPDATE:
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
break;
default:
throw new IllegalArgumentException("Unknown update op type for " + op);
}
}
boolean hasPendingUpdates() {
return mPendingUpdates.size() > 0;
}
int findPositionOffset(int position) {
return findPositionOffset(position, 0);
}
int findPositionOffset(int position, int firstPostponedItem) {
int count = mPostponedList.size();
for (int i = firstPostponedItem; i < count; ++i) {
UpdateOp op = mPostponedList.get(i);
if (op.cmd == UpdateOp.MOVE) {
if (op.positionStart == position) {
position = op.itemCount;
} else {
if (op.positionStart < position) {
position--; // like a remove
}
if (op.itemCount <= position) {
position++; // like an add
}
}
} else if (op.positionStart <= position) {
if (op.cmd == UpdateOp.REMOVE) {
if (position < op.positionStart + op.itemCount) {
return -1;
}
position -= op.itemCount;
} else if (op.cmd == UpdateOp.ADD) {
position += op.itemCount;
}
}
}
return position;
}
/**
* @return True if updates should be processed.
*/
boolean onItemRangeChanged(int positionStart, int itemCount) {
mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount));
return mPendingUpdates.size() == 1;
}
/**
* @return True if updates should be processed.
*/
boolean onItemRangeInserted(int positionStart, int itemCount) {
mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount));
return mPendingUpdates.size() == 1;
}
/**
* @return True if updates should be processed.
*/
boolean onItemRangeRemoved(int positionStart, int itemCount) {
mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount));
return mPendingUpdates.size() == 1;
}
/**
* @return True if updates should be processed.
*/
boolean onItemRangeMoved(int from, int to, int itemCount) {
if (from == to) {
return false;//no-op
}
if (itemCount != 1) {
throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
}
mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to));
return mPendingUpdates.size() == 1;
}
/**
* Skips pre-processing and applies all updates in one pass.
*/
void consumeUpdatesInOnePass() {
// we still consume postponed updates (if there is) in case there was a pre-process call
// w/o a matching consumePostponedUpdates.
consumePostponedUpdates();
final int count = mPendingUpdates.size();
for (int i = 0; i < count; i++) {
UpdateOp op = mPendingUpdates.get(i);
switch (op.cmd) {
case UpdateOp.ADD:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
break;
case UpdateOp.REMOVE:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
break;
case UpdateOp.UPDATE:
mCallback.onDispatchSecondPass(op);
mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
break;
case UpdateOp.MOVE:
mCallback.onDispatchSecondPass(op);
mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
break;
}
if (mOnItemProcessedCallback != null) {
mOnItemProcessedCallback.run();
}
}
recycleUpdateOpsAndClearList(mPendingUpdates);
}
/**
* Queued operation to happen when child views are updated.
*/
static class UpdateOp {
static final int ADD = 0;
static final int REMOVE = 1;
static final int UPDATE = 2;
static final int MOVE = 3;
static final int POOL_SIZE = 30;
int cmd;
int positionStart;
// holds the target position if this is a MOVE
int itemCount;
UpdateOp(int cmd, int positionStart, int itemCount) {
this.cmd = cmd;
this.positionStart = positionStart;
this.itemCount = itemCount;
}
String cmdToString() {
switch (cmd) {
case ADD:
return "add";
case REMOVE:
return "rm";
case UPDATE:
return "up";
case MOVE:
return "mv";
}
return "??";
}
@Override
public String toString() {
return "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UpdateOp op = (UpdateOp) o;
if (cmd != op.cmd) {
return false;
}
if (cmd == MOVE && Math.abs(itemCount - positionStart) == 1) {
// reverse of this is also true
if (itemCount == op.positionStart && positionStart == op.itemCount) {
return true;
}
}
if (itemCount != op.itemCount) {
return false;
}
if (positionStart != op.positionStart) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = cmd;
result = 31 * result + positionStart;
result = 31 * result + itemCount;
return result;
}
}
@Override
public UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount) {
UpdateOp op = mUpdateOpPool.acquire();
if (op == null) {
op = new UpdateOp(cmd, positionStart, itemCount);
} else {
op.cmd = cmd;
op.positionStart = positionStart;
op.itemCount = itemCount;
}
return op;
}
@Override
public void recycleUpdateOp(UpdateOp op) {
if (!mDisableRecycler) {
mUpdateOpPool.release(op);
}
}
void recycleUpdateOpsAndClearList(List<UpdateOp> ops) {
final int count = ops.size();
for (int i = 0; i < count; i++) {
recycleUpdateOp(ops.get(i));
}
ops.clear();
}
/**
* Contract between AdapterHelper and RecyclerView.
*/
static interface Callback {
ViewHolder findViewHolder(int position);
void offsetPositionsForRemovingInvisible(int positionStart, int itemCount);
void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount);
void markViewHoldersUpdated(int positionStart, int itemCount);
void onDispatchFirstPass(UpdateOp updateOp);
void onDispatchSecondPass(UpdateOp updateOp);
void offsetPositionsForAdd(int positionStart, int itemCount);
void offsetPositionsForMove(int from, int to);
}
}

View File

@ -0,0 +1,484 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v7.widget;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class to manage children.
* <p>
* It wraps a RecyclerView and adds ability to hide some children. There are two sets of methods
* provided by this class. <b>Regular</b> methods are the ones that replicate ViewGroup methods
* like getChildAt, getChildCount etc. These methods ignore hidden children.
* <p>
* When RecyclerView needs direct access to the view group children, it can call unfiltered
* methods like get getUnfilteredChildCount or getUnfilteredChildAt.
*/
class ChildHelper {
private static final boolean DEBUG = false;
private static final String TAG = "ChildrenHelper";
final Callback mCallback;
final Bucket mBucket;
final List<View> mHiddenViews;
ChildHelper(Callback callback) {
mCallback = callback;
mBucket = new Bucket();
mHiddenViews = new ArrayList<View>();
}
/**
* Adds a view to the ViewGroup
*
* @param child View to add.
* @param hidden If set to true, this item will be invisible from regular methods.
*/
void addView(View child, boolean hidden) {
addView(child, -1, hidden);
}
/**
* Add a view to the ViewGroup at an index
*
* @param child View to add.
* @param index Index of the child from the regular perspective (excluding hidden views).
* ChildHelper offsets this index to actual ViewGroup index.
* @param hidden If set to true, this item will be invisible from regular methods.
*/
void addView(View child, int index, boolean hidden) {
final int offset;
if (index < 0) {
offset = mCallback.getChildCount();
} else {
offset = getOffset(index);
}
mCallback.addView(child, offset);
mBucket.insert(offset, hidden);
if (hidden) {
mHiddenViews.add(child);
}
if (DEBUG) {
Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this);
}
}
private int getOffset(int index) {
if (index < 0) {
return -1; //anything below 0 won't work as diff will be undefined.
}
final int limit = mCallback.getChildCount();
int offset = index;
while (offset < limit) {
final int removedBefore = mBucket.countOnesBefore(offset);
final int diff = index - (offset - removedBefore);
if (diff == 0) {
while (mBucket.get(offset)) { // ensure this offset is not hidden
offset ++;
}
return offset;
} else {
offset += diff;
}
}
return -1;
}
/**
* Removes the provided View from underlying RecyclerView.
*
* @param view The view to remove.
*/
void removeView(View view) {
int index = mCallback.indexOfChild(view);
if (index < 0) {
return;
}
mCallback.removeViewAt(index);
if (mBucket.remove(index)) {
mHiddenViews.remove(view);
}
if (DEBUG) {
Log.d(TAG, "remove View off:" + index + "," + this);
}
}
/**
* Removes the view at the provided index from RecyclerView.
*
* @param index Index of the child from the regular perspective (excluding hidden views).
* ChildHelper offsets this index to actual ViewGroup index.
*/
void removeViewAt(int index) {
final int offset = getOffset(index);
final View view = mCallback.getChildAt(offset);
if (view == null) {
return;
}
mCallback.removeViewAt(offset);
if (mBucket.remove(offset)) {
mHiddenViews.remove(view);
}
if (DEBUG) {
Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);
}
}
/**
* Returns the child at provided index.
*
* @param index Index of the child to return in regular perspective.
*/
View getChildAt(int index) {
final int offset = getOffset(index);
return mCallback.getChildAt(offset);
}
/**
* Removes all views from the ViewGroup including the hidden ones.
*/
void removeAllViewsUnfiltered() {
mCallback.removeAllViews();
mBucket.reset();
mHiddenViews.clear();
if (DEBUG) {
Log.d(TAG, "removeAllViewsUnfiltered");
}
}
/**
* This can be used to find a disappearing view by position.
*
* @param position The adapter position of the item.
* @param type View type, can be {@link RecyclerView#INVALID_TYPE}.
* @return A hidden view with a valid ViewHolder that matches the position and type.
*/
View findHiddenNonRemovedView(int position, int type) {
final int count = mHiddenViews.size();
for (int i = 0; i < count; i++) {
final View view = mHiddenViews.get(i);
RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
if (holder.getPosition() == position && !holder.isInvalid() &&
(type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
return view;
}
}
return null;
}
/**
* Attaches the provided view to the underlying ViewGroup.
*
* @param child Child to attach.
* @param index Index of the child to attach in regular perspective.
* @param layoutParams LayoutParams for the child.
* @param hidden If set to true, this item will be invisible to the regular methods.
*/
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,
boolean hidden) {
final int offset;
if (index < 0) {
offset = mCallback.getChildCount();
} else {
offset = getOffset(index);
}
mCallback.attachViewToParent(child, offset, layoutParams);
mBucket.insert(offset, hidden);
if (DEBUG) {
Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + "," +
"h:" + hidden + ", " + this);
}
}
/**
* Returns the number of children that are not hidden.
*
* @return Number of children that are not hidden.
* @see #getChildAt(int)
*/
int getChildCount() {
return mCallback.getChildCount() - mHiddenViews.size();
}
/**
* Returns the total number of children.
*
* @return The total number of children including the hidden views.
* @see #getUnfilteredChildAt(int)
*/
int getUnfilteredChildCount() {
return mCallback.getChildCount();
}
/**
* Returns a child by ViewGroup offset. ChildHelper won't offset this index.
*
* @param index ViewGroup index of the child to return.
* @return The view in the provided index.
*/
View getUnfilteredChildAt(int index) {
return mCallback.getChildAt(index);
}
/**
* Detaches the view at the provided index.
*
* @param index Index of the child to return in regular perspective.
*/
void detachViewFromParent(int index) {
final int offset = getOffset(index);
mCallback.detachViewFromParent(offset);
mBucket.remove(offset);
if (DEBUG) {
Log.d(TAG, "detach view from parent " + index + ", off:" + offset);
}
}
/**
* Returns the index of the child in regular perspective.
*
* @param child The child whose index will be returned.
* @return The regular perspective index of the child or -1 if it does not exists.
*/
int indexOfChild(View child) {
final int index = mCallback.indexOfChild(child);
if (index == -1) {
return -1;
}
if (mBucket.get(index)) {
if (DEBUG) {
throw new IllegalArgumentException("cannot get index of a hidden child");
} else {
return -1;
}
}
// reverse the index
return index - mBucket.countOnesBefore(index);
}
/**
* Returns whether a View is visible to LayoutManager or not.
*
* @param view The child view to check. Should be a child of the Callback.
* @return True if the View is not visible to LayoutManager
*/
boolean isHidden(View view) {
return mHiddenViews.contains(view);
}
/**
* Marks a child view as hidden.
*
* @param view The view to hide.
*/
void hide(View view) {
final int offset = mCallback.indexOfChild(view);
if (offset < 0) {
throw new IllegalArgumentException("view is not a child, cannot hide " + view);
}
if (DEBUG && mBucket.get(offset)) {
throw new RuntimeException("trying to hide same view twice, how come ? " + view);
}
mBucket.set(offset);
mHiddenViews.add(view);
if (DEBUG) {
Log.d(TAG, "hiding child " + view + " at offset " + offset+ ", " + this);
}
}
@Override
public String toString() {
return mBucket.toString();
}
/**
* Removes a view from the ViewGroup if it is hidden.
*
* @param view The view to remove.
* @return True if the View is found and it is hidden. False otherwise.
*/
boolean removeViewIfHidden(View view) {
final int index = mCallback.indexOfChild(view);
if (index == -1) {
if (mHiddenViews.remove(view) && DEBUG) {
throw new IllegalStateException("view is in hidden list but not in view group");
}
return true;
}
if (mBucket.get(index)) {
mBucket.remove(index);
mCallback.removeViewAt(index);
if (!mHiddenViews.remove(view) && DEBUG) {
throw new IllegalStateException(
"removed a hidden view but it is not in hidden views list");
}
return true;
}
return false;
}
/**
* Bitset implementation that provides methods to offset indices.
*/
static class Bucket {
final static int BITS_PER_WORD = Long.SIZE;
final static long LAST_BIT = 1L << (Long.SIZE - 1);
long mData = 0;
Bucket next;
void set(int index) {
if (index >= BITS_PER_WORD) {
ensureNext();
next.set(index - BITS_PER_WORD);
} else {
mData |= 1L << index;
}
}
private void ensureNext() {
if (next == null) {
next = new Bucket();
}
}
void clear(int index) {
if (index >= BITS_PER_WORD) {
if (next != null) {
next.clear(index - BITS_PER_WORD);
}
} else {
mData &= ~(1L << index);
}
}
boolean get(int index) {
if (index >= BITS_PER_WORD) {
ensureNext();
return next.get(index - BITS_PER_WORD);
} else {
return (mData & (1L << index)) != 0;
}
}
void reset() {
mData = 0;
if (next != null) {
next.reset();
}
}
void insert(int index, boolean value) {
if (index >= BITS_PER_WORD) {
ensureNext();
next.insert(index - BITS_PER_WORD, value);
} else {
final boolean lastBit = (mData & LAST_BIT) != 0;
long mask = (1L << index) - 1;
final long before = mData & mask;
final long after = ((mData & ~mask)) << 1;
mData = before | after;
if (value) {
set(index);
} else {
clear(index);
}
if (lastBit || next != null) {
ensureNext();
next.insert(0, lastBit);
}
}
}
boolean remove(int index) {
if (index >= BITS_PER_WORD) {
ensureNext();
return next.remove(index - BITS_PER_WORD);
} else {
long mask = (1L << index);
final boolean value = (mData & mask) != 0;
mData &= ~mask;
mask = mask - 1;
final long before = mData & mask;
// cannot use >> because it adds one.
final long after = Long.rotateRight(mData & ~mask, 1);
mData = before | after;
if (next != null) {
if (next.get(0)) {
set(BITS_PER_WORD - 1);
}
next.remove(0);
}
return value;
}
}
int countOnesBefore(int index) {
if (next == null) {
if (index >= BITS_PER_WORD) {
return Long.bitCount(mData);
}
return Long.bitCount(mData & ((1L << index) - 1));
}
if (index < BITS_PER_WORD) {
return Long.bitCount(mData & ((1L << index) - 1));
} else {
return next.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
}
}
@Override
public String toString() {
return next == null ? Long.toBinaryString(mData)
: next.toString() + "xx" + Long.toBinaryString(mData);
}
}
static interface Callback {
int getChildCount();
void addView(View child, int index);
int indexOfChild(View view);
void removeViewAt(int index);
View getChildAt(int offset);
void removeAllViews();
RecyclerView.ViewHolder getChildViewHolder(View view);
void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
void detachViewFromParent(int offset);
}
}

View File

@ -0,0 +1,628 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v7.widget;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorCompat;
import android.support.v4.view.ViewPropertyAnimatorListener;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
/**
* This implementation of {@link RecyclerView.ItemAnimator} provides basic
* animations on remove, add, and move events that happen to the items in
* a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
*
* @see RecyclerView#setItemAnimator(RecyclerView.ItemAnimator)
*/
public class DefaultItemAnimator extends RecyclerView.ItemAnimator {
private static final boolean DEBUG = false;
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<ViewHolder>();
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<ViewHolder>();
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<MoveInfo>();
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<ChangeInfo>();
private ArrayList<ArrayList<ViewHolder>> mAdditionsList =
new ArrayList<ArrayList<ViewHolder>>();
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<ArrayList<MoveInfo>>();
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<ArrayList<ChangeInfo>>();
private ArrayList<ViewHolder> mAddAnimations = new ArrayList<ViewHolder>();
private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<ViewHolder>();
private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<ViewHolder>();
private ArrayList<ViewHolder> mChangeAnimations = new ArrayList<ViewHolder>();
private static class MoveInfo {
public ViewHolder holder;
public int fromX, fromY, toX, toY;
private MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
this.holder = holder;
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
}
private static class ChangeInfo {
public ViewHolder oldHolder, newHolder;
public int fromX, fromY, toX, toY;
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
this.oldHolder = oldHolder;
this.newHolder = newHolder;
}
private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
this(oldHolder, newHolder);
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
@Override
public String toString() {
return "ChangeInfo{" +
"oldHolder=" + oldHolder +
", newHolder=" + newHolder +
", fromX=" + fromX +
", fromY=" + fromY +
", toX=" + toX +
", toY=" + toY +
'}';
}
}
@Override
public void runPendingAnimations() {
boolean removalsPending = !mPendingRemovals.isEmpty();
boolean movesPending = !mPendingMoves.isEmpty();
boolean changesPending = !mPendingChanges.isEmpty();
boolean additionsPending = !mPendingAdditions.isEmpty();
if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
// nothing to animate
return;
}
// First, remove stuff
for (ViewHolder holder : mPendingRemovals) {
animateRemoveImpl(holder);
}
mPendingRemovals.clear();
// Next, move stuff
if (movesPending) {
final ArrayList<MoveInfo> moves = new ArrayList<MoveInfo>();
moves.addAll(mPendingMoves);
mMovesList.add(moves);
mPendingMoves.clear();
Runnable mover = new Runnable() {
@Override
public void run() {
for (MoveInfo moveInfo : moves) {
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
moveInfo.toX, moveInfo.toY);
}
moves.clear();
mMovesList.remove(moves);
}
};
if (removalsPending) {
View view = moves.get(0).holder.itemView;
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration());
} else {
mover.run();
}
}
// Next, change stuff, to run in parallel with move animations
if (changesPending) {
final ArrayList<ChangeInfo> changes = new ArrayList<ChangeInfo>();
changes.addAll(mPendingChanges);
mChangesList.add(changes);
mPendingChanges.clear();
Runnable changer = new Runnable() {
@Override
public void run() {
for (ChangeInfo change : changes) {
animateChangeImpl(change);
}
changes.clear();
mChangesList.remove(changes);
}
};
if (removalsPending) {
ViewHolder holder = changes.get(0).oldHolder;
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
} else {
changer.run();
}
}
// Next, add stuff
if (additionsPending) {
final ArrayList<ViewHolder> additions = new ArrayList<ViewHolder>();
additions.addAll(mPendingAdditions);
mAdditionsList.add(additions);
mPendingAdditions.clear();
Runnable adder = new Runnable() {
public void run() {
for (ViewHolder holder : additions) {
animateAddImpl(holder);
}
additions.clear();
mAdditionsList.remove(additions);
}
};
if (removalsPending || movesPending || changesPending) {
long removeDuration = removalsPending ? getRemoveDuration() : 0;
long moveDuration = movesPending ? getMoveDuration() : 0;
long changeDuration = changesPending ? getChangeDuration() : 0;
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = additions.get(0).itemView;
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
} else {
adder.run();
}
}
}
@Override
public boolean animateRemove(final ViewHolder holder) {
endAnimation(holder);
mPendingRemovals.add(holder);
return true;
}
private void animateRemoveImpl(final ViewHolder holder) {
final View view = holder.itemView;
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
animation.setDuration(getRemoveDuration())
.alpha(0).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
ViewCompat.setAlpha(view, 1);
dispatchRemoveFinished(holder);
mRemoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
mRemoveAnimations.add(holder);
}
@Override
public boolean animateAdd(final ViewHolder holder) {
endAnimation(holder);
ViewCompat.setAlpha(holder.itemView, 0);
mPendingAdditions.add(holder);
return true;
}
private void animateAddImpl(final ViewHolder holder) {
final View view = holder.itemView;
mAddAnimations.add(holder);
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
animation.alpha(1).setDuration(getAddDuration()).
setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
ViewCompat.setAlpha(view, 1);
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchAddFinished(holder);
mAddAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
int toX, int toY) {
final View view = holder.itemView;
fromX += ViewCompat.getTranslationX(holder.itemView);
fromY += ViewCompat.getTranslationY(holder.itemView);
endAnimation(holder);
int deltaX = toX - fromX;
int deltaY = toY - fromY;
if (deltaX == 0 && deltaY == 0) {
dispatchMoveFinished(holder);
return false;
}
if (deltaX != 0) {
ViewCompat.setTranslationX(view, -deltaX);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, -deltaY);
}
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
return true;
}
private void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
final View view = holder.itemView;
final int deltaX = toX - fromX;
final int deltaY = toY - fromY;
if (deltaX != 0) {
ViewCompat.animate(view).translationX(0);
}
if (deltaY != 0) {
ViewCompat.animate(view).translationY(0);
}
// TODO: make EndActions end listeners instead, since end actions aren't called when
// vpas are canceled (and can't end them. why?)
// need listener functionality in VPACompat for this. Ick.
mMoveAnimations.add(holder);
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view);
animation.setDuration(getMoveDuration()).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchMoveStarting(holder);
}
@Override
public void onAnimationCancel(View view) {
if (deltaX != 0) {
ViewCompat.setTranslationX(view, 0);
}
if (deltaY != 0) {
ViewCompat.setTranslationY(view, 0);
}
}
@Override
public void onAnimationEnd(View view) {
animation.setListener(null);
dispatchMoveFinished(holder);
mMoveAnimations.remove(holder);
dispatchFinishedWhenDone();
}
}).start();
}
@Override
public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
int fromX, int fromY, int toX, int toY) {
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
endAnimation(oldHolder);
int deltaX = (int) (toX - fromX - prevTranslationX);
int deltaY = (int) (toY - fromY - prevTranslationY);
// recover prev translation state after ending animation
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
if (newHolder != null && newHolder.itemView != null) {
// carry over translation values
endAnimation(newHolder);
ViewCompat.setTranslationX(newHolder.itemView, -deltaX);
ViewCompat.setTranslationY(newHolder.itemView, -deltaY);
ViewCompat.setAlpha(newHolder.itemView, 0);
}
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
return true;
}
private void animateChangeImpl(final ChangeInfo changeInfo) {
final ViewHolder holder = changeInfo.oldHolder;
final View view = holder.itemView;
final ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
mChangeAnimations.add(changeInfo.oldHolder);
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration(
getChangeDuration());
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX);
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY);
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.oldHolder, true);
}
@Override
public void onAnimationEnd(View view) {
oldViewAnim.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
ViewCompat.setTranslationY(view, 0);
dispatchChangeFinished(changeInfo.oldHolder, true);
mChangeAnimations.remove(changeInfo.oldHolder);
dispatchFinishedWhenDone();
}
}).start();
if (newView != null) {
mChangeAnimations.add(changeInfo.newHolder);
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView);
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()).
alpha(1).setListener(new VpaListenerAdapter() {
@Override
public void onAnimationStart(View view) {
dispatchChangeStarting(changeInfo.newHolder, false);
}
@Override
public void onAnimationEnd(View view) {
newViewAnimation.setListener(null);
ViewCompat.setAlpha(newView, 1);
ViewCompat.setTranslationX(newView, 0);
ViewCompat.setTranslationY(newView, 0);
dispatchChangeFinished(changeInfo.newHolder, false);
mChangeAnimations.remove(changeInfo.newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
}
private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
for (int i = infoList.size() - 1; i >= 0; i--) {
ChangeInfo changeInfo = infoList.get(i);
if (endChangeAnimationIfNecessary(changeInfo, item)) {
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
infoList.remove(changeInfo);
}
}
}
}
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
if (changeInfo.oldHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
}
if (changeInfo.newHolder != null) {
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
}
}
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
boolean oldItem = false;
if (changeInfo.newHolder == item) {
changeInfo.newHolder = null;
} else if (changeInfo.oldHolder == item) {
changeInfo.oldHolder = null;
oldItem = true;
} else {
return false;
}
ViewCompat.setAlpha(item.itemView, 1);
ViewCompat.setTranslationX(item.itemView, 0);
ViewCompat.setTranslationY(item.itemView, 0);
dispatchChangeFinished(item, oldItem);
return true;
}
@Override
public void endAnimation(ViewHolder item) {
final View view = item.itemView;
// this will trigger end callback which should set properties to their target values.
ViewCompat.animate(view).cancel();
// TODO if some other animations are chained to end, how do we cancel them as well?
for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
MoveInfo moveInfo = mPendingMoves.get(i);
if (moveInfo.holder == item) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
mPendingMoves.remove(item);
}
}
endChangeAnimation(mPendingChanges, item);
if (mPendingRemovals.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchRemoveFinished(item);
}
if (mPendingAdditions.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
}
for (int i = mChangesList.size() - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
endChangeAnimation(changes, item);
if (changes.isEmpty()) {
mChangesList.remove(changes);
}
}
for (int i = mMovesList.size() - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
for (int j = moves.size() - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
if (moveInfo.holder == item) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(moves);
}
break;
}
}
}
for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
if (additions.remove(item)) {
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
if (additions.isEmpty()) {
mAdditionsList.remove(additions);
}
}
}
// animations should be ended by the cancel above.
if (mRemoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mRemoveAnimations list");
}
if (mAddAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mAddAnimations list");
}
if (mChangeAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mChangeAnimations list");
}
if (mMoveAnimations.remove(item) && DEBUG) {
throw new IllegalStateException("after animation is cancelled, item should not be in "
+ "mMoveAnimations list");
}
dispatchFinishedWhenDone();
}
@Override
public boolean isRunning() {
return (!mPendingAdditions.isEmpty() ||
!mPendingChanges.isEmpty() ||
!mPendingMoves.isEmpty() ||
!mPendingRemovals.isEmpty() ||
!mMoveAnimations.isEmpty() ||
!mRemoveAnimations.isEmpty() ||
!mAddAnimations.isEmpty() ||
!mChangeAnimations.isEmpty() ||
!mMovesList.isEmpty() ||
!mAdditionsList.isEmpty() ||
!mChangesList.isEmpty());
}
/**
* Check the state of currently pending and running animations. If there are none
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any
* listeners.
*/
private void dispatchFinishedWhenDone() {
if (!isRunning()) {
dispatchAnimationsFinished();
}
}
@Override
public void endAnimations() {
int count = mPendingMoves.size();
for (int i = count - 1; i >= 0; i--) {
MoveInfo item = mPendingMoves.get(i);
View view = item.holder.itemView;
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item.holder);
mPendingMoves.remove(i);
}
count = mPendingRemovals.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mPendingRemovals.get(i);
dispatchRemoveFinished(item);
mPendingRemovals.remove(i);
}
count = mPendingAdditions.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mPendingAdditions.get(i);
View view = item.itemView;
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
mPendingAdditions.remove(i);
}
count = mPendingChanges.size();
for (int i = count - 1; i >= 0; i--) {
endChangeAnimationIfNecessary(mPendingChanges.get(i));
}
mPendingChanges.clear();
if (!isRunning()) {
return;
}
int listCount = mMovesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<MoveInfo> moves = mMovesList.get(i);
count = moves.size();
for (int j = count - 1; j >= 0; j--) {
MoveInfo moveInfo = moves.get(j);
ViewHolder item = moveInfo.holder;
View view = item.itemView;
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(moveInfo.holder);
moves.remove(j);
if (moves.isEmpty()) {
mMovesList.remove(moves);
}
}
}
listCount = mAdditionsList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ViewHolder> additions = mAdditionsList.get(i);
count = additions.size();
for (int j = count - 1; j >= 0; j--) {
ViewHolder item = additions.get(j);
View view = item.itemView;
ViewCompat.setAlpha(view, 1);
dispatchAddFinished(item);
additions.remove(j);
if (additions.isEmpty()) {
mAdditionsList.remove(additions);
}
}
}
listCount = mChangesList.size();
for (int i = listCount - 1; i >= 0; i--) {
ArrayList<ChangeInfo> changes = mChangesList.get(i);
count = changes.size();
for (int j = count - 1; j >= 0; j--) {
endChangeAnimationIfNecessary(changes.get(j));
if (changes.isEmpty()) {
mChangesList.remove(changes);
}
}
}
cancelAll(mRemoveAnimations);
cancelAll(mMoveAnimations);
cancelAll(mAddAnimations);
cancelAll(mChangeAnimations);
dispatchAnimationsFinished();
}
void cancelAll(List<ViewHolder> viewHolders) {
for (int i = viewHolders.size() - 1; i >= 0; i--) {
ViewCompat.animate(viewHolders.get(i).itemView).cancel();
}
}
private static class VpaListenerAdapter implements ViewPropertyAnimatorListener {
@Override
public void onAnimationStart(View view) {}
@Override
public void onAnimationEnd(View view) {}
@Override
public void onAnimationCancel(View view) {}
};
}

View File

@ -0,0 +1,816 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific languag`e governing permissions and
* limitations under the License.
*/
package android.support.v7.widget;
import android.content.Context;
import android.graphics.Rect;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;
import java.util.Arrays;
/**
* A {@link RecyclerView.LayoutManager} implementations that lays out items in a grid.
* <p>
* By default, each item occupies 1 span. You can change it by providing a custom
* {@link SpanSizeLookup} instance via {@link #setSpanSizeLookup(SpanSizeLookup)}.
*/
public class GridLayoutManager extends LinearLayoutManager {
private static final boolean DEBUG = false;
private static final String TAG = "GridLayoutManager";
public static final int DEFAULT_SPAN_COUNT = -1;
/**
* The measure spec for the scroll direction.
*/
static final int MAIN_DIR_SPEC =
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int mSpanCount = DEFAULT_SPAN_COUNT;
/**
* The size of each span
*/
int mSizePerSpan;
/**
* Temporary array to keep views in layoutChunk method
*/
View[] mSet;
final SparseIntArray mPreLayoutSpanSizeCache = new SparseIntArray();
final SparseIntArray mPreLayoutSpanIndexCache = new SparseIntArray();
SpanSizeLookup mSpanSizeLookup = new DefaultSpanSizeLookup();
// re-used variable to acquire decor insets from RecyclerView
final Rect mDecorInsets = new Rect();
/**
* Creates a vertical GridLayoutManager
*
* @param context Current context, will be used to access resources.
* @param spanCount The number of columns in the grid
*/
public GridLayoutManager(Context context, int spanCount) {
super(context);
setSpanCount(spanCount);
}
/**
* @param context Current context, will be used to access resources.
* @param spanCount The number of columns or rows in the grid
* @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
* #VERTICAL}.
* @param reverseLayout When set to true, layouts from end to start.
*/
public GridLayoutManager(Context context, int spanCount, int orientation,
boolean reverseLayout) {
super(context, orientation, reverseLayout);
setSpanCount(spanCount);
}
/**
* stackFromEnd is not supported by GridLayoutManager. Consider using
* {@link #setReverseLayout(boolean)}.
*/
@Override
public void setStackFromEnd(boolean stackFromEnd) {
if (stackFromEnd) {
throw new UnsupportedOperationException(
"GridLayoutManager does not support stack from end."
+ " Consider using reverse layout");
}
super.setStackFromEnd(false);
}
@Override
public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return mSpanCount;
}
if (state.getItemCount() < 1) {
return 0;
}
return getSpanGroupIndex(recycler, state, state.getItemCount() - 1);
}
@Override
public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == VERTICAL) {
return mSpanCount;
}
if (state.getItemCount() < 1) {
return 0;
}
return getSpanGroupIndex(recycler, state, state.getItemCount() - 1);
}
@Override
public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
ViewGroup.LayoutParams lp = host.getLayoutParams();
if (!(lp instanceof LayoutParams)) {
super.onInitializeAccessibilityNodeInfoForItem(host, info);
return;
}
LayoutParams glp = (LayoutParams) lp;
int spanGroupIndex = getSpanGroupIndex(recycler, state, glp.getViewPosition());
if (mOrientation == HORIZONTAL) {
info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
glp.getSpanIndex(), glp.getSpanSize(),
spanGroupIndex, 1,
mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
} else { // VERTICAL
info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
spanGroupIndex , 1,
glp.getSpanIndex(), glp.getSpanSize(),
mSpanCount > 1 && glp.getSpanSize() == mSpanCount, false));
}
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.isPreLayout()) {
cachePreLayoutSpanMapping();
}
super.onLayoutChildren(recycler, state);
if (DEBUG) {
validateChildOrder();
}
clearPreLayoutSpanMappingCache();
}
private void clearPreLayoutSpanMappingCache() {
mPreLayoutSpanSizeCache.clear();
mPreLayoutSpanIndexCache.clear();
}
private void cachePreLayoutSpanMapping() {
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
final int viewPosition = lp.getViewPosition();
mPreLayoutSpanSizeCache.put(viewPosition, lp.getSpanSize());
mPreLayoutSpanIndexCache.put(viewPosition, lp.getSpanIndex());
}
}
@Override
public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
mSpanSizeLookup.invalidateSpanIndexCache();
}
@Override
public void onItemsChanged(RecyclerView recyclerView) {
mSpanSizeLookup.invalidateSpanIndexCache();
}
@Override
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
mSpanSizeLookup.invalidateSpanIndexCache();
}
@Override
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
mSpanSizeLookup.invalidateSpanIndexCache();
}
@Override
public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
mSpanSizeLookup.invalidateSpanIndexCache();
}
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
return new LayoutParams(c, attrs);
}
@Override
public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
if (lp instanceof ViewGroup.MarginLayoutParams) {
return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
} else {
return new LayoutParams(lp);
}
}
@Override
public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
return lp instanceof LayoutParams;
}
/**
* Sets the source to get the number of spans occupied by each item in the adapter.
*
* @param spanSizeLookup {@link SpanSizeLookup} instance to be used to query number of spans
* occupied by each item
*/
public void setSpanSizeLookup(SpanSizeLookup spanSizeLookup) {
mSpanSizeLookup = spanSizeLookup;
}
/**
* Returns the current {@link SpanSizeLookup} used by the GridLayoutManager.
*
* @return The current {@link SpanSizeLookup} used by the GridLayoutManager.
*/
public SpanSizeLookup getSpanSizeLookup() {
return mSpanSizeLookup;
}
private void updateMeasurements() {
int totalSpace;
if (getOrientation() == VERTICAL) {
totalSpace = getWidth() - getPaddingRight() - getPaddingLeft();
} else {
totalSpace = getHeight() - getPaddingBottom() - getPaddingTop();
}
mSizePerSpan = totalSpace / mSpanCount;
}
@Override
void onAnchorReady(RecyclerView.State state, AnchorInfo anchorInfo) {
super.onAnchorReady(state, anchorInfo);
updateMeasurements();
if (state.getItemCount() > 0 && !state.isPreLayout()) {
ensureAnchorIsInFirstSpan(anchorInfo);
}
if (mSet == null || mSet.length != mSpanCount) {
mSet = new View[mSpanCount];
}
}
private void ensureAnchorIsInFirstSpan(AnchorInfo anchorInfo) {
int span = mSpanSizeLookup.getCachedSpanIndex(anchorInfo.mPosition, mSpanCount);
while (span > 0 && anchorInfo.mPosition > 0) {
anchorInfo.mPosition--;
span = mSpanSizeLookup.getCachedSpanIndex(anchorInfo.mPosition, mSpanCount);
}
}
private int getSpanGroupIndex(RecyclerView.Recycler recycler, RecyclerView.State state,
int viewPosition) {
if (!state.isPreLayout()) {
return mSpanSizeLookup.getSpanGroupIndex(viewPosition, mSpanCount);
}
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(viewPosition);
if (adapterPosition == -1) {
if (DEBUG) {
throw new RuntimeException("Cannot find span group index for position "
+ viewPosition);
}
Log.w(TAG, "Cannot find span size for pre layout position. " + viewPosition);
return 0;
}
return mSpanSizeLookup.getSpanGroupIndex(adapterPosition, mSpanCount);
}
private int getSpanIndex(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
if (!state.isPreLayout()) {
return mSpanSizeLookup.getCachedSpanIndex(pos, mSpanCount);
}
final int cached = mPreLayoutSpanIndexCache.get(pos, -1);
if (cached != -1) {
return cached;
}
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
if (adapterPosition == -1) {
if (DEBUG) {
throw new RuntimeException("Cannot find span index for pre layout position. It is"
+ " not cached, not in the adapter. Pos:" + pos);
}
Log.w(TAG, "Cannot find span size for pre layout position. It is"
+ " not cached, not in the adapter. Pos:" + pos);
return 0;
}
return mSpanSizeLookup.getCachedSpanIndex(adapterPosition, mSpanCount);
}
private int getSpanSize(RecyclerView.Recycler recycler, RecyclerView.State state, int pos) {
if (!state.isPreLayout()) {
return mSpanSizeLookup.getSpanSize(pos);
}
final int cached = mPreLayoutSpanSizeCache.get(pos, -1);
if (cached != -1) {
return cached;
}
final int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(pos);
if (adapterPosition == -1) {
if (DEBUG) {
throw new RuntimeException("Cannot find span size for pre layout position. It is"
+ " not cached, not in the adapter. Pos:" + pos);
}
Log.w(TAG, "Cannot find span size for pre layout position. It is"
+ " not cached, not in the adapter. Pos:" + pos);
return 1;
}
return mSpanSizeLookup.getSpanSize(adapterPosition);
}
@Override
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
final boolean layingOutInPrimaryDirection =
layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;
int count = 0;
int consumedSpanCount = 0;
int remainingSpan = mSpanCount;
if (!layingOutInPrimaryDirection) {
int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition);
int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition);
remainingSpan = itemSpanIndex + itemSpanSize;
}
while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
int pos = layoutState.mCurrentPosition;
final int spanSize = getSpanSize(recycler, state, pos);
if (spanSize > mSpanCount) {
throw new IllegalArgumentException("Item at position " + pos + " requires " +
spanSize + " spans but GridLayoutManager has only " + mSpanCount
+ " spans.");
}
remainingSpan -= spanSize;
if (remainingSpan < 0) {
break; // item did not fit into this row or column
}
View view = layoutState.next(recycler);
if (view == null) {
break;
}
consumedSpanCount += spanSize;
mSet[count] = view;
count++;
}
if (count == 0) {
result.mFinished = true;
return;
}
int maxSize = 0;
// we should assign spans before item decor offsets are calculated
assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection);
for (int i = 0; i < count; i++) {
View view = mSet[i];
if (layoutState.mScrapList == null) {
if (layingOutInPrimaryDirection) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (layingOutInPrimaryDirection) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
int spanSize = getSpanSize(recycler, state, getPosition(view));
final int spec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan * spanSize,
View.MeasureSpec.EXACTLY);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (mOrientation == VERTICAL) {
measureChildWithDecorationsAndMargin(view, spec, getMainDirSpec(lp.height));
} else {
measureChildWithDecorationsAndMargin(view, getMainDirSpec(lp.width), spec);
}
final int size = mOrientationHelper.getDecoratedMeasurement(view);
if (size > maxSize) {
maxSize = size;
}
}
// views that did not measure the maxSize has to be re-measured
final int maxMeasureSpec = getMainDirSpec(maxSize);
for (int i = 0; i < count; i ++) {
final View view = mSet[i];
if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
int spanSize = getSpanSize(recycler, state, getPosition(view));
final int spec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan * spanSize,
View.MeasureSpec.EXACTLY);
if (mOrientation == VERTICAL) {
measureChildWithDecorationsAndMargin(view, spec, maxMeasureSpec);
} else {
measureChildWithDecorationsAndMargin(view, maxMeasureSpec, spec);
}
}
}
result.mConsumed = maxSize;
int left = 0, right = 0, top = 0, bottom = 0;
if (mOrientation == VERTICAL) {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = bottom - maxSize;
} else {
top = layoutState.mOffset;
bottom = top + maxSize;
}
} else {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = right - maxSize;
} else {
left = layoutState.mOffset;
right = left + maxSize;
}
}
for (int i = 0; i < count; i++) {
View view = mSet[i];
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (mOrientation == VERTICAL) {
left = getPaddingLeft() + mSizePerSpan * params.mSpanIndex;
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
top = getPaddingTop() + mSizePerSpan * params.mSpanIndex;
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
right - params.rightMargin, bottom - params.bottomMargin);
if (DEBUG) {
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
+ ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable |= view.isFocusable();
}
Arrays.fill(mSet, null);
}
private int getMainDirSpec(int dim) {
if (dim < 0) {
return MAIN_DIR_SPEC;
} else {
return View.MeasureSpec.makeMeasureSpec(dim, View.MeasureSpec.EXACTLY);
}
}
private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) {
calculateItemDecorationsForChild(child, mDecorInsets);
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mDecorInsets.left,
lp.rightMargin + mDecorInsets.right);
heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mDecorInsets.top,
lp.bottomMargin + mDecorInsets.bottom);
child.measure(widthSpec, heightSpec);
}
private int updateSpecWithExtra(int spec, int startInset, int endInset) {
if (startInset == 0 && endInset == 0) {
return spec;
}
final int mode = View.MeasureSpec.getMode(spec);
if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
return View.MeasureSpec.makeMeasureSpec(
View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
}
return spec;
}
private void assignSpans(RecyclerView.Recycler recycler, RecyclerView.State state, int count,
int consumedSpanCount, boolean layingOutInPrimaryDirection) {
int span, spanDiff, start, end, diff;
// make sure we traverse from min position to max position
if (layingOutInPrimaryDirection) {
start = 0;
end = count;
diff = 1;
} else {
start = count - 1;
end = -1;
diff = -1;
}
if (mOrientation == VERTICAL && isLayoutRTL()) { // start from last span
span = consumedSpanCount - 1;
spanDiff = -1;
} else {
span = 0;
spanDiff = 1;
}
for (int i = start; i != end; i += diff) {
View view = mSet[i];
LayoutParams params = (LayoutParams) view.getLayoutParams();
params.mSpanSize = getSpanSize(recycler, state, getPosition(view));
if (spanDiff == -1 && params.mSpanSize > 1) {
params.mSpanIndex = span - (params.mSpanSize - 1);
} else {
params.mSpanIndex = span;
}
span += spanDiff * params.mSpanSize;
}
}
/**
* Returns the number of spans laid out by this grid.
*
* @return The number of spans
* @see #setSpanCount(int)
*/
public int getSpanCount() {
return mSpanCount;
}
/**
* Sets the number of spans to be laid out.
* <p>
* If {@link #getOrientation()} is {@link #VERTICAL}, this is the number of columns.
* If {@link #getOrientation()} is {@link #HORIZONTAL}, this is the number of rows.
*
* @param spanCount The total number of spans in the grid
* @see #getSpanCount()
*/
public void setSpanCount(int spanCount) {
if (spanCount == mSpanCount) {
return;
}
if (spanCount < 1) {
throw new IllegalArgumentException("Span count should be at least 1. Provided "
+ spanCount);
}
mSpanCount = spanCount;
mSpanSizeLookup.invalidateSpanIndexCache();
}
/**
* A helper class to provide the number of spans each item occupies.
* <p>
* Default implementation sets each item to occupy exactly 1 span.
*
* @see GridLayoutManager#setSpanSizeLookup(SpanSizeLookup)
*/
public static abstract class SpanSizeLookup {
final SparseIntArray mSpanIndexCache = new SparseIntArray();
private boolean mCacheSpanIndices = false;
/**
* Returns the number of span occupied by the item at <code>position</code>.
*
* @param position The adapter position of the item
* @return The number of spans occupied by the item at the provided position
*/
abstract public int getSpanSize(int position);
/**
* Sets whether the results of {@link #getSpanIndex(int, int)} method should be cached or
* not. By default these values are not cached. If you are not overriding
* {@link #getSpanIndex(int, int)}, you should set this to true for better performance.
*
* @param cacheSpanIndices Whether results of getSpanIndex should be cached or not.
*/
public void setSpanIndexCacheEnabled(boolean cacheSpanIndices) {
mCacheSpanIndices = cacheSpanIndices;
}
/**
* Clears the span index cache. GridLayoutManager automatically calls this method when
* adapter changes occur.
*/
public void invalidateSpanIndexCache() {
mSpanIndexCache.clear();
}
/**
* Returns whether results of {@link #getSpanIndex(int, int)} method are cached or not.
*
* @return True if results of {@link #getSpanIndex(int, int)} are cached.
*/
public boolean isSpanIndexCacheEnabled() {
return mCacheSpanIndices;
}
int getCachedSpanIndex(int position, int spanCount) {
if (!mCacheSpanIndices) {
return getSpanIndex(position, spanCount);
}
final int existing = mSpanIndexCache.get(position, -1);
if (existing != -1) {
return existing;
}
final int value = getSpanIndex(position, spanCount);
mSpanIndexCache.put(position, value);
return value;
}
/**
* Returns the final span index of the provided position.
* <p>
* If you have a faster way to calculate span index for your items, you should override
* this method. Otherwise, you should enable span index cache
* ({@link #setSpanIndexCacheEnabled(boolean)}) for better performance. When caching is
* disabled, default implementation traverses all items from 0 to
* <code>position</code>. When caching is enabled, it calculates from the closest cached
* value before the <code>position</code>.
* <p>
* If you override this method, you need to make sure it is consistent with
* {@link #getSpanSize(int)}. GridLayoutManager does not call this method for
* each item. It is called only for the reference item and rest of the items
* are assigned to spans based on the reference item. For example, you cannot assign a
* position to span 2 while span 1 is empty.
* <p>
* Note that span offsets always start with 0 and are not affected by RTL.
*
* @param position The position of the item
* @param spanCount The total number of spans in the grid
* @return The final span position of the item. Should be between 0 (inclusive) and
* <code>spanCount</code>(exclusive)
*/
public int getSpanIndex(int position, int spanCount) {
int positionSpanSize = getSpanSize(position);
if (positionSpanSize == spanCount) {
return 0; // quick return for full-span items
}
int span = 0;
int startPos = 0;
// If caching is enabled, try to jump
if (mCacheSpanIndices && mSpanIndexCache.size() > 0) {
int prevKey = findReferenceIndexFromCache(position);
if (prevKey >= 0) {
span = mSpanIndexCache.get(prevKey) + getSpanSize(prevKey);
startPos = prevKey + 1;
}
}
for (int i = startPos; i < position; i++) {
int size = getSpanSize(i);
span += size;
if (span == spanCount) {
span = 0;
} else if (span > spanCount) {
// did not fit, moving to next row / column
span = size;
}
}
if (span + positionSpanSize <= spanCount) {
return span;
}
return 0;
}
int findReferenceIndexFromCache(int position) {
int lo = 0;
int hi = mSpanIndexCache.size() - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final int midVal = mSpanIndexCache.keyAt(mid);
if (midVal < position) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
int index = lo - 1;
if (index >= 0 && index < mSpanIndexCache.size()) {
return mSpanIndexCache.keyAt(index);
}
return -1;
}
/**
* Returns the index of the group this position belongs.
* <p>
* For example, if grid has 3 columns and each item occupies 1 span, span group index
* for item 1 will be 0, item 5 will be 1.
*
* @param adapterPosition The position in adapter
* @param spanCount The total number of spans in the grid
* @return The index of the span group including the item at the given adapter position
*/
public int getSpanGroupIndex(int adapterPosition, int spanCount) {
int span = 0;
int group = 0;
int positionSpanSize = getSpanSize(adapterPosition);
for (int i = 0; i < adapterPosition; i++) {
int size = getSpanSize(i);
span += size;
if (span == spanCount) {
span = 0;
group++;
} else if (span > spanCount) {
// did not fit, moving to next row / column
span = size;
group++;
}
}
if (span + positionSpanSize > spanCount) {
group++;
}
return group;
}
}
@Override
public boolean supportsPredictiveItemAnimations() {
return mPendingSavedState == null;
}
/**
* Default implementation for {@link SpanSizeLookup}. Each item occupies 1 span.
*/
public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
@Override
public int getSpanSize(int position) {
return 1;
}
@Override
public int getSpanIndex(int position, int spanCount) {
return position % spanCount;
}
}
/**
* LayoutParams used by GridLayoutManager.
*/
public static class LayoutParams extends RecyclerView.LayoutParams {
/**
* Span Id for Views that are not laid out yet.
*/
public static final int INVALID_SPAN_ID = -1;
private int mSpanIndex = INVALID_SPAN_ID;
private int mSpanSize = 0;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(RecyclerView.LayoutParams source) {
super(source);
}
/**
* Returns the current span index of this View. If the View is not laid out yet, the return
* value is <code>undefined</code>.
* <p>
* Note that span index may change by whether the RecyclerView is RTL or not. For
* example, if the number of spans is 3 and layout is RTL, the rightmost item will have
* span index of 2. If the layout changes back to LTR, span index for this view will be 0.
* If the item was occupying 2 spans, span indices would be 1 and 0 respectively.
* <p>
* If the View occupies multiple spans, span with the minimum index is returned.
*
* @return The span index of the View.
*/
public int getSpanIndex() {
return mSpanIndex;
}
/**
* Returns the number of spans occupied by this View. If the View not laid out yet, the
* return value is <code>undefined</code>.
*
* @return The number of spans occupied by this View.
*/
public int getSpanSize() {
return mSpanSize;
}
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific languag`e governing permissions and
* limitations under the License.
*/
package android.support.v7.widget;
import android.view.View;
/**
* Helper class that keeps temporary state while {LayoutManager} is filling out the empty
* space.
*/
class LayoutState {
final static String TAG = "LayoutState";
final static int LAYOUT_START = -1;
final static int LAYOUT_END = 1;
final static int INVALID_LAYOUT = Integer.MIN_VALUE;
final static int ITEM_DIRECTION_HEAD = -1;
final static int ITEM_DIRECTION_TAIL = 1;
final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE;
/**
* Number of pixels that we should fill, in the layout direction.
*/
int mAvailable;
/**
* Current position on the adapter to get the next item.
*/
int mCurrentPosition;
/**
* Defines the direction in which the data adapter is traversed.
* Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL}
*/
int mItemDirection;
/**
* Defines the direction in which the layout is filled.
* Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
*/
int mLayoutDirection;
/**
* Used if you want to pre-layout items that are not yet visible.
* The difference with {@link #mAvailable} is that, when recycling, distance rendered for
* {@link #mExtra} is not considered not to recycle visible children.
*/
int mExtra = 0;
/**
* @return true if there are more items in the data adapter
*/
boolean hasMore(RecyclerView.State state) {
return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
}
/**
* Gets the view for the next element that we should render.
* Also updates current item index to the next item, based on {@link #mItemDirection}
*
* @return The next element that we should render.
*/
View next(RecyclerView.Recycler recycler) {
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,338 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v7.widget;
import android.content.Context;
import android.graphics.PointF;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
/**
* {@link RecyclerView.SmoothScroller} implementation which uses
* {@link LinearInterpolator} until the target position becames a child of
* the RecyclerView and then uses
* {@link DecelerateInterpolator} to slowly approach to target position.
*/
abstract public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
private static final String TAG = "LinearSmoothScroller";
private static final boolean DEBUG = false;
private static final float MILLISECONDS_PER_INCH = 25f;
private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
/**
* Align child view's left or top with parent view's left or top
*
* @see #calculateDtToFit(int, int, int, int, int)
* @see #calculateDxToMakeVisible(View, int)
* @see #calculateDyToMakeVisible(View, int)
*/
public static final int SNAP_TO_START = -1;
/**
* Align child view's right or bottom with parent view's right or bottom
*
* @see #calculateDtToFit(int, int, int, int, int)
* @see #calculateDxToMakeVisible(View, int)
* @see #calculateDyToMakeVisible(View, int)
*/
public static final int SNAP_TO_END = 1;
/**
* <p>Decides if the child should be snapped from start or end, depending on where it
* currently is in relation to its parent.</p>
* <p>For instance, if the view is virtually on the left of RecyclerView, using
* {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}</p>
*
* @see #calculateDtToFit(int, int, int, int, int)
* @see #calculateDxToMakeVisible(View, int)
* @see #calculateDyToMakeVisible(View, int)
*/
public static final int SNAP_TO_ANY = 0;
// Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target
// view is not laid out until interim target position is reached, we can detect the case before
// scrolling slows down and reschedule another interim target scroll
private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f;
protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator();
protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
protected PointF mTargetVector;
private final float MILLISECONDS_PER_PX;
// Temporary variables to keep track of the interim scroll target. These values do not
// point to a real item position, rather point to an estimated location pixels.
protected int mInterimTargetDx = 0, mInterimTargetDy = 0;
public LinearSmoothScroller(Context context) {
MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
}
/**
* {@inheritDoc}
*/
@Override
protected void onStart() {
}
/**
* {@inheritDoc}
*/
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
final int distance = (int) Math.sqrt(dx * dx + dy * dy);
final int time = calculateTimeForDeceleration(distance);
if (time > 0) {
action.update(-dx, -dy, time, mDecelerateInterpolator);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
if (getChildCount() == 0) {
stop();
return;
}
if (DEBUG && mTargetVector != null
&& ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
throw new IllegalStateException("Scroll happened in the opposite direction"
+ " of the target. Some calculations are wrong");
}
mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);
if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
updateActionForInterimTarget(action);
} // everything is valid, keep going
}
/**
* {@inheritDoc}
*/
@Override
protected void onStop() {
mInterimTargetDx = mInterimTargetDy = 0;
mTargetVector = null;
}
/**
* Calculates the scroll speed.
*
* @param displayMetrics DisplayMetrics to be used for real dimension calculations
* @return The time (in ms) it should take for each pixel. For instance, if returned value is
* 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
*/
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
/**
* <p>Calculates the time for deceleration so that transition from LinearInterpolator to
* DecelerateInterpolator looks smooth.</p>
*
* @param dx Distance to scroll
* @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
* from LinearInterpolation
*/
protected int calculateTimeForDeceleration(int dx) {
// we want to cover same area with the linear interpolator for the first 10% of the
// interpolation. After that, deceleration will take control.
// area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
// which gives 0.100028 when x = .3356
// this is why we divide linear scrolling time with .3356
return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
}
/**
* Calculates the time it should take to scroll the given distance (in pixels)
*
* @param dx Distance in pixels that we want to scroll
* @return Time in milliseconds
* @see #calculateSpeedPerPixel(DisplayMetrics)
*/
protected int calculateTimeForScrolling(int dx) {
// In a case where dx is very small, rounding may return 0 although dx > 0.
// To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
// time.
return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
}
/**
* When scrolling towards a child view, this method defines whether we should align the left
* or the right edge of the child with the parent RecyclerView.
*
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
* @see #SNAP_TO_START
* @see #SNAP_TO_END
* @see #SNAP_TO_ANY
*/
protected int getHorizontalSnapPreference() {
return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
}
/**
* When scrolling towards a child view, this method defines whether we should align the top
* or the bottom edge of the child with the parent RecyclerView.
*
* @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
* @see #SNAP_TO_START
* @see #SNAP_TO_END
* @see #SNAP_TO_ANY
*/
protected int getVerticalSnapPreference() {
return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
}
/**
* When the target scroll position is not a child of the RecyclerView, this method calculates
* a direction vector towards that child and triggers a smooth scroll.
*
* @see #computeScrollVectorForPosition(int)
*/
protected void updateActionForInterimTarget(Action action) {
// find an interim target position
PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
Log.e(TAG, "To support smooth scrolling, you should override \n"
+ "LayoutManager#computeScrollVectorForPosition.\n"
+ "Falling back to instant scroll");
final int target = getTargetPosition();
stop();
instantScrollToPosition(target);
return;
}
normalize(scrollVector);
mTargetVector = scrollVector;
mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
// To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
// interim target. Since we track the distance travelled in onSeekTargetStep callback, it
// won't actually scroll more than what we need.
action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO)
, (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO)
, (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
}
private int clampApplyScroll(int tmpDt, int dt) {
final int before = tmpDt;
tmpDt -= dt;
if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
return 0;
}
return tmpDt;
}
/**
* Helper method for {@link #calculateDxToMakeVisible(View, int)} and
* {@link #calculateDyToMakeVisible(View, int)}
*/
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
snapPreference) {
switch (snapPreference) {
case SNAP_TO_START:
return boxStart - viewStart;
case SNAP_TO_END:
return boxEnd - viewEnd;
case SNAP_TO_ANY:
final int dtStart = boxStart - viewStart;
if (dtStart > 0) {
return dtStart;
}
final int dtEnd = boxEnd - viewEnd;
if (dtEnd < 0) {
return dtEnd;
}
break;
default:
throw new IllegalArgumentException("snap preference should be one of the"
+ " constants defined in SmoothScroller, starting with SNAP_");
}
return 0;
}
/**
* Calculates the vertical scroll amount necessary to make the given view fully visible
* inside the RecyclerView.
*
* @param view The view which we want to make fully visible
* @param snapPreference The edge which the view should snap to when entering the visible
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
* {@link #SNAP_TO_END}.
* @return The vertical scroll amount necessary to make the view visible with the given
* snap preference.
*/
public int calculateDyToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (!layoutManager.canScrollVertically()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
final int start = layoutManager.getPaddingTop();
final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
return calculateDtToFit(top, bottom, start, end, snapPreference);
}
/**
* Calculates the horizontal scroll amount necessary to make the given view fully visible
* inside the RecyclerView.
*
* @param view The view which we want to make fully visible
* @param snapPreference The edge which the view should snap to when entering the visible
* area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
* {@link #SNAP_TO_END}
* @return The vertical scroll amount necessary to make the view visible with the given
* snap preference.
*/
public int calculateDxToMakeVisible(View view, int snapPreference) {
final RecyclerView.LayoutManager layoutManager = getLayoutManager();
if (!layoutManager.canScrollHorizontally()) {
return 0;
}
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
final int start = layoutManager.getPaddingLeft();
final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
return calculateDtToFit(left, right, start, end, snapPreference);
}
abstract public PointF computeScrollVectorForPosition(int targetPosition);
}

View File

@ -0,0 +1,238 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v7.widget;
import android.support.v7.widget.AdapterHelper.UpdateOp;
import java.util.List;
import static android.support.v7.widget.AdapterHelper.UpdateOp.ADD;
import static android.support.v7.widget.AdapterHelper.UpdateOp.MOVE;
import static android.support.v7.widget.AdapterHelper.UpdateOp.REMOVE;
import static android.support.v7.widget.AdapterHelper.UpdateOp.UPDATE;
class OpReorderer {
final Callback mCallback;
public OpReorderer(Callback callback) {
mCallback = callback;
}
void reorderOps(List<UpdateOp> ops) {
// since move operations breaks continuity, their effects on ADD/RM are hard to handle.
// we push them to the end of the list so that they can be handled easily.
int badMove;
while ((badMove = getLastMoveOutOfOrder(ops)) != -1) {
swapMoveOp(ops, badMove, badMove + 1);
}
}
private void swapMoveOp(List<UpdateOp> list, int badMove, int next) {
final UpdateOp moveOp = list.get(badMove);
final UpdateOp nextOp = list.get(next);
switch (nextOp.cmd) {
case REMOVE:
swapMoveRemove(list, badMove, moveOp, next, nextOp);
break;
case ADD:
swapMoveAdd(list, badMove, moveOp, next, nextOp);
break;
case UPDATE:
swapMoveUpdate(list, badMove, moveOp, next, nextOp);
break;
}
}
void swapMoveRemove(List<UpdateOp> list, int movePos, UpdateOp moveOp,
int removePos, UpdateOp removeOp) {
UpdateOp extraRm = null;
// check if move is nulled out by remove
boolean revertedMove = false;
final boolean moveIsBackwards;
if (moveOp.positionStart < moveOp.itemCount) {
moveIsBackwards = false;
if (removeOp.positionStart == moveOp.positionStart
&& removeOp.itemCount == moveOp.itemCount - moveOp.positionStart) {
revertedMove = true;
}
} else {
moveIsBackwards = true;
if (removeOp.positionStart == moveOp.itemCount + 1 &&
removeOp.itemCount == moveOp.positionStart - moveOp.itemCount) {
revertedMove = true;
}
}
// going in reverse, first revert the effect of add
if (moveOp.itemCount < removeOp.positionStart) {
removeOp.positionStart--;
} else if (moveOp.itemCount < removeOp.positionStart + removeOp.itemCount) {
// move is removed.
removeOp.itemCount --;
moveOp.cmd = REMOVE;
moveOp.itemCount = 1;
if (removeOp.itemCount == 0) {
list.remove(removePos);
mCallback.recycleUpdateOp(removeOp);
}
// no need to swap, it is already a remove
return;
}
// now affect of add is consumed. now apply effect of first remove
if (moveOp.positionStart <= removeOp.positionStart) {
removeOp.positionStart++;
} else if (moveOp.positionStart < removeOp.positionStart + removeOp.itemCount) {
final int remaining = removeOp.positionStart + removeOp.itemCount
- moveOp.positionStart;
extraRm = mCallback.obtainUpdateOp(REMOVE, moveOp.positionStart + 1, remaining);
removeOp.itemCount = moveOp.positionStart - removeOp.positionStart;
}
// if effects of move is reverted by remove, we are done.
if (revertedMove) {
list.set(movePos, removeOp);
list.remove(removePos);
mCallback.recycleUpdateOp(moveOp);
return;
}
// now find out the new locations for move actions
if (moveIsBackwards) {
if (extraRm != null) {
if (moveOp.positionStart > extraRm.positionStart) {
moveOp.positionStart -= extraRm.itemCount;
}
if (moveOp.itemCount > extraRm.positionStart) {
moveOp.itemCount -= extraRm.itemCount;
}
}
if (moveOp.positionStart > removeOp.positionStart) {
moveOp.positionStart -= removeOp.itemCount;
}
if (moveOp.itemCount > removeOp.positionStart) {
moveOp.itemCount -= removeOp.itemCount;
}
} else {
if (extraRm != null) {
if (moveOp.positionStart >= extraRm.positionStart) {
moveOp.positionStart -= extraRm.itemCount;
}
if (moveOp.itemCount >= extraRm.positionStart) {
moveOp.itemCount -= extraRm.itemCount;
}
}
if (moveOp.positionStart >= removeOp.positionStart) {
moveOp.positionStart -= removeOp.itemCount;
}
if (moveOp.itemCount >= removeOp.positionStart) {
moveOp.itemCount -= removeOp.itemCount;
}
}
list.set(movePos, removeOp);
if (moveOp.positionStart != moveOp.itemCount) {
list.set(removePos, moveOp);
} else {
list.remove(removePos);
}
if (extraRm != null) {
list.add(movePos, extraRm);
}
}
private void swapMoveAdd(List<UpdateOp> list, int move, UpdateOp moveOp, int add,
UpdateOp addOp) {
int offset = 0;
// going in reverse, first revert the effect of add
if (moveOp.itemCount < addOp.positionStart) {
offset--;
}
if (moveOp.positionStart < addOp.positionStart) {
offset++;
}
if (addOp.positionStart <= moveOp.positionStart) {
moveOp.positionStart += addOp.itemCount;
}
if (addOp.positionStart <= moveOp.itemCount) {
moveOp.itemCount += addOp.itemCount;
}
addOp.positionStart += offset;
list.set(move, addOp);
list.set(add, moveOp);
}
void swapMoveUpdate(List<UpdateOp> list, int move, UpdateOp moveOp, int update,
UpdateOp updateOp) {
UpdateOp extraUp1 = null;
UpdateOp extraUp2 = null;
// going in reverse, first revert the effect of add
if (moveOp.itemCount < updateOp.positionStart) {
updateOp.positionStart--;
} else if (moveOp.itemCount < updateOp.positionStart + updateOp.itemCount) {
// moved item is updated. add an update for it
updateOp.itemCount--;
extraUp1 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart, 1);
}
// now affect of add is consumed. now apply effect of first remove
if (moveOp.positionStart <= updateOp.positionStart) {
updateOp.positionStart++;
} else if (moveOp.positionStart < updateOp.positionStart + updateOp.itemCount) {
final int remaining = updateOp.positionStart + updateOp.itemCount
- moveOp.positionStart;
extraUp2 = mCallback.obtainUpdateOp(UPDATE, moveOp.positionStart + 1, remaining);
updateOp.itemCount -= remaining;
}
list.set(update, moveOp);
if (updateOp.itemCount > 0) {
list.set(move, updateOp);
} else {
list.remove(move);
mCallback.recycleUpdateOp(updateOp);
}
if (extraUp1 != null) {
list.add(move, extraUp1);
}
if (extraUp2 != null) {
list.add(move, extraUp2);
}
}
private int getLastMoveOutOfOrder(List<UpdateOp> list) {
boolean foundNonMove = false;
for (int i = list.size() - 1; i >= 0; i--) {
final UpdateOp op1 = list.get(i);
if (op1.cmd == MOVE) {
if (foundNonMove) {
return i;
}
} else {
foundNonMove = true;
}
}
return -1;
}
static interface Callback {
UpdateOp obtainUpdateOp(int cmd, int startPosition, int itemCount);
void recycleUpdateOp(UpdateOp op);
}
}

View File

@ -0,0 +1,338 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v7.widget;
import android.view.View;
import android.widget.LinearLayout;
/**
* Helper class for LayoutManagers to abstract measurements depending on the View's orientation.
* <p>
* It is developed to easily support vertical and horizontal orientations in a LayoutManager but
* can also be used to abstract calls around view bounds and child measurements with margins and
* decorations.
*
* @see #createHorizontalHelper(RecyclerView.LayoutManager)
* @see #createVerticalHelper(RecyclerView.LayoutManager)
*/
public abstract class OrientationHelper {
private static final int INVALID_SIZE = Integer.MIN_VALUE;
protected final RecyclerView.LayoutManager mLayoutManager;
public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
public static final int VERTICAL = LinearLayout.VERTICAL;
private int mLastTotalSpace = INVALID_SIZE;
private OrientationHelper(RecyclerView.LayoutManager layoutManager) {
mLayoutManager = layoutManager;
}
/**
* Call this method after onLayout method is complete if state is NOT pre-layout.
* This method records information like layout bounds that might be useful in the next layout
* calculations.
*/
public void onLayoutComplete() {
mLastTotalSpace = getTotalSpace();
}
/**
* Returns the layout space change between the previous layout pass and current layout pass.
* <p>
* Make sure you call {@link #onLayoutComplete()} at the end of your LayoutManager's
* {@link RecyclerView.LayoutManager#onLayoutChildren(RecyclerView.Recycler,
* RecyclerView.State)} method.
*
* @return The difference between the current total space and previous layout's total space.
* @see #onLayoutComplete()
*/
public int getTotalSpaceChange() {
return INVALID_SIZE == mLastTotalSpace ? 0 : getTotalSpace() - mLastTotalSpace;
}
/**
* Returns the start of the view including its decoration and margin.
* <p>
* For example, for the horizontal helper, if a View's left is at pixel 20, has 2px left
* decoration and 3px left margin, returned value will be 15px.
*
* @param view The view element to check
* @return The first pixel of the element
* @see #getDecoratedEnd(View)
*/
public abstract int getDecoratedStart(View view);
/**
* Returns the end of the view including its decoration and margin.
* <p>
* For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right
* decoration and 3px right margin, returned value will be 205.
*
* @param view The view element to check
* @return The last pixel of the element
* @see #getDecoratedStart(View)
*/
public abstract int getDecoratedEnd(View view);
/**
* Returns the space occupied by this View in the current orientation including decorations and
* margins.
*
* @param view The view element to check
* @return Total space occupied by this view
* @see #getDecoratedMeasurementInOther(View)
*/
public abstract int getDecoratedMeasurement(View view);
/**
* Returns the space occupied by this View in the perpendicular orientation including
* decorations and margins.
*
* @param view The view element to check
* @return Total space occupied by this view in the perpendicular orientation to current one
* @see #getDecoratedMeasurement(View)
*/
public abstract int getDecoratedMeasurementInOther(View view);
/**
* Returns the start position of the layout after the start padding is added.
*
* @return The very first pixel we can draw.
*/
public abstract int getStartAfterPadding();
/**
* Returns the end position of the layout after the end padding is removed.
*
* @return The end boundary for this layout.
*/
public abstract int getEndAfterPadding();
/**
* Returns the end position of the layout without taking padding into account.
*
* @return The end boundary for this layout without considering padding.
*/
public abstract int getEnd();
/**
* Offsets all children's positions by the given amount.
*
* @param amount Value to add to each child's layout parameters
*/
public abstract void offsetChildren(int amount);
/**
* Returns the total space to layout. This number is the difference between
* {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}.
*
* @return Total space to layout children
*/
public abstract int getTotalSpace();
/**
* Offsets the child in this orientation.
*
* @param view View to offset
* @param offset offset amount
*/
public abstract void offsetChild(View view, int offset);
/**
* Returns the padding at the end of the layout. For horizontal helper, this is the right
* padding and for vertical helper, this is the bottom padding. This method does not check
* whether the layout is RTL or not.
*
* @return The padding at the end of the layout.
*/
public abstract int getEndPadding();
/**
* Creates an OrientationHelper for the given LayoutManager and orientation.
*
* @param layoutManager LayoutManager to attach to
* @param orientation Desired orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}
* @return A new OrientationHelper
*/
public static OrientationHelper createOrientationHelper(
RecyclerView.LayoutManager layoutManager, int orientation) {
switch (orientation) {
case HORIZONTAL:
return createHorizontalHelper(layoutManager);
case VERTICAL:
return createVerticalHelper(layoutManager);
}
throw new IllegalArgumentException("invalid orientation");
}
/**
* Creates a horizontal OrientationHelper for the given LayoutManager.
*
* @param layoutManager The LayoutManager to attach to.
* @return A new OrientationHelper
*/
public static OrientationHelper createHorizontalHelper(
RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
@Override
public int getEndAfterPadding() {
return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
}
@Override
public int getEnd() {
return mLayoutManager.getWidth();
}
@Override
public void offsetChildren(int amount) {
mLayoutManager.offsetChildrenHorizontal(amount);
}
@Override
public int getStartAfterPadding() {
return mLayoutManager.getPaddingLeft();
}
@Override
public int getDecoratedMeasurement(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
@Override
public int getDecoratedMeasurementInOther(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}
@Override
public int getDecoratedEnd(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedRight(view) + params.rightMargin;
}
@Override
public int getDecoratedStart(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
}
@Override
public int getTotalSpace() {
return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
- mLayoutManager.getPaddingRight();
}
@Override
public void offsetChild(View view, int offset) {
view.offsetLeftAndRight(offset);
}
@Override
public int getEndPadding() {
return mLayoutManager.getPaddingRight();
}
};
}
/**
* Creates a vertical OrientationHelper for the given LayoutManager.
*
* @param layoutManager The LayoutManager to attach to.
* @return A new OrientationHelper
*/
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
@Override
public int getEndAfterPadding() {
return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
}
@Override
public int getEnd() {
return mLayoutManager.getHeight();
}
@Override
public void offsetChildren(int amount) {
mLayoutManager.offsetChildrenVertical(amount);
}
@Override
public int getStartAfterPadding() {
return mLayoutManager.getPaddingTop();
}
@Override
public int getDecoratedMeasurement(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin;
}
@Override
public int getDecoratedMeasurementInOther(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ params.rightMargin;
}
@Override
public int getDecoratedEnd(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
}
@Override
public int getDecoratedStart(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
view.getLayoutParams();
return mLayoutManager.getDecoratedTop(view) - params.topMargin;
}
@Override
public int getTotalSpace() {
return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
- mLayoutManager.getPaddingBottom();
}
@Override
public void offsetChild(View view, int offset) {
view.offsetTopAndBottom(offset);
}
@Override
public int getEndPadding() {
return mLayoutManager.getPaddingBottom();
}
};
}
}

View File

@ -0,0 +1,460 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v7.widget;
import java.util.ArrayList;
/**
* Like a SparseArray, but with the ability to offset key ranges for bulk insertions/deletions.
*/
class PositionMap<E> implements Cloneable {
private static final Object DELETED = new Object();
private boolean mGarbage = false;
private int[] mKeys;
private Object[] mValues;
private int mSize;
/**
* Creates a new SparseArray containing no mappings.
*/
public PositionMap() {
this(10);
}
/**
* Creates a new PositionMap containing no mappings that will not
* require any additional memory allocation to store the specified
* number of mappings. If you supply an initial capacity of 0, the
* sparse array will be initialized with a light-weight representation
* not requiring any additional array allocations.
*/
public PositionMap(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = ContainerHelpers.EMPTY_INTS;
mValues = ContainerHelpers.EMPTY_OBJECTS;
} else {
initialCapacity = idealIntArraySize(initialCapacity);
mKeys = new int[initialCapacity];
mValues = new Object[initialCapacity];
}
mSize = 0;
}
@Override
@SuppressWarnings("unchecked")
public PositionMap<E> clone() {
PositionMap<E> clone = null;
try {
clone = (PositionMap<E>) super.clone();
clone.mKeys = mKeys.clone();
clone.mValues = mValues.clone();
} catch (CloneNotSupportedException cnse) {
/* ignore */
}
return clone;
}
/**
* Gets the Object mapped from the specified key, or <code>null</code>
* if no such mapping has been made.
*/
public E get(int key) {
return get(key, null);
}
/**
* Gets the Object mapped from the specified key, or the specified Object
* if no such mapping has been made.
*/
@SuppressWarnings("unchecked")
public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {
return (E) mValues[i];
}
}
/**
* Removes the mapping from the specified key, if there was any.
*/
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
/**
* Alias for {@link #delete(int)}.
*/
public void remove(int key) {
delete(key);
}
/**
* Removes the mapping at the specified index.
*/
public void removeAt(int index) {
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}
/**
* Remove a range of mappings as a batch.
*
* @param index Index to begin at
* @param size Number of mappings to remove
*/
public void removeAtRange(int index, int size) {
final int end = Math.min(mSize, index + size);
for (int i = index; i < end; i++) {
removeAt(i);
}
}
public void insertKeyRange(int keyStart, int count) {
}
public void removeKeyRange(ArrayList<E> removedItems, int keyStart, int count) {
}
private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
int n = mSize;
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
Object val = values[i];
if (val != DELETED) {
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
mGarbage = false;
mSize = o;
// Log.e("SparseArray", "gc end with " + mSize);
}
/**
* Adds a mapping from the specified key to the specified value,
* replacing the previous mapping from the specified key if there
* was one.
*/
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
mValues[i] = value;
} else {
i = ~i;
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
if (mSize >= mKeys.length) {
int n = idealIntArraySize(mSize + 1);
int[] nkeys = new int[n];
Object[] nvalues = new Object[n];
// Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
mKeys = nkeys;
mValues = nvalues;
}
if (mSize - i != 0) {
// Log.e("SparseArray", "move " + (mSize - i));
System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
}
mKeys[i] = key;
mValues[i] = value;
mSize++;
}
}
/**
* Returns the number of key-value mappings that this SparseArray
* currently stores.
*/
public int size() {
if (mGarbage) {
gc();
}
return mSize;
}
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the key from the <code>index</code>th key-value mapping that this
* SparseArray stores.
*/
public int keyAt(int index) {
if (mGarbage) {
gc();
}
return mKeys[index];
}
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the value from the <code>index</code>th key-value mapping that this
* SparseArray stores.
*/
@SuppressWarnings("unchecked")
public E valueAt(int index) {
if (mGarbage) {
gc();
}
return (E) mValues[index];
}
/**
* Given an index in the range <code>0...size()-1</code>, sets a new
* value for the <code>index</code>th key-value mapping that this
* SparseArray stores.
*/
public void setValueAt(int index, E value) {
if (mGarbage) {
gc();
}
mValues[index] = value;
}
/**
* Returns the index for which {@link #keyAt} would return the
* specified key, or a negative number if the specified
* key is not mapped.
*/
public int indexOfKey(int key) {
if (mGarbage) {
gc();
}
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
/**
* Returns an index for which {@link #valueAt} would return the
* specified key, or a negative number if no keys map to the
* specified value.
* <p>Beware that this is a linear search, unlike lookups by key,
* and that multiple keys can map to the same value and this will
* find only one of them.
* <p>Note also that unlike most collections' {@code indexOf} methods,
* this method compares values using {@code ==} rather than {@code equals}.
*/
public int indexOfValue(E value) {
if (mGarbage) {
gc();
}
for (int i = 0; i < mSize; i++)
if (mValues[i] == value)
return i;
return -1;
}
/**
* Removes all key-value mappings from this SparseArray.
*/
public void clear() {
int n = mSize;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
values[i] = null;
}
mSize = 0;
mGarbage = false;
}
/**
* Puts a key/value pair into the array, optimizing for the case where
* the key is greater than all existing keys in the array.
*/
public void append(int key, E value) {
if (mSize != 0 && key <= mKeys[mSize - 1]) {
put(key, value);
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc();
}
int pos = mSize;
if (pos >= mKeys.length) {
int n = idealIntArraySize(pos + 1);
int[] nkeys = new int[n];
Object[] nvalues = new Object[n];
// Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
mKeys = nkeys;
mValues = nvalues;
}
mKeys[pos] = key;
mValues[pos] = value;
mSize = pos + 1;
}
/**
* {@inheritDoc}
*
* <p>This implementation composes a string by iterating over its mappings. If
* this map contains itself as a value, the string "(this Map)"
* will appear in its place.
*/
@Override
public String toString() {
if (size() <= 0) {
return "{}";
}
StringBuilder buffer = new StringBuilder(mSize * 28);
buffer.append('{');
for (int i=0; i<mSize; i++) {
if (i > 0) {
buffer.append(", ");
}
int key = keyAt(i);
buffer.append(key);
buffer.append('=');
Object value = valueAt(i);
if (value != this) {
buffer.append(value);
} else {
buffer.append("(this Map)");
}
}
buffer.append('}');
return buffer.toString();
}
static int idealByteArraySize(int need) {
for (int i = 4; i < 32; i++)
if (need <= (1 << i) - 12)
return (1 << i) - 12;
return need;
}
static int idealBooleanArraySize(int need) {
return idealByteArraySize(need);
}
static int idealShortArraySize(int need) {
return idealByteArraySize(need * 2) / 2;
}
static int idealCharArraySize(int need) {
return idealByteArraySize(need * 2) / 2;
}
static int idealIntArraySize(int need) {
return idealByteArraySize(need * 4) / 4;
}
static int idealFloatArraySize(int need) {
return idealByteArraySize(need * 4) / 4;
}
static int idealObjectArraySize(int need) {
return idealByteArraySize(need * 4) / 4;
}
static int idealLongArraySize(int need) {
return idealByteArraySize(need * 8) / 8;
}
static class ContainerHelpers {
static final boolean[] EMPTY_BOOLEANS = new boolean[0];
static final int[] EMPTY_INTS = new int[0];
static final long[] EMPTY_LONGS = new long[0];
static final Object[] EMPTY_OBJECTS = new Object[0];
// This is Arrays.binarySearch(), but doesn't do any argument validation.
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final int midVal = array[mid];
if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return mid; // value found
}
}
return ~lo; // value not present
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,97 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v7.widget;
import android.os.Bundle;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
/**
* The AccessibilityDelegate used by RecyclerView.
* <p>
* This class handles basic accessibility actions and delegates them to LayoutManager.
*/
public class RecyclerViewAccessibilityDelegate extends AccessibilityDelegateCompat {
final RecyclerView mRecyclerView;
public RecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
mRecyclerView = recyclerView;
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (super.performAccessibilityAction(host, action, args)) {
return true;
}
if (mRecyclerView.getLayoutManager() != null) {
return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args);
}
return false;
}
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName(RecyclerView.class.getName());
if (mRecyclerView.getLayoutManager() != null) {
mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info);
}
}
@Override
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(host, event);
event.setClassName(RecyclerView.class.getName());
if (host instanceof RecyclerView) {
RecyclerView rv = (RecyclerView) host;
if (rv.getLayoutManager() != null) {
rv.getLayoutManager().onInitializeAccessibilityEvent(event);
}
}
}
AccessibilityDelegateCompat getItemDelegate() {
return mItemDelegate;
}
final AccessibilityDelegateCompat mItemDelegate = new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
if (mRecyclerView.getLayoutManager() != null) {
mRecyclerView.getLayoutManager().
onInitializeAccessibilityNodeInfoForItem(host, info);
}
}
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (super.performAccessibilityAction(host, action, args)) {
return true;
}
if (mRecyclerView.getLayoutManager() != null) {
return mRecyclerView.getLayoutManager().
performAccessibilityActionForItem(host, action, args);
}
return false;
}
};
}

View File

@ -0,0 +1,94 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v7.widget;
import android.view.View;
/**
* A helper class to do scroll offset calculations.
*/
class ScrollbarHelper {
/**
* @param startChild View closest to start of the list. (top or left)
* @param endChild View closest to end of the list (bottom or right)
*/
static int computeScrollOffset(RecyclerView.State state, OrientationHelper orientation,
View startChild, View endChild, RecyclerView.LayoutManager lm,
boolean smoothScrollbarEnabled, boolean reverseLayout) {
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
endChild == null) {
return 0;
}
final int minPosition = Math.min(lm.getPosition(startChild), lm.getPosition(endChild));
final int maxPosition = Math.max(lm.getPosition(startChild), lm.getPosition(endChild));
final int itemsBefore = reverseLayout
? Math.max(0, state.getItemCount() - maxPosition - 1)
: Math.max(0, minPosition - 1);
if (!smoothScrollbarEnabled) {
return itemsBefore;
}
final int laidOutArea = Math.abs(orientation.getDecoratedEnd(endChild) -
orientation.getDecoratedStart(startChild));
final int itemRange = Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1;
final float avgSizePerRow = (float) laidOutArea / itemRange;
return Math.round(itemsBefore * avgSizePerRow + (orientation.getStartAfterPadding()
- orientation.getDecoratedStart(startChild)));
}
/**
* @param startChild View closest to start of the list. (top or left)
* @param endChild View closest to end of the list (bottom or right)
*/
static int computeScrollExtent(RecyclerView.State state, OrientationHelper orientation,
View startChild, View endChild, RecyclerView.LayoutManager lm,
boolean smoothScrollbarEnabled) {
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
endChild == null) {
return 0;
}
if (!smoothScrollbarEnabled) {
return Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild)) + 1;
}
final int extend = orientation.getDecoratedEnd(endChild)
- orientation.getDecoratedStart(startChild);
return Math.min(orientation.getTotalSpace(), extend);
}
/**
* @param startChild View closest to start of the list. (top or left)
* @param endChild View closest to end of the list (bottom or right)
*/
static int computeScrollRange(RecyclerView.State state, OrientationHelper orientation,
View startChild, View endChild, RecyclerView.LayoutManager lm,
boolean smoothScrollbarEnabled) {
if (lm.getChildCount() == 0 || state.getItemCount() == 0 || startChild == null ||
endChild == null) {
return 0;
}
if (!smoothScrollbarEnabled) {
return state.getItemCount();
}
// smooth scrollbar enabled. try to estimate better.
final int laidOutArea = orientation.getDecoratedEnd(endChild) -
orientation.getDecoratedStart(startChild);
final int laidOutRange = Math.abs(lm.getPosition(startChild) - lm.getPosition(endChild))
+ 1;
// estimate a size for full list.
return (int) ((float) laidOutArea / laidOutRange * state.getItemCount());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,159 +0,0 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.swiperefreshlayout.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.view.View;
import android.view.animation.Animation;
import android.widget.ImageView;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
/**
* Private class created to work around issues with AnimationListeners being
* called before the animation is actually complete and support shadows on older
* platforms.
*/
class CircleImageView extends ImageView {
private static final int KEY_SHADOW_COLOR = 0x1E000000;
private static final int FILL_SHADOW_COLOR = 0x3D000000;
// PX
private static final float X_OFFSET = 0f;
private static final float Y_OFFSET = 1.75f;
private static final float SHADOW_RADIUS = 3.5f;
private static final int SHADOW_ELEVATION = 4;
private Animation.AnimationListener mListener;
int mShadowRadius;
CircleImageView(Context context, int color) {
super(context);
final float density = getContext().getResources().getDisplayMetrics().density;
final int shadowYOffset = (int) (density * Y_OFFSET);
final int shadowXOffset = (int) (density * X_OFFSET);
mShadowRadius = (int) (density * SHADOW_RADIUS);
ShapeDrawable circle;
if (elevationSupported()) {
circle = new ShapeDrawable(new OvalShape());
ViewCompat.setElevation(this, SHADOW_ELEVATION * density);
} else {
OvalShape oval = new OvalShadow(mShadowRadius);
circle = new ShapeDrawable(oval);
setLayerType(View.LAYER_TYPE_SOFTWARE, circle.getPaint());
circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset,
KEY_SHADOW_COLOR);
final int padding = mShadowRadius;
// set padding so the inner image sits correctly within the shadow.
setPadding(padding, padding, padding, padding);
}
circle.getPaint().setColor(color);
ViewCompat.setBackground(this, circle);
}
private boolean elevationSupported() {
return android.os.Build.VERSION.SDK_INT >= 21;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!elevationSupported()) {
setMeasuredDimension(getMeasuredWidth() + mShadowRadius * 2, getMeasuredHeight()
+ mShadowRadius * 2);
}
}
public void setAnimationListener(Animation.AnimationListener listener) {
mListener = listener;
}
@Override
public void onAnimationStart() {
super.onAnimationStart();
if (mListener != null) {
mListener.onAnimationStart(getAnimation());
}
}
@Override
public void onAnimationEnd() {
super.onAnimationEnd();
if (mListener != null) {
mListener.onAnimationEnd(getAnimation());
}
}
/**
* Update the background color of the circle image view.
*
* @param colorRes Id of a color resource.
*/
public void setBackgroundColorRes(int colorRes) {
setBackgroundColor(ContextCompat.getColor(getContext(), colorRes));
}
@Override
public void setBackgroundColor(int color) {
if (getBackground() instanceof ShapeDrawable) {
((ShapeDrawable) getBackground()).getPaint().setColor(color);
}
}
private class OvalShadow extends OvalShape {
private RadialGradient mRadialGradient;
private Paint mShadowPaint;
OvalShadow(int shadowRadius) {
super();
mShadowPaint = new Paint();
mShadowRadius = shadowRadius;
updateRadialGradient((int) rect().width());
}
@Override
protected void onResize(float width, float height) {
super.onResize(width, height);
updateRadialGradient((int) width);
}
@Override
public void draw(Canvas canvas, Paint paint) {
final int viewWidth = CircleImageView.this.getWidth();
final int viewHeight = CircleImageView.this.getHeight();
canvas.drawCircle(viewWidth / 2, viewHeight / 2, viewWidth / 2, mShadowPaint);
canvas.drawCircle(viewWidth / 2, viewHeight / 2, viewWidth / 2 - mShadowRadius, paint);
}
private void updateRadialGradient(int diameter) {
mRadialGradient = new RadialGradient(diameter / 2, diameter / 2,
mShadowRadius, new int[] { FILL_SHADOW_COLOR, Color.TRANSPARENT },
null, Shader.TileMode.CLAMP);
mShadowPaint.setShader(mRadialGradient);
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
import android.content.Intent;
/**
* Error indicating that there was an authentication failure when performing a Request.
*/
@SuppressWarnings("serial")
public class AuthFailureError extends VolleyError {
/** An intent that can be used to resolve this exception. (Brings up the password dialog.) */
private Intent mResolutionIntent;
public AuthFailureError() { }
public AuthFailureError(Intent intent) {
mResolutionIntent = intent;
}
public AuthFailureError(NetworkResponse response) {
super(response);
}
public AuthFailureError(String message) {
super(message);
}
public AuthFailureError(String message, Exception reason) {
super(message, reason);
}
public Intent getResolutionIntent() {
return mResolutionIntent;
}
@Override
public String getMessage() {
if (mResolutionIntent != null) {
return "User needs to (re)enter credentials.";
}
return super.getMessage();
}
}

View File

@ -0,0 +1,97 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
import java.util.Collections;
import java.util.Map;
/**
* An interface for a cache keyed by a String with a byte array as data.
*/
public interface Cache {
/**
* Retrieves an entry from the cache.
* @param key Cache key
* @return An {@link Entry} or null in the event of a cache miss
*/
public Entry get(String key);
/**
* Adds or replaces an entry to the cache.
* @param key Cache key
* @param entry Data to store and metadata for cache coherency, TTL, etc.
*/
public void put(String key, Entry entry);
/**
* Performs any potentially long-running actions needed to initialize the cache;
* will be called from a worker thread.
*/
public void initialize();
/**
* Invalidates an entry in the cache.
* @param key Cache key
* @param fullExpire True to fully expire the entry, false to soft expire
*/
public void invalidate(String key, boolean fullExpire);
/**
* Removes an entry from the cache.
* @param key Cache key
*/
public void remove(String key);
/**
* Empties the cache.
*/
public void clear();
/**
* Data and metadata for an entry returned by the cache.
*/
public static class Entry {
/** The data returned from cache. */
public byte[] data;
/** ETag for cache coherency. */
public String etag;
/** Date of this response as reported by the server. */
public long serverDate;
/** TTL for this record. */
public long ttl;
/** Soft TTL for this record. */
public long softTtl;
/** Immutable response headers as received from server; must be non-null. */
public Map<String, String> responseHeaders = Collections.emptyMap();
/** True if the entry is expired. */
public boolean isExpired() {
return this.ttl < System.currentTimeMillis();
}
/** True if a refresh is needed from the original data source. */
public boolean refreshNeeded() {
return this.softTtl < System.currentTimeMillis();
}
}
}

View File

@ -0,0 +1,159 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
import android.os.Process;
import java.util.concurrent.BlockingQueue;
/**
* Provides a thread for performing cache triage on a queue of requests.
*
* Requests added to the specified cache queue are resolved from cache.
* Any deliverable response is posted back to the caller via a
* {@link ResponseDelivery}. Cache misses and responses that require
* refresh are enqueued on the specified network queue for processing
* by a {@link NetworkDispatcher}.
*/
@SuppressWarnings("rawtypes")
public class CacheDispatcher extends Thread {
private static final boolean DEBUG = VolleyLog.DEBUG;
/** The queue of requests coming in for triage. */
private final BlockingQueue<Request> mCacheQueue;
/** The queue of requests going out to the network. */
private final BlockingQueue<Request> mNetworkQueue;
/** The cache to read from. */
private final Cache mCache;
/** For posting responses. */
private final ResponseDelivery mDelivery;
/** Used for telling us to die. */
private volatile boolean mQuit = false;
/**
* Creates a new cache triage dispatcher thread. You must call {@link #start()}
* in order to begin processing.
*
* @param cacheQueue Queue of incoming requests for triage
* @param networkQueue Queue to post requests that require network to
* @param cache Cache interface to use for resolution
* @param delivery Delivery interface to use for posting responses
*/
public CacheDispatcher(
BlockingQueue<Request> cacheQueue, BlockingQueue<Request> networkQueue,
Cache cache, ResponseDelivery delivery) {
mCacheQueue = cacheQueue;
mNetworkQueue = networkQueue;
mCache = cache;
mDelivery = delivery;
}
/**
* Forces this dispatcher to quit immediately. If any requests are still in
* the queue, they are not guaranteed to be processed.
*/
public void quit() {
mQuit = true;
interrupt();
}
@Override
public void run() {
if (DEBUG) VolleyLog.v("start new dispatcher");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// Make a blocking call to initialize the cache.
mCache.initialize();
while (true) {
try {
// Get a request from the cache triage queue, blocking until
// at least one is available.
final Request request = mCacheQueue.take();
request.addMarker("cache-queue-take");
// If the request has been canceled, don't bother dispatching it.
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
continue;
}
// Attempt to retrieve this item from cache.
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
// Cache miss; send off to the network dispatcher.
mNetworkQueue.put(request);
continue;
}
// If it is completely expired, just send it to the network.
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}
// We have a cache hit; parse its data for delivery back to the request.
request.addMarker("cache-hit");
Response<?> response = request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
if (!entry.refreshNeeded()) {
// Completely unexpired cache hit. Just deliver the response.
mDelivery.postResponse(request, response);
} else {
// Soft-expired cache hit. We can deliver the cached response,
// but we need to also send the request to the network for
// refreshing.
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
// Mark the response as intermediate.
response.intermediate = true;
// Post the intermediate response back to the user and have
// the delivery then forward the request along to the network.
mDelivery.postResponse(request, response, new Runnable() {
@Override
public void run() {
try {
mNetworkQueue.put(request);
} catch (InterruptedException e) {
// Not much we can do about this.
}
}
});
}
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
return;
}
continue;
}
}
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
/**
* Default retry policy for requests.
*/
public class DefaultRetryPolicy implements RetryPolicy {
/** The current timeout in milliseconds. */
private int mCurrentTimeoutMs;
/** The current retry count. */
private int mCurrentRetryCount;
/** The maximum number of attempts. */
private final int mMaxNumRetries;
/** The backoff multiplier for for the policy. */
private final float mBackoffMultiplier;
/** The default socket timeout in milliseconds */
public static final int DEFAULT_TIMEOUT_MS = 2500;
/** The default number of retries */
public static final int DEFAULT_MAX_RETRIES = 1;
/** The default backoff multiplier */
public static final float DEFAULT_BACKOFF_MULT = 1f;
/**
* Constructs a new retry policy using the default timeouts.
*/
public DefaultRetryPolicy() {
this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
}
/**
* Constructs a new retry policy.
* @param initialTimeoutMs The initial timeout for the policy.
* @param maxNumRetries The maximum number of retries.
* @param backoffMultiplier Backoff multiplier for the policy.
*/
public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
mCurrentTimeoutMs = initialTimeoutMs;
mMaxNumRetries = maxNumRetries;
mBackoffMultiplier = backoffMultiplier;
}
/**
* Returns the current timeout.
*/
@Override
public int getCurrentTimeout() {
return mCurrentTimeoutMs;
}
/**
* Returns the current retry count.
*/
@Override
public int getCurrentRetryCount() {
return mCurrentRetryCount;
}
/**
* Prepares for the next retry by applying a backoff to the timeout.
* @param error The error code of the last attempt.
*/
@Override
public void retry(VolleyError error) throws VolleyError {
mCurrentRetryCount++;
mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
if (!hasAttemptRemaining()) {
throw error;
}
}
/**
* Returns true if this policy has attempts remaining, false otherwise.
*/
protected boolean hasAttemptRemaining() {
return mCurrentRetryCount <= mMaxNumRetries;
}
}

View File

@ -0,0 +1,118 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
import android.os.Handler;
import java.util.concurrent.Executor;
/**
* Delivers responses and errors.
*/
public class ExecutorDelivery implements ResponseDelivery {
/** Used for posting responses, typically to the main thread. */
private final Executor mResponsePoster;
/**
* Creates a new response delivery interface.
* @param handler {@link Handler} to post responses on
*/
public ExecutorDelivery(final Handler handler) {
// Make an Executor that just wraps the handler.
mResponsePoster = new Executor() {
@Override
public void execute(Runnable command) {
handler.post(command);
}
};
}
/**
* Creates a new response delivery interface, mockable version
* for testing.
* @param executor For running delivery tasks
*/
public ExecutorDelivery(Executor executor) {
mResponsePoster = executor;
}
@Override
public void postResponse(Request<?> request, Response<?> response) {
postResponse(request, response, null);
}
@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
request.markDelivered();
request.addMarker("post-response");
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}
@Override
public void postError(Request<?> request, VolleyError error) {
request.addMarker("post-error");
Response<?> response = Response.error(error);
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));
}
/**
* A Runnable used for delivering network responses to a listener on the
* main thread.
*/
@SuppressWarnings("rawtypes")
private class ResponseDeliveryRunnable implements Runnable {
private final Request mRequest;
private final Response mResponse;
private final Runnable mRunnable;
public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
mRequest = request;
mResponse = response;
mRunnable = runnable;
}
@SuppressWarnings("unchecked")
@Override
public void run() {
// If this request has canceled, finish it and don't deliver.
if (mRequest.isCanceled()) {
mRequest.finish("canceled-at-delivery");
return;
}
// Deliver a normal response or error, depending.
if (mResponse.isSuccess()) {
mRequest.deliverResponse(mResponse.result);
} else {
mRequest.deliverError(mResponse.error);
}
// If this is an intermediate response, add a marker, otherwise we're done
// and the request can be finished.
if (mResponse.intermediate) {
mRequest.addMarker("intermediate-response");
} else {
mRequest.finish("done");
}
// If we have been provided a post-delivery runnable, run it.
if (mRunnable != null) {
mRunnable.run();
}
}
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
/**
* An interface for performing requests.
*/
public interface Network {
/**
* Performs the specified request.
* @param request Request to process
* @return A {@link NetworkResponse} with data and caching metadata; will never be null
* @throws VolleyError on errors
*/
public NetworkResponse performRequest(Request<?> request) throws VolleyError;
}

View File

@ -0,0 +1,142 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
import android.net.TrafficStats;
import android.os.Build;
import android.os.Process;
import java.util.concurrent.BlockingQueue;
/**
* Provides a thread for performing network dispatch from a queue of requests.
*
* Requests added to the specified queue are processed from the network via a
* specified {@link Network} interface. Responses are committed to cache, if
* eligible, using a specified {@link Cache} interface. Valid responses and
* errors are posted back to the caller via a {@link ResponseDelivery}.
*/
@SuppressWarnings("rawtypes")
public class NetworkDispatcher extends Thread {
/** The queue of requests to service. */
private final BlockingQueue<Request> mQueue;
/** The network interface for processing requests. */
private final Network mNetwork;
/** The cache to write to. */
private final Cache mCache;
/** For posting responses and errors. */
private final ResponseDelivery mDelivery;
/** Used for telling us to die. */
private volatile boolean mQuit = false;
/**
* Creates a new network dispatcher thread. You must call {@link #start()}
* in order to begin processing.
*
* @param queue Queue of incoming requests for triage
* @param network Network interface to use for performing requests
* @param cache Cache interface to use for writing responses to cache
* @param delivery Delivery interface to use for posting responses
*/
public NetworkDispatcher(BlockingQueue<Request> queue,
Network network, Cache cache,
ResponseDelivery delivery) {
mQueue = queue;
mNetwork = network;
mCache = cache;
mDelivery = delivery;
}
/**
* Forces this dispatcher to quit immediately. If any requests are still in
* the queue, they are not guaranteed to be processed.
*/
public void quit() {
mQuit = true;
interrupt();
}
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Request request;
while (true) {
try {
// Take a request from the queue.
request = mQueue.take();
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
return;
}
continue;
}
try {
request.addMarker("network-queue-take");
// If the request was cancelled already, do not perform the
// network request.
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
continue;
}
// Tag the request (if API >= 14)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
TrafficStats.setThreadStatsTag(request.getTrafficStatsTag());
}
// Perform the network request.
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");
// If the server returned 304 AND we delivered a response already,
// we're done -- don't deliver a second identical response.
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}
// Parse the response here on the worker thread.
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
// Write to cache if applicable.
// TODO: Only update cache metadata instead of entire record for 304s.
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
// Post the response back.
request.markDelivered();
mDelivery.postResponse(request, response);
} catch (VolleyError volleyError) {
parseAndDeliverNetworkError(request, volleyError);
} catch (Exception e) {
VolleyLog.e(e, "Unhandled exception %s", e.toString());
mDelivery.postError(request, new VolleyError(e));
}
}
}
private void parseAndDeliverNetworkError(Request<?> request, VolleyError error) {
error = request.parseNetworkError(error);
mDelivery.postError(request, error);
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
/**
* Indicates that there was a network error when performing a Volley request.
*/
@SuppressWarnings("serial")
public class NetworkError extends VolleyError {
public NetworkError() {
super();
}
public NetworkError(Throwable cause) {
super(cause);
}
public NetworkError(NetworkResponse networkResponse) {
super(networkResponse);
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
import org.apache.http.HttpStatus;
import java.util.Collections;
import java.util.Map;
/**
* Data and headers returned from {@link Network#performRequest(Request)}.
*/
public class NetworkResponse {
/**
* Creates a new network response.
* @param statusCode the HTTP status code
* @param data Response body
* @param headers Headers returned with this response, or null for none
* @param notModified True if the server returned a 304 and the data was already in cache
*/
public NetworkResponse(int statusCode, byte[] data, Map<String, String> headers,
boolean notModified) {
this.statusCode = statusCode;
this.data = data;
this.headers = headers;
this.notModified = notModified;
}
public NetworkResponse(byte[] data) {
this(HttpStatus.SC_OK, data, Collections.<String, String>emptyMap(), false);
}
public NetworkResponse(byte[] data, Map<String, String> headers) {
this(HttpStatus.SC_OK, data, headers, false);
}
/** The HTTP status code. */
public final int statusCode;
/** Raw data from this response. */
public final byte[] data;
/** Response headers. */
public final Map<String, String> headers;
/** True if the server returned a 304 (Not Modified). */
public final boolean notModified;
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
/**
* Error indicating that no connection could be established when performing a Volley request.
*/
@SuppressWarnings("serial")
public class NoConnectionError extends NetworkError {
public NoConnectionError() {
super();
}
public NoConnectionError(Throwable reason) {
super(reason);
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
/**
* Indicates that the server's response could not be parsed.
*/
@SuppressWarnings("serial")
public class ParseError extends VolleyError {
public ParseError() { }
public ParseError(NetworkResponse networkResponse) {
super(networkResponse);
}
public ParseError(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,543 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
import android.net.TrafficStats;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.text.TextUtils;
import com.android.volley.VolleyLog.MarkerLog;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.Map;
/**
* Base class for all network requests.
*
* @param <T> The type of parsed response this request expects.
*/
public abstract class Request<T> implements Comparable<Request<T>> {
/**
* Default encoding for POST or PUT parameters. See {@link #getParamsEncoding()}.
*/
private static final String DEFAULT_PARAMS_ENCODING = "UTF-8";
/**
* Supported request methods.
*/
public interface Method {
int DEPRECATED_GET_OR_POST = -1;
int GET = 0;
int POST = 1;
int PUT = 2;
int DELETE = 3;
}
/** An event log tracing the lifetime of this request; for debugging. */
private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null;
/** Request method of this request. Currently supports GET, POST, PUT, and DELETE. */
private final int mMethod;
/** URL of this request. */
private final String mUrl;
/** Default tag for {@link TrafficStats}. */
private final int mDefaultTrafficStatsTag;
/** Listener interface for errors. */
private final Response.ErrorListener mErrorListener;
/** Sequence number of this request, used to enforce FIFO ordering. */
private Integer mSequence;
/** The request queue this request is associated with. */
private RequestQueue mRequestQueue;
/** Whether or not responses to this request should be cached. */
private boolean mShouldCache = true;
/** Whether or not this request has been canceled. */
private boolean mCanceled = false;
/** Whether or not a response has been delivered for this request yet. */
private boolean mResponseDelivered = false;
// A cheap variant of request tracing used to dump slow requests.
private long mRequestBirthTime = 0;
/** Threshold at which we should log the request (even when debug logging is not enabled). */
private static final long SLOW_REQUEST_THRESHOLD_MS = 3000;
/** The retry policy for this request. */
private RetryPolicy mRetryPolicy;
/**
* When a request can be retrieved from cache but must be refreshed from
* the network, the cache entry will be stored here so that in the event of
* a "Not Modified" response, we can be sure it hasn't been evicted from cache.
*/
private Cache.Entry mCacheEntry = null;
/** An opaque token tagging this request; used for bulk cancellation. */
private Object mTag;
/**
* Creates a new request with the given URL and error listener. Note that
* the normal response listener is not provided here as delivery of responses
* is provided by subclasses, who have a better idea of how to deliver an
* already-parsed response.
*
* @deprecated Use {@link #Request(int, String, com.android.volley.Response.ErrorListener)}.
*/
public Request(String url, Response.ErrorListener listener) {
this(Method.DEPRECATED_GET_OR_POST, url, listener);
}
/**
* Creates a new request with the given method (one of the values from {@link Method}),
* URL, and error listener. Note that the normal response listener is not provided here as
* delivery of responses is provided by subclasses, who have a better idea of how to deliver
* an already-parsed response.
*/
public Request(int method, String url, Response.ErrorListener listener) {
mMethod = method;
mUrl = url;
mErrorListener = listener;
setRetryPolicy(new DefaultRetryPolicy());
mDefaultTrafficStatsTag = TextUtils.isEmpty(url) ? 0: Uri.parse(url).getHost().hashCode();
}
/**
* Return the method for this request. Can be one of the values in {@link Method}.
*/
public int getMethod() {
return mMethod;
}
/**
* Set a tag on this request. Can be used to cancel all requests with this
* tag by {@link RequestQueue#cancelAll(Object)}.
*/
public void setTag(Object tag) {
mTag = tag;
}
/**
* Returns this request's tag.
* @see Request#setTag(Object)
*/
public Object getTag() {
return mTag;
}
/**
* @return A tag for use with {@link TrafficStats#setThreadStatsTag(int)}
*/
public int getTrafficStatsTag() {
return mDefaultTrafficStatsTag;
}
/**
* Sets the retry policy for this request.
*/
public void setRetryPolicy(RetryPolicy retryPolicy) {
mRetryPolicy = retryPolicy;
}
/**
* Adds an event to this request's event log; for debugging.
*/
public void addMarker(String tag) {
if (MarkerLog.ENABLED) {
mEventLog.add(tag, Thread.currentThread().getId());
} else if (mRequestBirthTime == 0) {
mRequestBirthTime = SystemClock.elapsedRealtime();
}
}
/**
* Notifies the request queue that this request has finished (successfully or with error).
*
* <p>Also dumps all events from this request's event log; for debugging.</p>
*/
void finish(final String tag) {
if (mRequestQueue != null) {
mRequestQueue.finish(this);
}
if (MarkerLog.ENABLED) {
final long threadId = Thread.currentThread().getId();
if (Looper.myLooper() != Looper.getMainLooper()) {
// If we finish marking off of the main thread, we need to
// actually do it on the main thread to ensure correct ordering.
Handler mainThread = new Handler(Looper.getMainLooper());
mainThread.post(new Runnable() {
@Override
public void run() {
mEventLog.add(tag, threadId);
mEventLog.finish(this.toString());
}
});
return;
}
mEventLog.add(tag, threadId);
mEventLog.finish(this.toString());
} else {
long requestTime = SystemClock.elapsedRealtime() - mRequestBirthTime;
if (requestTime >= SLOW_REQUEST_THRESHOLD_MS) {
VolleyLog.d("%d ms: %s", requestTime, this.toString());
}
}
}
/**
* Associates this request with the given queue. The request queue will be notified when this
* request has finished.
*/
public void setRequestQueue(RequestQueue requestQueue) {
mRequestQueue = requestQueue;
}
/**
* Sets the sequence number of this request. Used by {@link RequestQueue}.
*/
public final void setSequence(int sequence) {
mSequence = sequence;
}
/**
* Returns the sequence number of this request.
*/
public final int getSequence() {
if (mSequence == null) {
throw new IllegalStateException("getSequence called before setSequence");
}
return mSequence;
}
/**
* Returns the URL of this request.
*/
public String getUrl() {
return mUrl;
}
/**
* Returns the cache key for this request. By default, this is the URL.
*/
public String getCacheKey() {
return getUrl();
}
/**
* Annotates this request with an entry retrieved for it from cache.
* Used for cache coherency support.
*/
public void setCacheEntry(Cache.Entry entry) {
mCacheEntry = entry;
}
/**
* Returns the annotated cache entry, or null if there isn't one.
*/
public Cache.Entry getCacheEntry() {
return mCacheEntry;
}
/**
* Mark this request as canceled. No callback will be delivered.
*/
public void cancel() {
mCanceled = true;
}
/**
* Returns true if this request has been canceled.
*/
public boolean isCanceled() {
return mCanceled;
}
/**
* Returns a list of extra HTTP headers to go along with this request. Can
* throw {@link AuthFailureError} as authentication may be required to
* provide these values.
* @throws AuthFailureError In the event of auth failure
*/
public Map<String, String> getHeaders() throws AuthFailureError {
return Collections.emptyMap();
}
/**
* Returns a Map of POST parameters to be used for this request, or null if
* a simple GET should be used. Can throw {@link AuthFailureError} as
* authentication may be required to provide these values.
*
* <p>Note that only one of getPostParams() and getPostBody() can return a non-null
* value.</p>
* @throws AuthFailureError In the event of auth failure
*
* @deprecated Use {@link #getParams()} instead.
*/
protected Map<String, String> getPostParams() throws AuthFailureError {
return getParams();
}
/**
* Returns which encoding should be used when converting POST parameters returned by
* {@link #getPostParams()} into a raw POST body.
*
* <p>This controls both encodings:
* <ol>
* <li>The string encoding used when converting parameter names and values into bytes prior
* to URL encoding them.</li>
* <li>The string encoding used when converting the URL encoded parameters into a raw
* byte array.</li>
* </ol>
*
* @deprecated Use {@link #getParamsEncoding()} instead.
*/
protected String getPostParamsEncoding() {
return getParamsEncoding();
}
/**
* @deprecated Use {@link #getBodyContentType()} instead.
*/
public String getPostBodyContentType() {
return getBodyContentType();
}
/**
* Returns the raw POST body to be sent.
*
* @throws AuthFailureError In the event of auth failure
*
* @deprecated Use {@link #getBody()} instead.
*/
public byte[] getPostBody() throws AuthFailureError {
// Note: For compatibility with legacy clients of volley, this implementation must remain
// here instead of simply calling the getBody() function because this function must
// call getPostParams() and getPostParamsEncoding() since legacy clients would have
// overridden these two member functions for POST requests.
Map<String, String> postParams = getPostParams();
if (postParams != null && postParams.size() > 0) {
return encodeParameters(postParams, getPostParamsEncoding());
}
return null;
}
/**
* Returns a Map of parameters to be used for a POST or PUT request. Can throw
* {@link AuthFailureError} as authentication may be required to provide these values.
*
* <p>Note that you can directly override {@link #getBody()} for custom data.</p>
*
* @throws AuthFailureError in the event of auth failure
*/
protected Map<String, String> getParams() throws AuthFailureError {
return null;
}
/**
* Returns which encoding should be used when converting POST or PUT parameters returned by
* {@link #getParams()} into a raw POST or PUT body.
*
* <p>This controls both encodings:
* <ol>
* <li>The string encoding used when converting parameter names and values into bytes prior
* to URL encoding them.</li>
* <li>The string encoding used when converting the URL encoded parameters into a raw
* byte array.</li>
* </ol>
*/
protected String getParamsEncoding() {
return DEFAULT_PARAMS_ENCODING;
}
public String getBodyContentType() {
return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
}
/**
* Returns the raw POST or PUT body to be sent.
*
* @throws AuthFailureError in the event of auth failure
*/
public byte[] getBody() throws AuthFailureError {
Map<String, String> params = getParams();
if (params != null && params.size() > 0) {
return encodeParameters(params, getParamsEncoding());
}
return null;
}
/**
* Converts <code>params</code> into an application/x-www-form-urlencoded encoded string.
*/
private byte[] encodeParameters(Map<String, String> params, String paramsEncoding) {
StringBuilder encodedParams = new StringBuilder();
try {
for (Map.Entry<String, String> entry : params.entrySet()) {
encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding));
encodedParams.append('=');
encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding));
encodedParams.append('&');
}
return encodedParams.toString().getBytes(paramsEncoding);
} catch (UnsupportedEncodingException uee) {
throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee);
}
}
/**
* Set whether or not responses to this request should be cached.
*/
public final void setShouldCache(boolean shouldCache) {
mShouldCache = shouldCache;
}
/**
* Returns true if responses to this request should be cached.
*/
public final boolean shouldCache() {
return mShouldCache;
}
/**
* Priority values. Requests will be processed from higher priorities to
* lower priorities, in FIFO order.
*/
public enum Priority {
LOW,
NORMAL,
HIGH,
IMMEDIATE
}
/**
* Returns the {@link Priority} of this request; {@link Priority#NORMAL} by default.
*/
public Priority getPriority() {
return Priority.NORMAL;
}
/**
* Returns the socket timeout in milliseconds per retry attempt. (This value can be changed
* per retry attempt if a backoff is specified via backoffTimeout()). If there are no retry
* attempts remaining, this will cause delivery of a {@link TimeoutError} error.
*/
public final int getTimeoutMs() {
return mRetryPolicy.getCurrentTimeout();
}
/**
* Returns the retry policy that should be used for this request.
*/
public RetryPolicy getRetryPolicy() {
return mRetryPolicy;
}
/**
* Mark this request as having a response delivered on it. This can be used
* later in the request's lifetime for suppressing identical responses.
*/
public void markDelivered() {
mResponseDelivered = true;
}
/**
* Returns true if this request has had a response delivered for it.
*/
public boolean hasHadResponseDelivered() {
return mResponseDelivered;
}
/**
* Subclasses must implement this to parse the raw network response
* and return an appropriate response type. This method will be
* called from a worker thread. The response will not be delivered
* if you return null.
* @param response Response from the network
* @return The parsed response, or null in the case of an error
*/
abstract protected Response<T> parseNetworkResponse(NetworkResponse response);
/**
* Subclasses can override this method to parse 'networkError' and return a more specific error.
*
* <p>The default implementation just returns the passed 'networkError'.</p>
*
* @param volleyError the error retrieved from the network
* @return an NetworkError augmented with additional information
*/
protected VolleyError parseNetworkError(VolleyError volleyError) {
return volleyError;
}
/**
* Subclasses must implement this to perform delivery of the parsed
* response to their listeners. The given response is guaranteed to
* be non-null; responses that fail to parse are not delivered.
* @param response The parsed response returned by
* {@link #parseNetworkResponse(NetworkResponse)}
*/
abstract protected void deliverResponse(T response);
/**
* Delivers error message to the ErrorListener that the Request was
* initialized with.
*
* @param error Error details
*/
public void deliverError(VolleyError error) {
if (mErrorListener != null) {
mErrorListener.onErrorResponse(error);
}
}
/**
* Our comparator sorts from high to low priority, and secondarily by
* sequence number to provide FIFO ordering.
*/
@Override
public int compareTo(Request<T> other) {
Priority left = this.getPriority();
Priority right = other.getPriority();
// High-priority requests are "lesser" so they are sorted to the front.
// Equal priorities are sorted by sequence number to provide FIFO ordering.
return left == right ?
this.mSequence - other.mSequence :
right.ordinal() - left.ordinal();
}
@Override
public String toString() {
String trafficStatsTag = "0x" + Integer.toHexString(getTrafficStatsTag());
return (mCanceled ? "[X] " : "[ ] ") + getUrl() + " " + trafficStatsTag + " "
+ getPriority() + " " + mSequence;
}
}

View File

@ -0,0 +1,287 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
import android.os.Handler;
import android.os.Looper;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A request dispatch queue with a thread pool of dispatchers.
*
* Calling {@link #add(Request)} will enqueue the given Request for dispatch,
* resolving from either cache or network on a worker thread, and then delivering
* a parsed response on the main thread.
*/
@SuppressWarnings("rawtypes")
public class RequestQueue {
/** Used for generating monotonically-increasing sequence numbers for requests. */
private AtomicInteger mSequenceGenerator = new AtomicInteger();
/**
* Staging area for requests that already have a duplicate request in flight.
*
* <ul>
* <li>containsKey(cacheKey) indicates that there is a request in flight for the given cache
* key.</li>
* <li>get(cacheKey) returns waiting requests for the given cache key. The in flight request
* is <em>not</em> contained in that list. Is null if no requests are staged.</li>
* </ul>
*/
private final Map<String, Queue<Request>> mWaitingRequests =
new HashMap<String, Queue<Request>>();
/**
* The set of all requests currently being processed by this RequestQueue. A Request
* will be in this set if it is waiting in any queue or currently being processed by
* any dispatcher.
*/
private final Set<Request> mCurrentRequests = new HashSet<Request>();
/** The cache triage queue. */
private final PriorityBlockingQueue<Request> mCacheQueue =
new PriorityBlockingQueue<Request>();
/** The queue of requests that are actually going out to the network. */
private final PriorityBlockingQueue<Request> mNetworkQueue =
new PriorityBlockingQueue<Request>();
/** Number of network request dispatcher threads to start. */
private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
/** Cache interface for retrieving and storing respones. */
private final Cache mCache;
/** Network interface for performing requests. */
private final Network mNetwork;
/** Response delivery mechanism. */
private final ResponseDelivery mDelivery;
/** The network dispatchers. */
private NetworkDispatcher[] mDispatchers;
/** The cache dispatcher. */
private CacheDispatcher mCacheDispatcher;
/**
* Creates the worker pool. Processing will not begin until {@link #start()} is called.
*
* @param cache A Cache to use for persisting responses to disk
* @param network A Network interface for performing HTTP requests
* @param threadPoolSize Number of network dispatcher threads to create
* @param delivery A ResponseDelivery interface for posting responses and errors
*/
public RequestQueue(Cache cache, Network network, int threadPoolSize,
ResponseDelivery delivery) {
mCache = cache;
mNetwork = network;
mDispatchers = new NetworkDispatcher[threadPoolSize];
mDelivery = delivery;
}
/**
* Creates the worker pool. Processing will not begin until {@link #start()} is called.
*
* @param cache A Cache to use for persisting responses to disk
* @param network A Network interface for performing HTTP requests
* @param threadPoolSize Number of network dispatcher threads to create
*/
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
this(cache, network, threadPoolSize,
new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}
/**
* Creates the worker pool. Processing will not begin until {@link #start()} is called.
*
* @param cache A Cache to use for persisting responses to disk
* @param network A Network interface for performing HTTP requests
*/
public RequestQueue(Cache cache, Network network) {
this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
}
/**
* Starts the dispatchers in this queue.
*/
public void start() {
stop(); // Make sure any currently running dispatchers are stopped.
// Create the cache dispatcher and start it.
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();
// Create network dispatchers (and corresponding threads) up to the pool size.
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
/**
* Stops the cache and network dispatchers.
*/
public void stop() {
if (mCacheDispatcher != null) {
mCacheDispatcher.quit();
}
for (int i = 0; i < mDispatchers.length; i++) {
if (mDispatchers[i] != null) {
mDispatchers[i].quit();
}
}
}
/**
* Gets a sequence number.
*/
public int getSequenceNumber() {
return mSequenceGenerator.incrementAndGet();
}
/**
* Gets the {@link Cache} instance being used.
*/
public Cache getCache() {
return mCache;
}
/**
* A simple predicate or filter interface for Requests, for use by
* {@link RequestQueue#cancelAll(RequestFilter)}.
*/
public interface RequestFilter {
public boolean apply(Request<?> request);
}
/**
* Cancels all requests in this queue for which the given filter applies.
* @param filter The filtering function to use
*/
public void cancelAll(RequestFilter filter) {
synchronized (mCurrentRequests) {
for (Request<?> request : mCurrentRequests) {
if (filter.apply(request)) {
request.cancel();
}
}
}
}
/**
* Cancels all requests in this queue with the given tag. Tag must be non-null
* and equality is by identity.
*/
public void cancelAll(final Object tag) {
if (tag == null) {
throw new IllegalArgumentException("Cannot cancelAll with a null tag");
}
cancelAll(new RequestFilter() {
@Override
public boolean apply(Request<?> request) {
return request.getTag() == tag;
}
});
}
/**
* Adds a Request to the dispatch queue.
* @param request The request to service
* @return The passed-in request
*/
public Request add(Request request) {
// Tag the request as belonging to this queue and add it to the set of current requests.
request.setRequestQueue(this);
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}
// Process requests in the order they are added.
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");
// If the request is uncacheable, skip the cache queue and go straight to the network.
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
// Insert request into stage if there's already a request with the same cache key in flight.
synchronized (mWaitingRequests) {
String cacheKey = request.getCacheKey();
if (mWaitingRequests.containsKey(cacheKey)) {
// There is already a request in flight. Queue up.
Queue<Request> stagedRequests = mWaitingRequests.get(cacheKey);
if (stagedRequests == null) {
stagedRequests = new LinkedList<Request>();
}
stagedRequests.add(request);
mWaitingRequests.put(cacheKey, stagedRequests);
if (VolleyLog.DEBUG) {
VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
}
} else {
// Insert 'null' queue for this cacheKey, indicating there is now a request in
// flight.
mWaitingRequests.put(cacheKey, null);
mCacheQueue.add(request);
}
return request;
}
}
/**
* Called from {@link Request#finish(String)}, indicating that processing of the given request
* has finished.
*
* <p>Releases waiting requests for <code>request.getCacheKey()</code> if
* <code>request.shouldCache()</code>.</p>
*/
void finish(Request request) {
// Remove from the set of requests currently being processed.
synchronized (mCurrentRequests) {
mCurrentRequests.remove(request);
}
if (request.shouldCache()) {
synchronized (mWaitingRequests) {
String cacheKey = request.getCacheKey();
Queue<Request> waitingRequests = mWaitingRequests.remove(cacheKey);
if (waitingRequests != null) {
if (VolleyLog.DEBUG) {
VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
waitingRequests.size(), cacheKey);
}
// Process all queued up requests. They won't be considered as in flight, but
// that's not a problem as the cache has been primed by 'request'.
mCacheQueue.addAll(waitingRequests);
}
}
}
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
/**
* Encapsulates a parsed response for delivery.
*
* @param <T> Parsed type of this response
*/
public class Response<T> {
/** Callback interface for delivering parsed responses. */
public interface Listener<T> {
/** Called when a response is received. */
public void onResponse(T response);
}
/** Callback interface for delivering error responses. */
public interface ErrorListener {
/**
* Callback method that an error has been occurred with the
* provided error code and optional user-readable message.
*/
public void onErrorResponse(VolleyError error);
}
/** Returns a successful response containing the parsed result. */
public static <T> Response<T> success(T result, Cache.Entry cacheEntry) {
return new Response<T>(result, cacheEntry);
}
/**
* Returns a failed response containing the given error code and an optional
* localized message displayed to the user.
*/
public static <T> Response<T> error(VolleyError error) {
return new Response<T>(error);
}
/** Parsed response, or null in the case of error. */
public final T result;
/** Cache metadata for this response, or null in the case of error. */
public final Cache.Entry cacheEntry;
/** Detailed error information if <code>errorCode != OK</code>. */
public final VolleyError error;
/** True if this response was a soft-expired one and a second one MAY be coming. */
public boolean intermediate = false;
/**
* Returns whether this response is considered successful.
*/
public boolean isSuccess() {
return error == null;
}
private Response(T result, Cache.Entry cacheEntry) {
this.result = result;
this.cacheEntry = cacheEntry;
this.error = null;
}
private Response(VolleyError error) {
this.result = null;
this.cacheEntry = null;
this.error = error;
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
public interface ResponseDelivery {
/**
* Parses a response from the network or cache and delivers it.
*/
public void postResponse(Request<?> request, Response<?> response);
/**
* Parses a response from the network or cache and delivers it. The provided
* Runnable will be executed after delivery.
*/
public void postResponse(Request<?> request, Response<?> response, Runnable runnable);
/**
* Posts an error for the given request.
*/
public void postError(Request<?> request, VolleyError error);
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
/**
* Retry policy for a request.
*/
public interface RetryPolicy {
/**
* Returns the current timeout (used for logging).
*/
public int getCurrentTimeout();
/**
* Returns the current retry count (used for logging).
*/
public int getCurrentRetryCount();
/**
* Prepares for the next retry by applying a backoff to the timeout.
* @param error The error code of the last attempt.
* @throws VolleyError In the event that the retry could not be performed (for example if we
* ran out of attempts), the passed in error is thrown.
*/
public void retry(VolleyError error) throws VolleyError;
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
/**
* Indicates that the error responded with an error response.
*/
@SuppressWarnings("serial")
public class ServerError extends VolleyError {
public ServerError(NetworkResponse networkResponse) {
super(networkResponse);
}
public ServerError() {
super();
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.volley;
/**
* Indicates that the connection or the socket timed out.
*/
@SuppressWarnings("serial")
public class TimeoutError extends VolleyError { }

Some files were not shown because too many files have changed in this diff Show More